diff --git a/assets/images/empty_inbox.png b/assets/images/empty_inbox.png new file mode 100644 index 0000000..b398276 Binary files /dev/null and b/assets/images/empty_inbox.png differ diff --git a/lib/core/bloc/paperless_statistics_cubit.dart b/lib/core/bloc/paperless_statistics_cubit.dart deleted file mode 100644 index e2d1f0e..0000000 --- a/lib/core/bloc/paperless_statistics_cubit.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'dart:math'; - -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:injectable/injectable.dart'; -import 'package:paperless_mobile/core/model/paperless_statistics.dart'; -import 'package:paperless_mobile/core/model/paperless_statistics_state.dart'; -import 'package:paperless_mobile/core/service/paperless_statistics_service.dart'; - -@singleton -class PaperlessStatisticsCubit extends Cubit { - final PaperlessStatisticsService statisticsService; - - PaperlessStatisticsCubit(this.statisticsService) - : super(PaperlessStatisticsState(isLoaded: false)); - - Future updateStatistics() async { - final stats = await statisticsService.getStatistics(); - emit(PaperlessStatisticsState(isLoaded: true, statistics: stats)); - } - - void decrementInboxCount() { - if (state.isLoaded) { - emit( - PaperlessStatisticsState( - isLoaded: true, - statistics: PaperlessStatistics( - documentsInInbox: max(0, state.statistics!.documentsInInbox - 1), - documentsTotal: state.statistics!.documentsTotal, - ), - ), - ); - } - } - - void incrementInboxCount() { - if (state.isLoaded) { - emit( - PaperlessStatisticsState( - isLoaded: true, - statistics: PaperlessStatistics( - documentsInInbox: state.statistics!.documentsInInbox + 1, - documentsTotal: state.statistics!.documentsTotal, - ), - ), - ); - } - } - - void resetInboxCount() { - if (state.isLoaded) { - emit( - PaperlessStatisticsState( - isLoaded: true, - statistics: PaperlessStatistics( - documentsInInbox: 0, - documentsTotal: state.statistics!.documentsTotal, - ), - ), - ); - } - } -} diff --git a/lib/core/global/asset_images.dart b/lib/core/global/asset_images.dart index f7807d1..3613158 100644 --- a/lib/core/global/asset_images.dart +++ b/lib/core/global/asset_images.dart @@ -4,16 +4,16 @@ enum AssetImages { headacheDocuments("images/documents_headache.png"), organizeDocuments("images/organize_documents.png"), secureDocuments("images/secure_documents.png"), - success("images/success.png"); + success("images/success.png"), + emptyInbox("images/empty_inbox.png"); final String relativePath; const AssetImages(String relativePath) : relativePath = "assets/$relativePath"; - Image get image => Image.asset( - relativePath, - key: ObjectKey("assetimage_$relativePath"), - ); + AssetImage get image => AssetImage(relativePath); - void load(context) => precacheImage(image.image, context); + void load(context) => precacheImage(image, context); } + +late Image emptyInboxImage; diff --git a/lib/core/interceptor/authentication.interceptor.dart b/lib/core/interceptor/authentication.interceptor.dart index 6252f72..5c0aba9 100644 --- a/lib/core/interceptor/authentication.interceptor.dart +++ b/lib/core/interceptor/authentication.interceptor.dart @@ -15,7 +15,7 @@ class AuthenticationInterceptor implements InterceptorContract { Future interceptRequest({required BaseRequest request}) async { final authState = authenticationCubit.state; if (kDebugMode) { - log("Intercepted request to ${request.url.toString()}"); + log("Intercepted ${request.method} request to ${request.url.toString()}"); } if (authState.authentication == null) { throw const ErrorMessage(ErrorCode.notAuthenticated); diff --git a/lib/features/app_intro/application_intro_slideshow.dart b/lib/features/app_intro/application_intro_slideshow.dart index 4f97d20..17a5442 100644 --- a/lib/features/app_intro/application_intro_slideshow.dart +++ b/lib/features/app_intro/application_intro_slideshow.dart @@ -18,9 +18,8 @@ class ApplicationIntroSlideshow extends StatefulWidget { } class _ApplicationIntroSlideshowState extends State { - Image organizeImage = AssetImages.organizeDocuments.image; - Image secureImage = AssetImages.secureDocuments.image; - Image successImage = AssetImages.success.image; + AssetImage secureImage = AssetImages.secureDocuments.image; + AssetImage successImage = AssetImages.success.image; @override Widget build(BuildContext context) { @@ -50,7 +49,9 @@ class _ApplicationIntroSlideshowState extends State { ), image: Padding( padding: const EdgeInsets.all(8.0), - child: organizeImage, + child: Image( + image: AssetImages.organizeDocuments.image, + ), ), bodyWidget: Column( crossAxisAlignment: CrossAxisAlignment.center, @@ -70,7 +71,7 @@ class _ApplicationIntroSlideshowState extends State { ), image: Padding( padding: const EdgeInsets.all(8.0), - child: secureImage, + child: Image(image: AssetImages.secureDocuments.image), ), bodyWidget: Column( crossAxisAlignment: CrossAxisAlignment.center, @@ -90,7 +91,7 @@ class _ApplicationIntroSlideshowState extends State { ), image: Padding( padding: const EdgeInsets.all(8.0), - child: successImage, + child: Image(image: AssetImages.success.image), ), bodyWidget: Column( children: const [ diff --git a/lib/features/document_details/bloc/document_details_cubit.dart b/lib/features/document_details/bloc/document_details_cubit.dart new file mode 100644 index 0000000..b13f5c2 --- /dev/null +++ b/lib/features/document_details/bloc/document_details_cubit.dart @@ -0,0 +1,31 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:injectable/injectable.dart'; +import 'package:paperless_mobile/features/documents/model/document.model.dart'; +import 'package:paperless_mobile/features/documents/repository/document_repository.dart'; + +part 'document_details_state.dart'; + +class DocumentDetailsCubit extends Cubit { + final DocumentRepository _documentRepository; + + DocumentDetailsCubit(this._documentRepository, DocumentModel initialDocument) + : super(DocumentDetailsState(document: initialDocument)); + + Future delete(DocumentModel document) async { + await _documentRepository.delete(document); + emit(const DocumentDetailsState()); + } + + Future update(DocumentModel document) async { + final updatedDocument = await _documentRepository.update(document); + emit(DocumentDetailsState(document: updatedDocument)); + } + + Future assignAsn(DocumentModel document) async { + if (document.archiveSerialNumber == null) { + final int asn = await _documentRepository.findNextAsn(); + update(document.copyWith(archiveSerialNumber: asn)); + } + } +} diff --git a/lib/features/document_details/bloc/document_details_state.dart b/lib/features/document_details/bloc/document_details_state.dart new file mode 100644 index 0000000..8bbca82 --- /dev/null +++ b/lib/features/document_details/bloc/document_details_state.dart @@ -0,0 +1,12 @@ +part of 'document_details_cubit.dart'; + +class DocumentDetailsState with EquatableMixin { + final DocumentModel? document; + + const DocumentDetailsState({ + this.document, + }); + + @override + List get props => [document]; +} diff --git a/lib/features/documents/view/pages/document_details_page.dart b/lib/features/document_details/view/pages/document_details_page.dart similarity index 62% rename from lib/features/documents/view/pages/document_details_page.dart rename to lib/features/document_details/view/pages/document_details_page.dart index d0db7e0..a0140c5 100644 --- a/lib/features/documents/view/pages/document_details_page.dart +++ b/lib/features/document_details/view/pages/document_details_page.dart @@ -5,15 +5,13 @@ import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_mobile/core/bloc/paperless_statistics_cubit.dart'; -import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart'; -import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart'; +import 'package:intl/date_symbol_data_local.dart'; +import 'package:intl/intl.dart'; import 'package:paperless_mobile/core/model/error_message.dart'; import 'package:paperless_mobile/core/widgets/highlighted_text.dart'; import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; +import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; import 'package:paperless_mobile/features/documents/model/document.model.dart'; import 'package:paperless_mobile/features/documents/model/document_meta_data.model.dart'; import 'package:paperless_mobile/features/documents/repository/document_repository.dart'; @@ -21,26 +19,26 @@ import 'package:paperless_mobile/features/documents/view/pages/document_edit_pag import 'package:paperless_mobile/features/documents/view/pages/document_view.dart'; import 'package:paperless_mobile/features/documents/view/widgets/delete_document_confirmation_dialog.dart'; import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart'; +import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart'; import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart'; import 'package:paperless_mobile/features/labels/document_type/view/widgets/document_type_widget.dart'; import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_widget.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/util.dart'; -import 'package:intl/intl.dart'; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; class DocumentDetailsPage extends StatefulWidget { - final int documentId; final bool allowEdit; final bool isLabelClickable; + final String? titleAndContentQueryString; const DocumentDetailsPage({ Key? key, - required this.documentId, - this.allowEdit = true, this.isLabelClickable = true, + this.titleAndContentQueryString, + this.allowEdit = true, }) : super(key: key); @override @@ -48,130 +46,194 @@ class DocumentDetailsPage extends StatefulWidget { } class _DocumentDetailsPageState extends State { - static final DateFormat _detailedDateFormat = - DateFormat("MMM d, yyyy HH:mm:ss"); + @override + void initState() { + super.initState(); + initializeDateFormatting(); + } bool _isDownloadPending = false; @override Widget build(BuildContext context) { - return BlocBuilder( - // buildWhen required because rebuild would happen after delete causing error. - buildWhen: (previous, current) { - return current.documents - .where((element) => element.id == widget.documentId) - .isNotEmpty; + return WillPopScope( + onWillPop: () { + print("Returning document..."); + Navigator.of(context) + .pop(BlocProvider.of(context).state.document); + return Future.value(false); }, - builder: (context, state) { - final document = - state.documents.where((doc) => doc.id == widget.documentId).first; - return DefaultTabController( - length: 3, - child: Scaffold( - floatingActionButtonLocation: - FloatingActionButtonLocation.endDocked, - floatingActionButton: widget.allowEdit - ? FloatingActionButton( - child: const Icon(Icons.edit), - onPressed: () => _onEdit(document), - ) - : null, - bottomNavigationBar: BottomAppBar( - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - IconButton( - icon: const Icon(Icons.delete), - onPressed: - widget.allowEdit ? () => _onDelete(document) : null, - ).padded(const EdgeInsets.symmetric(horizontal: 4)), - IconButton( - icon: const Icon(Icons.download), - onPressed: - Platform.isAndroid ? () => _onDownload(document) : null, - ).padded(const EdgeInsets.only(right: 4)), - IconButton( - icon: const Icon(Icons.open_in_new), - onPressed: () => _onOpen(document), - ).padded(const EdgeInsets.only(right: 4)), - IconButton( - icon: const Icon(Icons.share), - onPressed: () => _onShare(document), - ), - ], - ), - ), - body: NestedScrollView( - headerSliverBuilder: (context, innerBoxIsScrolled) => [ - SliverAppBar( - leading: IconButton( - icon: const Icon( - Icons.arrow_back, - color: Colors - .black, //TODO: check if there is a way to dynamically determine color... + child: DefaultTabController( + length: 3, + child: Scaffold( + floatingActionButtonLocation: FloatingActionButtonLocation.endDocked, + floatingActionButton: widget.allowEdit + ? FloatingActionButton( + child: const Icon(Icons.edit), + onPressed: _onEdit, + ) + : null, + bottomNavigationBar: + BlocBuilder( + builder: (context, state) { + return BottomAppBar( + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + IconButton( + icon: const Icon(Icons.delete), + onPressed: widget.allowEdit && state.document != null + ? () => _onDelete(state.document!) + : null, + ).padded(const EdgeInsets.symmetric(horizontal: 4)), + IconButton( + icon: const Icon(Icons.download), + onPressed: Platform.isAndroid && state.document != null + ? () => _onDownload(state.document!) + : null, + ).padded(const EdgeInsets.only(right: 4)), + IconButton( + icon: const Icon(Icons.open_in_new), + onPressed: state.document != null + ? () => _onOpen(state.document!) + : null, + ).padded(const EdgeInsets.only(right: 4)), + IconButton( + icon: const Icon(Icons.share), + onPressed: state.document != null + ? () => _onShare(state.document!) + : null, ), - onPressed: () => Navigator.pop(context), + ], + ), + ); + }, + ), + body: NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SliverAppBar( + leading: IconButton( + icon: const Icon( + Icons.arrow_back, + color: Colors + .black, //TODO: check if there is a way to dynamically determine color... ), - floating: true, - pinned: true, - expandedHeight: 200.0, - flexibleSpace: DocumentPreview( - id: document.id, - fit: BoxFit.cover, - ), - bottom: ColoredTabBar( - backgroundColor: - Theme.of(context).colorScheme.primaryContainer, - tabBar: TabBar( - tabs: [ - Tab( - child: Text( - S.of(context).documentDetailsPageTabOverviewLabel, - style: TextStyle( - color: Theme.of(context) - .colorScheme - .onPrimaryContainer), - ), + onPressed: () => Navigator.pop( + context, + BlocProvider.of(context) + .state + .document), + ), + floating: true, + pinned: true, + expandedHeight: 200.0, + flexibleSpace: + BlocBuilder( + builder: (context, state) { + if (state.document == null) { + return Container(height: 200); + } + return DocumentPreview( + id: state.document!.id, + fit: BoxFit.cover, + ); + }, + ), + bottom: ColoredTabBar( + backgroundColor: + Theme.of(context).colorScheme.primaryContainer, + tabBar: TabBar( + tabs: [ + Tab( + child: Text( + S.of(context).documentDetailsPageTabOverviewLabel, + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onPrimaryContainer), ), - Tab( - child: Text( - S.of(context).documentDetailsPageTabContentLabel, - style: TextStyle( - color: Theme.of(context) - .colorScheme - .onPrimaryContainer), - ), + ), + Tab( + child: Text( + S.of(context).documentDetailsPageTabContentLabel, + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onPrimaryContainer), ), - Tab( - child: Text( - S.of(context).documentDetailsPageTabMetaDataLabel, - style: TextStyle( - color: Theme.of(context) - .colorScheme - .onPrimaryContainer), - ), + ), + Tab( + child: Text( + S.of(context).documentDetailsPageTabMetaDataLabel, + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onPrimaryContainer), ), - ], - ), + ), + ], ), ), - ], - body: TabBarView( - children: [ - _buildDocumentOverview( - document, state.filter.titleAndContentMatchString), - _buildDocumentContentView( - document, state.filter.titleAndContentMatchString), - _buildDocumentMetaDataView(document), - ].padded(), ), + ], + body: BlocBuilder( + builder: (context, state) { + if (state.document == null) { + return TabBarView( + children: [ + Container(), + Container(), + Container(), + ], + ); + } + return TabBarView( + children: [ + _buildDocumentOverview( + state.document!, + widget.titleAndContentQueryString, + ), + _buildDocumentContentView( + state.document!, + widget.titleAndContentQueryString, + ), + _buildDocumentMetaDataView( + state.document!, + ), + ].padded(), + ); + }, ), ), - ); - }, + ), + ), ); } + Future _onEdit() async { + { + final cubit = BlocProvider.of(context); + if (cubit.state.document == null) { + return; + } + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => GlobalStateBlocProvider( + child: DocumentEditPage( + document: cubit.state.document!, + onEdit: (updatedDocument) { + return BlocProvider.of(context) + .update(updatedDocument); + }, + ), + ), + maintainState: false, + ), + ); + } + } + Widget _buildDocumentMetaDataView(DocumentModel document) { return FutureBuilder( future: getIt().getMetaData(document), @@ -182,11 +244,11 @@ class _DocumentDetailsPageState extends State { final meta = snapshot.data!; return ListView( children: [ - _DetailsItem.text(_detailedDateFormat.format(document.modified), + _DetailsItem.text(DateFormat().format(document.modified), label: S.of(context).documentModifiedPropertyLabel, context: context), _separator(), - _DetailsItem.text(_detailedDateFormat.format(document.added), + _DetailsItem.text(DateFormat().format(document.added), label: S.of(context).documentAddedPropertyLabel, context: context), _separator(), @@ -233,7 +295,7 @@ class _DocumentDetailsPageState extends State { Future _assignAsn(DocumentModel document) async { try { - await BlocProvider.of(context).assignAsn(document); + await BlocProvider.of(context).assignAsn(document); } on ErrorMessage catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } @@ -265,8 +327,7 @@ class _DocumentDetailsPageState extends State { ), _separator(), _DetailsItem.text( - DateFormat.yMMMd(Localizations.localeOf(context).toLanguageTag()) - .format(document.created), + DateFormat().format(document.created), context: context, label: S.of(context).documentCreatedPropertyLabel, ), @@ -311,6 +372,8 @@ class _DocumentDetailsPageState extends State { child: TagsWidget( isClickable: widget.isLabelClickable, tagIds: document.tags, + isSelectedPredicate: (_) => false, + onTagSelected: (int tagId) {}, ), ), ), @@ -345,22 +408,6 @@ class _DocumentDetailsPageState extends State { return const SizedBox(height: 32.0); } - void _onEdit(DocumentModel document) async { - final wasUpdated = await Navigator.push( - context, - MaterialPageRoute( - builder: (_) => GlobalStateBlocProvider( - child: DocumentEditPage(document: document), - ), - maintainState: true, - ), - ) ?? - false; - if (wasUpdated) { - BlocProvider.of(context).updateStatistics(); - } - } - Future _onDownload(DocumentModel document) async { if (!Platform.isAndroid) { showSnackBar( @@ -411,12 +458,13 @@ class _DocumentDetailsPageState extends State { false; if (delete) { try { - await BlocProvider.of(context).remove(document); + await BlocProvider.of(context).delete(document); showSnackBar(context, S.of(context).documentDeleteSuccessMessage); } on ErrorMessage catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } finally { - Navigator.pop(context); + // Document deleted => go back to primary route + Navigator.popUntil(context, (route) => route.isFirst); } } } diff --git a/lib/features/documents/bloc/documents_cubit.dart b/lib/features/documents/bloc/documents_cubit.dart index 7dba9f4..ca25792 100644 --- a/lib/features/documents/bloc/documents_cubit.dart +++ b/lib/features/documents/bloc/documents_cubit.dart @@ -15,11 +15,6 @@ class DocumentsCubit extends Cubit { DocumentsCubit(this.documentRepository) : super(DocumentsState.initial); - Future remove(DocumentModel document) async { - await documentRepository.delete(document); - await reload(); - } - Future bulkRemove(List documents) async { await documentRepository.bulkAction( BulkDeleteAction(documents.map((doc) => doc.id)), @@ -40,8 +35,13 @@ class DocumentsCubit extends Cubit { await reload(); } - Future update(DocumentModel document) async { - await documentRepository.update(document); + Future update( + DocumentModel document, [ + bool updateRemote = true, + ]) async { + if (updateRemote) { + await documentRepository.update(document); + } await reload(); } @@ -83,13 +83,6 @@ class DocumentsCubit extends Cubit { isLoaded: true, value: [...state.value, result], filter: newFilter)); } - Future assignAsn(DocumentModel document) async { - if (document.archiveSerialNumber == null) { - final int asn = await documentRepository.findNextAsn(); - update(document.copyWith(archiveSerialNumber: asn)); - } - } - /// /// Update filter state and automatically reload documents. Always resets page to 1. /// Use [DocumentsCubit.loadMore] to load more data. diff --git a/lib/features/documents/model/bulk_edit.model.dart b/lib/features/documents/model/bulk_edit.model.dart index cabebab..cdfce1a 100644 --- a/lib/features/documents/model/bulk_edit.model.dart +++ b/lib/features/documents/model/bulk_edit.model.dart @@ -16,6 +16,7 @@ class BulkDeleteAction extends BulkAction { return { 'documents': documentIds.toList(), 'method': 'delete', + 'parameters': {}, }; } } @@ -33,8 +34,9 @@ class BulkModifyTagsAction extends BulkAction { BulkModifyTagsAction.addTags(super.documents, this.addTags) : removeTags = const []; - BulkModifyTagsAction.removeTags(super.documents, this.removeTags) - : addTags = const []; + BulkModifyTagsAction.removeTags(super.documents, Iterable tags) + : addTags = const [], + removeTags = tags; @override JSON toJson() { diff --git a/lib/features/documents/model/document.model.dart b/lib/features/documents/model/document.model.dart index e5d5e80..894b9fe 100644 --- a/lib/features/documents/model/document.model.dart +++ b/lib/features/documents/model/document.model.dart @@ -2,8 +2,6 @@ import 'package:equatable/equatable.dart'; import 'package:paperless_mobile/core/type/types.dart'; -import 'package:paperless_mobile/features/documents/model/query_parameters/id_query_parameter.dart'; -import 'package:paperless_mobile/features/documents/model/query_parameters/tags_query.dart'; class DocumentModel extends Equatable { static const idKey = 'id'; diff --git a/lib/features/documents/model/saved_view.model.dart b/lib/features/documents/model/saved_view.model.dart index 99a1adc..cc375c0 100644 --- a/lib/features/documents/model/saved_view.model.dart +++ b/lib/features/documents/model/saved_view.model.dart @@ -61,7 +61,7 @@ class SavedView with EquatableMixin { DocumentFilter toDocumentFilter() { return filterRules.fold( DocumentFilter( - sortOrder: sortReverse ? SortOrder.ascending : SortOrder.descending, + sortOrder: sortReverse ? SortOrder.descending : SortOrder.ascending, sortField: sortField, ), (filter, filterRule) => filterRule.applyToFilter(filter), @@ -80,7 +80,7 @@ class SavedView with EquatableMixin { sortField: filter.sortField, showInSidebar: showInSidebar, showOnDashboard: showOnDashboard, - sortReverse: filter.sortOrder == SortOrder.ascending, + sortReverse: filter.sortOrder == SortOrder.descending, ); JSON toJson() { diff --git a/lib/features/documents/view/pages/document_edit_page.dart b/lib/features/documents/view/pages/document_edit_page.dart index f2cf832..d6f2944 100644 --- a/lib/features/documents/view/pages/document_edit_page.dart +++ b/lib/features/documents/view/pages/document_edit_page.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:typed_data'; import 'package:flutter/material.dart'; @@ -33,7 +34,13 @@ import 'package:paperless_mobile/util.dart'; class DocumentEditPage extends StatefulWidget { final DocumentModel document; - const DocumentEditPage({Key? key, required this.document}) : super(key: key); + final FutureOr Function(DocumentModel updatedDocument) onEdit; + + const DocumentEditPage({ + Key? key, + required this.document, + required this.onEdit, + }) : super(key: key); @override State createState() => _DocumentEditPageState(); @@ -66,7 +73,7 @@ class _DocumentEditPageState extends State { onPressed: () async { if (_formKey.currentState?.saveAndValidate() ?? false) { final values = _formKey.currentState!.value; - final updatedDocument = widget.document.copyWith( + var updatedDocument = widget.document.copyWith( title: values[fkTitle], created: values[fkCreatedDate], overwriteDocumentType: true, @@ -81,15 +88,17 @@ class _DocumentEditPageState extends State { setState(() { _isSubmitLoading = true; }); - bool wasUpdated = false; + try { - await getIt().update(updatedDocument); - showSnackBar(context, S.of(context).documentUpdateErrorMessage); - wasUpdated = true; + await widget.onEdit(updatedDocument); + showSnackBar(context, S.of(context).documentUpdateSuccessMessage); } on ErrorMessage catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } finally { - Navigator.pop(context, wasUpdated); + setState(() { + _isSubmitLoading = false; + }); + Navigator.pop(context); } } }, diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index 4b30ef6..500c5fe 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -2,12 +2,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; -import 'package:paperless_mobile/core/bloc/paperless_statistics_cubit.dart'; import 'package:paperless_mobile/core/model/error_message.dart'; +import 'package:paperless_mobile/di_initializer.dart'; +import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; +import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; import 'package:paperless_mobile/features/documents/model/document.model.dart'; -import 'package:paperless_mobile/features/documents/view/pages/document_details_page.dart'; +import 'package:paperless_mobile/features/documents/model/query_parameters/tags_query.dart'; +import 'package:paperless_mobile/features/documents/repository/document_repository.dart'; import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart'; import 'package:paperless_mobile/features/documents/view/widgets/grid/document_grid.dart'; import 'package:paperless_mobile/features/documents/view/widgets/list/document_list.dart'; @@ -160,22 +163,25 @@ class _DocumentsPageState extends State { switch (settings.preferredViewType) { case ViewType.list: child = DocumentListView( - onTap: _openDocumentDetails, + onTap: _openDetails, state: state, onSelected: _onSelected, pagingController: _pagingController, hasInternetConnection: connectivityState == ConnectivityState.connected, + onTagSelected: _addTagToFilter, ); break; case ViewType.grid: child = DocumentGridView( - onTap: _openDocumentDetails, - state: state, - onSelected: _onSelected, - pagingController: _pagingController, - hasInternetConnection: - connectivityState == ConnectivityState.connected); + onTap: _openDetails, + state: state, + onSelected: _onSelected, + pagingController: _pagingController, + hasInternetConnection: + connectivityState == ConnectivityState.connected, + onTagSelected: (int tagId) => _addTagToFilter, + ); break; } @@ -222,26 +228,63 @@ class _DocumentsPageState extends State { ); } - void _openDocumentDetails(DocumentModel model) { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => MultiBlocProvider( - providers: [ - BlocProvider.value(value: BlocProvider.of(context)), - BlocProvider.value( - value: BlocProvider.of(context)), - BlocProvider.value( - value: BlocProvider.of(context)), - BlocProvider.value(value: BlocProvider.of(context)), - BlocProvider.value( - value: BlocProvider.of(context)), - BlocProvider.value( - value: BlocProvider.of(context)), - ], - child: DocumentDetailsPage(documentId: model.id), - ), + Future _openDetails(DocumentModel document) async { + await Navigator.of(context).push( + _buildDetailsPageRoute(document), + ); + BlocProvider.of(context).reload(); + } + + MaterialPageRoute _buildDetailsPageRoute( + DocumentModel document) { + return MaterialPageRoute( + builder: (_) => MultiBlocProvider( + providers: [ + BlocProvider.value( + value: BlocProvider.of(context), + ), + BlocProvider.value( + value: BlocProvider.of(context), + ), + BlocProvider.value( + value: BlocProvider.of(context), + ), + BlocProvider.value( + value: BlocProvider.of(context), + ), + BlocProvider.value( + value: BlocProvider.of(context), + ), + BlocProvider.value( + value: DocumentDetailsCubit(getIt(), document), + ), + ], + child: const DocumentDetailsPage(), ), ); } + + void _addTagToFilter(int tagId) { + final cubit = BlocProvider.of(context); + try { + final tagsQuery = cubit.state.filter.tags is IdsTagsQuery + ? cubit.state.filter.tags as IdsTagsQuery + : const IdsTagsQuery(); + if (tagsQuery.includedIds.contains(tagId)) { + cubit.updateCurrentFilter( + (filter) => filter.copyWith( + tags: tagsQuery.withIdsRemoved([tagId]), + ), + ); + } else { + cubit.updateCurrentFilter( + (filter) => filter.copyWith( + tags: tagsQuery.withIdQueriesAdded([IncludeTagIdQuery(tagId)]), + ), + ); + } + } on ErrorMessage catch (error, stackTrace) { + showErrorMessage(context, error, stackTrace); + } + } } diff --git a/lib/features/documents/view/widgets/documents_empty_state.dart b/lib/features/documents/view/widgets/documents_empty_state.dart index e2d15bb..9bcbb2c 100644 --- a/lib/features/documents/view/widgets/documents_empty_state.dart +++ b/lib/features/documents/view/widgets/documents_empty_state.dart @@ -4,8 +4,8 @@ import 'package:paperless_mobile/core/widgets/empty_state.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; -import 'package:paperless_mobile/features/documents/bloc/saved_view_cubit.dart'; import 'package:paperless_mobile/features/documents/model/document_filter.dart'; +import 'package:paperless_mobile/features/saved_view/bloc/saved_view_cubit.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/util.dart'; @@ -23,7 +23,7 @@ class DocumentsEmptyState extends StatelessWidget { title: S.of(context).documentsPageEmptyStateOopsText, subtitle: S.of(context).documentsPageEmptyStateNothingHereText, bottomChild: state.filter != DocumentFilter.initial - ? ElevatedButton( + ? TextButton( onPressed: () async { await BlocProvider.of(context).updateFilter(); BlocProvider.of(context).resetSelection(); diff --git a/lib/features/documents/view/widgets/grid/document_grid.dart b/lib/features/documents/view/widgets/grid/document_grid.dart index 7fc9e7b..911e1ba 100644 --- a/lib/features/documents/view/widgets/grid/document_grid.dart +++ b/lib/features/documents/view/widgets/grid/document_grid.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; import 'package:paperless_mobile/features/documents/model/document.model.dart'; +import 'package:paperless_mobile/features/documents/model/query_parameters/tags_query.dart'; import 'package:paperless_mobile/features/documents/view/widgets/grid/document_grid_item.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; @@ -11,6 +12,7 @@ class DocumentGridView extends StatelessWidget { final PagingController pagingController; final DocumentsState state; final bool hasInternetConnection; + final void Function(int tagId) onTagSelected; const DocumentGridView({ super.key, @@ -19,6 +21,7 @@ class DocumentGridView extends StatelessWidget { required this.state, required this.onSelected, required this.hasInternetConnection, + required this.onTagSelected, }); @override Widget build(BuildContext context) { @@ -38,6 +41,14 @@ class DocumentGridView extends StatelessWidget { isSelected: state.selection.contains(item), onSelected: onSelected, isAtLeastOneSelected: state.selection.isNotEmpty, + isTagSelectedPredicate: (int tagId) { + return state.filter.tags is IdsTagsQuery + ? (state.filter.tags as IdsTagsQuery) + .includedIds + .contains(tagId) + : false; + }, + onTagSelected: onTagSelected, ); }, noItemsFoundIndicatorBuilder: (context) => diff --git a/lib/features/documents/view/widgets/grid/document_grid_item.dart b/lib/features/documents/view/widgets/grid/document_grid_item.dart index 6a207e4..0463f9e 100644 --- a/lib/features/documents/view/widgets/grid/document_grid_item.dart +++ b/lib/features/documents/view/widgets/grid/document_grid_item.dart @@ -12,6 +12,8 @@ class DocumentGridItem extends StatelessWidget { final void Function(DocumentModel) onTap; final void Function(DocumentModel) onSelected; final bool isAtLeastOneSelected; + final bool Function(int tagId) isTagSelectedPredicate; + final void Function(int tagId) onTagSelected; const DocumentGridItem({ Key? key, @@ -20,6 +22,8 @@ class DocumentGridItem extends StatelessWidget { required this.onSelected, required this.isSelected, required this.isAtLeastOneSelected, + required this.isTagSelectedPredicate, + required this.onTagSelected, }) : super(key: key); @override @@ -65,6 +69,8 @@ class DocumentGridItem extends StatelessWidget { TagsWidget( tagIds: document.tags, isMultiLine: false, + isSelectedPredicate: isTagSelectedPredicate, + onTagSelected: onTagSelected, ), const Spacer(), Text( diff --git a/lib/features/documents/view/widgets/list/document_list.dart b/lib/features/documents/view/widgets/list/document_list.dart index dacf63c..5556b64 100644 --- a/lib/features/documents/view/widgets/list/document_list.dart +++ b/lib/features/documents/view/widgets/list/document_list.dart @@ -3,8 +3,10 @@ import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart import 'package:paperless_mobile/core/widgets/offline_widget.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; import 'package:paperless_mobile/features/documents/model/document.model.dart'; +import 'package:paperless_mobile/features/documents/model/query_parameters/tags_query.dart'; import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:paperless_mobile/features/labels/tags/model/tag.model.dart'; class DocumentListView extends StatelessWidget { final void Function(DocumentModel) onTap; @@ -14,6 +16,8 @@ class DocumentListView extends StatelessWidget { final DocumentsState state; final bool hasInternetConnection; final bool isLabelClickable; + final void Function(int tagId) onTagSelected; + const DocumentListView({ super.key, required this.onTap, @@ -22,6 +26,7 @@ class DocumentListView extends StatelessWidget { required this.onSelected, required this.hasInternetConnection, this.isLabelClickable = true, + required this.onTagSelected, }); @override @@ -38,6 +43,14 @@ class DocumentListView extends StatelessWidget { isSelected: state.selection.contains(document), onSelected: onSelected, isAtLeastOneSelected: state.selection.isNotEmpty, + isTagSelectedPredicate: (int tagId) { + return state.filter.tags is IdsTagsQuery + ? (state.filter.tags as IdsTagsQuery) + .includedIds + .contains(tagId) + : false; + }, + onTagSelected: onTagSelected, ); }, noItemsFoundIndicatorBuilder: (context) => hasInternetConnection diff --git a/lib/features/documents/view/widgets/list/document_list_item.dart b/lib/features/documents/view/widgets/list/document_list_item.dart index 21b857d..1551c4d 100644 --- a/lib/features/documents/view/widgets/list/document_list_item.dart +++ b/lib/features/documents/view/widgets/list/document_list_item.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:paperless_mobile/features/documents/model/document.model.dart'; import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart'; import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart'; +import 'package:paperless_mobile/features/labels/tags/model/tag.model.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart'; class DocumentListItem extends StatelessWidget { @@ -12,6 +13,9 @@ class DocumentListItem extends StatelessWidget { final bool isSelected; final bool isAtLeastOneSelected; final bool isLabelClickable; + final bool Function(int tagId) isTagSelectedPredicate; + + final void Function(int tagId) onTagSelected; const DocumentListItem({ Key? key, @@ -21,6 +25,8 @@ class DocumentListItem extends StatelessWidget { required this.isSelected, required this.isAtLeastOneSelected, this.isLabelClickable = true, + required this.isTagSelectedPredicate, + required this.onTagSelected, }) : super(key: key); @override @@ -62,6 +68,8 @@ class DocumentListItem extends StatelessWidget { isClickable: isLabelClickable, tagIds: document.tags, isMultiLine: false, + isSelectedPredicate: isTagSelectedPredicate, + onTagSelected: onTagSelected, ), ), ), diff --git a/lib/features/documents/view/widgets/search/document_filter_panel.dart b/lib/features/documents/view/widgets/search/document_filter_panel.dart index 564da9c..5f26517 100644 --- a/lib/features/documents/view/widgets/search/document_filter_panel.dart +++ b/lib/features/documents/view/widgets/search/document_filter_panel.dart @@ -5,7 +5,6 @@ import 'package:paperless_mobile/core/model/error_message.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; -import 'package:paperless_mobile/features/documents/bloc/saved_view_cubit.dart'; import 'package:paperless_mobile/features/documents/model/document.model.dart'; import 'package:paperless_mobile/features/documents/model/document_filter.dart'; import 'package:paperless_mobile/features/documents/model/query_parameters/correspondent_query.dart'; @@ -23,6 +22,7 @@ import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_ import 'package:paperless_mobile/features/labels/storage_path/model/storage_path.model.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart'; import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart'; +import 'package:paperless_mobile/features/saved_view/bloc/saved_view_cubit.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:intl/intl.dart'; import 'package:paperless_mobile/util.dart'; diff --git a/lib/features/documents/view/widgets/selection/confirm_delete_saved_view_dialog.dart b/lib/features/documents/view/widgets/selection/confirm_delete_saved_view_dialog.dart index c074848..885c7a1 100644 --- a/lib/features/documents/view/widgets/selection/confirm_delete_saved_view_dialog.dart +++ b/lib/features/documents/view/widgets/selection/confirm_delete_saved_view_dialog.dart @@ -1,10 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_mobile/core/model/error_message.dart'; -import 'package:paperless_mobile/features/documents/bloc/saved_view_cubit.dart'; import 'package:paperless_mobile/features/documents/model/saved_view.model.dart'; import 'package:paperless_mobile/generated/l10n.dart'; -import 'package:paperless_mobile/util.dart'; class ConfirmDeleteSavedViewDialog extends StatelessWidget { const ConfirmDeleteSavedViewDialog({ diff --git a/lib/features/documents/view/widgets/selection/saved_view_selection_widget.dart b/lib/features/documents/view/widgets/selection/saved_view_selection_widget.dart index 6dd3816..94f2e35 100644 --- a/lib/features/documents/view/widgets/selection/saved_view_selection_widget.dart +++ b/lib/features/documents/view/widgets/selection/saved_view_selection_widget.dart @@ -3,11 +3,11 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_mobile/core/model/error_message.dart'; import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; -import 'package:paperless_mobile/features/documents/bloc/saved_view_cubit.dart'; -import 'package:paperless_mobile/features/documents/bloc/saved_view_state.dart'; import 'package:paperless_mobile/features/documents/model/saved_view.model.dart'; import 'package:paperless_mobile/features/documents/view/widgets/selection/add_saved_view_page.dart'; import 'package:paperless_mobile/features/documents/view/widgets/selection/confirm_delete_saved_view_dialog.dart'; +import 'package:paperless_mobile/features/saved_view/bloc/saved_view_cubit.dart'; +import 'package:paperless_mobile/features/saved_view/bloc/saved_view_state.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/util.dart'; diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index 7fef3bf..f247f97 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -3,12 +3,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart'; -import 'package:paperless_mobile/core/bloc/paperless_statistics_cubit.dart'; import 'package:paperless_mobile/core/model/error_message.dart'; import 'package:paperless_mobile/core/widgets/offline_banner.dart'; import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; -import 'package:paperless_mobile/features/documents/bloc/saved_view_cubit.dart'; import 'package:paperless_mobile/features/documents/view/pages/documents_page.dart'; import 'package:paperless_mobile/features/home/view/widget/bottom_navigation_bar.dart'; import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart'; @@ -20,6 +18,7 @@ import 'package:paperless_mobile/features/labels/document_type/bloc/document_typ import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart'; import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart'; import 'package:paperless_mobile/features/labels/view/pages/labels_page.dart'; +import 'package:paperless_mobile/features/saved_view/bloc/saved_view_cubit.dart'; import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart'; import 'package:paperless_mobile/features/scan/view/scanner_page.dart'; import 'package:paperless_mobile/util.dart'; @@ -88,7 +87,6 @@ class _HomePageState extends State { return Future.wait([ BlocProvider.of(context) .updateInformtion(), - getIt().updateStatistics(), getIt().initialize(), getIt().initialize(), getIt().initialize(), diff --git a/lib/features/home/view/widget/info_drawer.dart b/lib/features/home/view/widget/info_drawer.dart index 89bdb8d..6ec9929 100644 --- a/lib/features/home/view/widget/info_drawer.dart +++ b/lib/features/home/view/widget/info_drawer.dart @@ -1,7 +1,6 @@ import 'package:badges/badges.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_mobile/core/bloc/paperless_statistics_cubit.dart'; import 'package:paperless_mobile/core/model/paperless_statistics_state.dart'; import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart'; import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart'; @@ -120,17 +119,10 @@ class InfoDrawer extends StatelessWidget { color: Theme.of(context).colorScheme.primaryContainer, ), ), - BlocBuilder( - builder: (context, state) { - return ListTile( - title: Text(S.of(context).bottomNavInboxPageLabel), - leading: const Icon(Icons.inbox), - trailing: state.isLoaded - ? Text(state.statistics!.documentsInInbox.toString()) - : null, - onTap: () => _onOpenInbox(context), - ); - }, + ListTile( + title: Text(S.of(context).bottomNavInboxPageLabel), + leading: const Icon(Icons.inbox), + onTap: () => _onOpenInbox(context), ), Divider(), ListTile( @@ -217,15 +209,9 @@ class InfoDrawer extends StatelessWidget { MaterialPageRoute( builder: (_) => GlobalStateBlocProvider( additionalProviders: [ - BlocProvider.value( - value: BlocProvider.of(context), - ), BlocProvider.value( value: getIt()..initialize(), ), - BlocProvider.value( - value: getIt(), - ), ], child: const InboxPage(), ), diff --git a/lib/features/inbox/bloc/inbox_cubit.dart b/lib/features/inbox/bloc/inbox_cubit.dart index 3fe2148..4079e77 100644 --- a/lib/features/inbox/bloc/inbox_cubit.dart +++ b/lib/features/inbox/bloc/inbox_cubit.dart @@ -48,11 +48,13 @@ class InboxCubit extends Cubit { sortField: SortField.added, )) .then((psr) => psr.results); - emit(InboxState( - isLoaded: true, - inboxItems: inboxDocuments, - inboxTags: state.inboxTags, - )); + emit( + InboxState( + isLoaded: true, + inboxItems: inboxDocuments, + inboxTags: state.inboxTags, + ), + ); } /// @@ -61,12 +63,13 @@ class InboxCubit extends Cubit { /// Future> remove(DocumentModel document) async { if (!state.isLoaded) { - throw "State has not yet loaded. Ensure the state is loaded when calling this method!"; + throw "State has not loaded yet. Ensure the state is loaded when calling this method!"; } final tagsToRemove = document.tags.toSet().intersection(state.inboxTags.toSet()); final updatedTags = {...document.tags}..removeAll(tagsToRemove); + await _documentRepository.update( document.copyWith( tags: updatedTags, @@ -84,31 +87,43 @@ class InboxCubit extends Cubit { return tagsToRemove; } + /// + /// Adds the previously removed tags to the document and performs an update. + /// Future undoRemove( - DocumentModel document, Iterable removedTags) async { + DocumentModel document, + Iterable removedTags, + ) async { final updatedDoc = document.copyWith( tags: {...document.tags, ...removedTags}, overwriteTags: true, ); await _documentRepository.update(updatedDoc); - emit(InboxState( - isLoaded: true, - inboxItems: [...state.inboxItems, updatedDoc] - ..sort((d1, d2) => d1.added.compareTo(d2.added)), - inboxTags: state.inboxTags, - )); + emit( + InboxState( + isLoaded: true, + inboxItems: [...state.inboxItems, updatedDoc] + ..sort((d1, d2) => d2.added.compareTo(d1.added)), + inboxTags: state.inboxTags, + ), + ); } /// /// Removes inbox tags from all documents in the inbox. /// Future clearInbox() async { - await _documentRepository.bulkAction(BulkModifyTagsAction.removeTags( - state.inboxItems.map((e) => e.id), state.inboxTags)); + await _documentRepository.bulkAction( + BulkModifyTagsAction.removeTags( + state.inboxItems.map((e) => e.id), + state.inboxTags, + ), + ); emit( InboxState( isLoaded: true, inboxTags: state.inboxTags, + inboxItems: [], ), ); } diff --git a/lib/features/inbox/view/pages/inbox_page.dart b/lib/features/inbox/view/pages/inbox_page.dart index 0eb8a86..716b6e2 100644 --- a/lib/features/inbox/view/pages/inbox_page.dart +++ b/lib/features/inbox/view/pages/inbox_page.dart @@ -1,11 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/date_symbol_data_local.dart'; -import 'package:paperless_mobile/core/bloc/paperless_statistics_cubit.dart'; import 'package:paperless_mobile/core/model/error_message.dart'; import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/model/document.model.dart'; import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart'; import 'package:paperless_mobile/features/inbox/bloc/state/inbox_state.dart'; @@ -21,7 +19,8 @@ class InboxPage extends StatefulWidget { } class _InboxPageState extends State { - final GlobalKey _listKey = GlobalKey(); + final GlobalKey _emptyStateRefreshIndicatorKey = + GlobalKey(); @override void initState() { @@ -31,7 +30,7 @@ class _InboxPageState extends State { @override Widget build(BuildContext context) { - final bloc = BlocProvider.of(context); + //TODO: Group by date (today, yseterday, etc.) return Scaffold( appBar: AppBar( title: Text(S.of(context).bottomNavInboxPageLabel), @@ -39,6 +38,27 @@ class _InboxPageState extends State { icon: const Icon(Icons.close), onPressed: () => Navigator.pop(context), ), + bottom: PreferredSize( + preferredSize: Size.fromHeight(14), + child: BlocBuilder( + builder: (context, state) { + return Align( + alignment: Alignment.centerRight, + child: ClipRRect( + borderRadius: BorderRadius.circular(8.0), + child: ColoredBox( + color: Theme.of(context).colorScheme.secondaryContainer, + child: Text( + '${state.inboxItems.length} unseen', + textAlign: TextAlign.start, + style: Theme.of(context).textTheme.caption, + ).padded(const EdgeInsets.symmetric(horizontal: 4.0)), + ), + ), + ); + }, + ).padded(const EdgeInsets.symmetric(horizontal: 8.0)), + ), ), floatingActionButton: BlocBuilder( builder: (context, state) { @@ -47,8 +67,8 @@ class _InboxPageState extends State { icon: const Icon(Icons.done_all), onPressed: state.isLoaded && state.inboxItems.isNotEmpty ? () => _onMarkAllAsSeen( - bloc.state.inboxItems, - bloc.state.inboxTags, + state.inboxItems, + state.inboxTags, ) : null, ); @@ -61,10 +81,26 @@ class _InboxPageState extends State { } if (state.inboxItems.isEmpty) { - return Text( - "You do not have new documents in your inbox.", - textAlign: TextAlign.center, - ).padded(); + return RefreshIndicator( + key: _emptyStateRefreshIndicatorKey, + onRefresh: () => + BlocProvider.of(context).reloadInbox(), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('You do not have unseen documents.'), + TextButton( + onPressed: () => + _emptyStateRefreshIndicatorKey.currentState?.show(), + child: Text('Refresh'), + ), + ], + ), + ), + ); } return RefreshIndicator( onRefresh: () => BlocProvider.of(context).reloadInbox(), @@ -84,10 +120,9 @@ class _InboxPageState extends State { ), ), Expanded( - child: AnimatedList( - key: _listKey, - initialItemCount: state.inboxItems.length, - itemBuilder: (context, index, animation) { + child: ListView.builder( + itemCount: state.inboxItems.length, + itemBuilder: (context, index) { final doc = state.inboxItems.elementAt(index); return _buildListItem(context, doc); }, @@ -108,11 +143,11 @@ class _InboxPageState extends State { mainAxisAlignment: MainAxisAlignment.end, children: [ Icon( - Icons.done, + Icons.done_all, color: Theme.of(context).colorScheme.primary, ).padded(), Text( - 'Mark as read', //TODO: INTL + 'Mark as seen', //TODO: INTL style: TextStyle( color: Theme.of(context).colorScheme.primary, ), @@ -120,57 +155,49 @@ class _InboxPageState extends State { ], ).padded(), confirmDismiss: (_) => _onItemDismissed(doc), - key: ObjectKey(doc.id), + key: UniqueKey(), child: DocumentInboxItem(document: doc), ); } - Widget _buildSlideAnimation( - BuildContext context, - animation, - Widget child, - ) { - return SlideTransition( - position: Tween( - begin: const Offset(-1, 0), - end: Offset.zero, - ).animate(animation), - child: child, - ); - } - Future _onMarkAllAsSeen( Iterable documents, Iterable inboxTags, ) async { - for (int i = documents.length - 1; i >= 0; i--) { - final doc = documents.elementAt(i); - _listKey.currentState?.removeItem( - 0, - (context, animation) => _buildSlideAnimation( - context, - animation, - _buildListItem(context, doc), - ), - ); - await Future.delayed(const Duration(milliseconds: 75)); + final isActionConfirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('Confirm action'), + content: Text( + 'Are you sure you want to mark all documents as seen? This will perform a bulk edit operation removing all inbox tags from the documents.\nThis action is not reversible! Are you sure you want to continue?', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(S.of(context).genericActionCancelLabel), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text(S.of(context).genericActionOkLabel), + ), + ], + ), + ) ?? + false; + if (isActionConfirmed) { + await BlocProvider.of(context).clearInbox(); } - await BlocProvider.of(context) - .bulkEditTags(documents, removeTags: inboxTags); - BlocProvider.of(context).resetInboxCount(); } Future _onItemDismissed(DocumentModel doc) async { try { final removedTags = await BlocProvider.of(context).remove(doc); - BlocProvider.of(context).decrementInboxCount(); showSnackBar( context, 'Document removed from inbox.', //TODO: INTL action: SnackBarAction( label: 'UNDO', //TODO: INTL - textColor: Theme.of(context).colorScheme.primary, onPressed: () => _onUndoMarkAsSeen(doc, removedTags), ), ); @@ -194,7 +221,6 @@ class _InboxPageState extends State { try { await BlocProvider.of(context) .undoRemove(document, removedTags); - BlocProvider.of(context).incrementInboxCount(); } on ErrorMessage catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } diff --git a/lib/features/inbox/view/widgets/document_inbox_item.dart b/lib/features/inbox/view/widgets/document_inbox_item.dart index 7527bc5..4cdd361 100644 --- a/lib/features/inbox/view/widgets/document_inbox_item.dart +++ b/lib/features/inbox/view/widgets/document_inbox_item.dart @@ -1,9 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; +import 'package:paperless_mobile/di_initializer.dart'; +import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/model/document.model.dart'; -import 'package:paperless_mobile/features/documents/view/pages/document_details_page.dart'; +import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; +import 'package:paperless_mobile/features/documents/repository/document_repository.dart'; import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart'; import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart'; @@ -37,6 +40,8 @@ class DocumentInboxItem extends StatelessWidget { tagIds: document.tags, isMultiLine: false, isClickable: false, + isSelectedPredicate: (_) => false, + onTagSelected: (_) {}, ), ], ), @@ -45,11 +50,14 @@ class DocumentInboxItem extends StatelessWidget { MaterialPageRoute( builder: (_) => GlobalStateBlocProvider( additionalProviders: [ - BlocProvider.value( - value: BlocProvider.of(context)), + BlocProvider( + create: (context) => DocumentDetailsCubit( + getIt(), + document, + ), + ), ], - child: DocumentDetailsPage( - documentId: document.id, + child: const DocumentDetailsPage( allowEdit: false, isLabelClickable: false, ), diff --git a/lib/features/labels/bloc/global_state_bloc_provider.dart b/lib/features/labels/bloc/global_state_bloc_provider.dart index 3de0a76..8b36cd3 100644 --- a/lib/features/labels/bloc/global_state_bloc_provider.dart +++ b/lib/features/labels/bloc/global_state_bloc_provider.dart @@ -1,11 +1,11 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_mobile/di_initializer.dart'; -import 'package:paperless_mobile/features/documents/bloc/saved_view_cubit.dart'; import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart'; import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart'; import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart'; import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart'; +import 'package:paperless_mobile/features/saved_view/bloc/saved_view_cubit.dart'; class GlobalStateBlocProvider extends StatelessWidget { final List additionalProviders; diff --git a/lib/features/labels/tags/view/pages/edit_tag_page.dart b/lib/features/labels/tags/view/pages/edit_tag_page.dart index df5a8f5..ae2cfe0 100644 --- a/lib/features/labels/tags/view/pages/edit_tag_page.dart +++ b/lib/features/labels/tags/view/pages/edit_tag_page.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; -import 'package:paperless_mobile/core/bloc/paperless_statistics_cubit.dart'; import 'package:paperless_mobile/core/model/error_message.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/model/document_filter.dart'; @@ -24,8 +23,6 @@ class EditTagPage extends StatelessWidget { label: tag, onSubmit: (tag) async { await BlocProvider.of(context).replace(tag); - //If inbox property was added/removed from tag, the number of documents in inbox may increase/decrease. - BlocProvider.of(context).updateStatistics(); }, onDelete: (tag) => _onDelete(tag, context), fromJson: Tag.fromJson, diff --git a/lib/features/labels/tags/view/widgets/tag_widget.dart b/lib/features/labels/tags/view/widgets/tag_widget.dart index f8e4440..64bb639 100644 --- a/lib/features/labels/tags/view/widgets/tag_widget.dart +++ b/lib/features/labels/tags/view/widgets/tag_widget.dart @@ -1,80 +1,43 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_mobile/core/model/error_message.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; -import 'package:paperless_mobile/features/documents/model/query_parameters/tags_query.dart'; import 'package:paperless_mobile/features/labels/tags/model/tag.model.dart'; -import 'package:paperless_mobile/util.dart'; class TagWidget extends StatelessWidget { final Tag tag; - final void Function()? afterTagTapped; + final VoidCallback? afterTagTapped; + final VoidCallback onSelected; + final bool isSelected; final bool isClickable; + const TagWidget({ super.key, required this.tag, required this.afterTagTapped, this.isClickable = true, + required this.onSelected, + required this.isSelected, }); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(right: 4.0), - child: BlocBuilder( - builder: (context, state) { - final isIdsQuery = state.filter.tags is IdsTagsQuery; - return FilterChip( - selected: isIdsQuery - ? (state.filter.tags as IdsTagsQuery) - .includedIds - .contains(tag.id) - : false, - selectedColor: tag.color, - onSelected: (_) => _addTagToFilter(context), - visualDensity: const VisualDensity(vertical: -2), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - label: Text( - tag.name, - style: TextStyle(color: tag.textColor), - ), - checkmarkColor: tag.textColor, - backgroundColor: tag.color, - side: BorderSide.none, - ); - }, + child: AbsorbPointer( + absorbing: !isClickable, + child: FilterChip( + selected: isSelected, + selectedColor: tag.color, + onSelected: (_) => onSelected(), + visualDensity: const VisualDensity(vertical: -2), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + label: Text( + tag.name, + style: TextStyle(color: tag.textColor), + ), + checkmarkColor: tag.textColor, + backgroundColor: tag.color, + side: BorderSide.none, + ), ), ); } - - void _addTagToFilter(BuildContext context) { - if (!isClickable) { - return; - } - final cubit = BlocProvider.of(context); - try { - final tagsQuery = cubit.state.filter.tags is IdsTagsQuery - ? cubit.state.filter.tags as IdsTagsQuery - : const IdsTagsQuery(); - if (tagsQuery.includedIds.contains(tag.id)) { - cubit.updateCurrentFilter( - (filter) => filter.copyWith( - tags: tagsQuery.withIdsRemoved([tag.id!]), - ), - ); - } else { - cubit.updateCurrentFilter( - (filter) => filter.copyWith( - tags: tagsQuery.withIdQueriesAdded([IncludeTagIdQuery(tag.id!)]), - ), - ); - } - if (afterTagTapped != null) { - afterTagTapped!(); - } - } on ErrorMessage catch (error, stackTrace) { - showErrorMessage(context, error, stackTrace); - } - } } diff --git a/lib/features/labels/tags/view/widgets/tags_widget.dart b/lib/features/labels/tags/view/widgets/tags_widget.dart index 6d09178..5b6c256 100644 --- a/lib/features/labels/tags/view/widgets/tags_widget.dart +++ b/lib/features/labels/tags/view/widgets/tags_widget.dart @@ -1,5 +1,3 @@ -import 'dart:developer'; - import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_mobile/features/labels/model/label_state.dart'; @@ -10,8 +8,10 @@ import 'package:paperless_mobile/features/labels/tags/view/widgets/tag_widget.da class TagsWidget extends StatefulWidget { final Iterable tagIds; final bool isMultiLine; - final void Function()? afterTagTapped; + final VoidCallback? afterTagTapped; + final void Function(int tagId) onTagSelected; final bool isClickable; + final bool Function(int id) isSelectedPredicate; const TagsWidget({ Key? key, @@ -19,6 +19,8 @@ class TagsWidget extends StatefulWidget { this.afterTagTapped, this.isMultiLine = true, this.isClickable = true, + required this.isSelectedPredicate, + required this.onTagSelected, }) : super(key: key); @override @@ -37,6 +39,8 @@ class _TagsWidgetState extends State { tag: state.getLabel(id)!, afterTagTapped: widget.afterTagTapped, isClickable: widget.isClickable, + isSelected: widget.isSelectedPredicate(id), + onSelected: () => widget.onTagSelected(id), ), ) .toList(); diff --git a/lib/features/labels/view/pages/labels_page.dart b/lib/features/labels/view/pages/labels_page.dart index a359f24..2df6a22 100644 --- a/lib/features/labels/view/pages/labels_page.dart +++ b/lib/features/labels/view/pages/labels_page.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_mobile/core/bloc/paperless_statistics_cubit.dart'; import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart'; import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; @@ -218,7 +217,6 @@ class _LabelsPageState extends State builder: (_) => GlobalStateBlocProvider( additionalProviders: [ BlocProvider.value(value: BlocProvider.of(context)), - BlocProvider.value(value: getIt()), ], child: EditTagPage(tag: tag), ), diff --git a/lib/features/linked_documents_preview/view/pages/linked_documents_page.dart b/lib/features/linked_documents_preview/view/pages/linked_documents_page.dart index bf7a6a5..46de313 100644 --- a/lib/features/linked_documents_preview/view/pages/linked_documents_page.dart +++ b/lib/features/linked_documents_preview/view/pages/linked_documents_page.dart @@ -2,10 +2,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart'; +import 'package:paperless_mobile/di_initializer.dart'; +import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; import 'package:paperless_mobile/features/documents/model/document.model.dart'; -import 'package:paperless_mobile/features/documents/view/pages/document_details_page.dart'; +import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; +import 'package:paperless_mobile/features/documents/repository/document_repository.dart'; import 'package:paperless_mobile/features/documents/view/widgets/list/document_list.dart'; import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart'; import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart'; @@ -68,15 +71,15 @@ class _LinkedDocumentsPageState extends State { builder: (ctxt) => GlobalStateBlocProvider( additionalProviders: [ BlocProvider.value( - value: BlocProvider.of( - context, + value: DocumentDetailsCubit( + getIt(), + document, ), ), ], - child: DocumentDetailsPage( - documentId: doc.id, - allowEdit: false, + child: const DocumentDetailsPage( isLabelClickable: false, + allowEdit: false, ), ), ), @@ -84,6 +87,8 @@ class _LinkedDocumentsPageState extends State { }, isSelected: false, isAtLeastOneSelected: false, + isTagSelectedPredicate: (_) => false, + onTagSelected: (int tag) {}, ); }, ), diff --git a/lib/features/documents/bloc/saved_view_cubit.dart b/lib/features/saved_view/bloc/saved_view_cubit.dart similarity index 91% rename from lib/features/documents/bloc/saved_view_cubit.dart rename to lib/features/saved_view/bloc/saved_view_cubit.dart index 9237be0..998ec64 100644 --- a/lib/features/documents/bloc/saved_view_cubit.dart +++ b/lib/features/saved_view/bloc/saved_view_cubit.dart @@ -1,10 +1,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_mobile/core/model/error_message.dart'; +import 'package:injectable/injectable.dart'; import 'package:paperless_mobile/di_initializer.dart'; -import 'package:paperless_mobile/features/documents/bloc/saved_view_state.dart'; import 'package:paperless_mobile/features/documents/model/saved_view.model.dart'; import 'package:paperless_mobile/features/documents/repository/saved_views_repository.dart'; -import 'package:injectable/injectable.dart'; +import 'package:paperless_mobile/features/saved_view/bloc/saved_view_state.dart'; @singleton class SavedViewCubit extends Cubit { diff --git a/lib/features/documents/bloc/saved_view_state.dart b/lib/features/saved_view/bloc/saved_view_state.dart similarity index 100% rename from lib/features/documents/bloc/saved_view_state.dart rename to lib/features/saved_view/bloc/saved_view_state.dart diff --git a/lib/features/scan/view/document_upload_page.dart b/lib/features/scan/view/document_upload_page.dart index 8306759..c23ec44 100644 --- a/lib/features/scan/view/document_upload_page.dart +++ b/lib/features/scan/view/document_upload_page.dart @@ -3,7 +3,6 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; -import 'package:paperless_mobile/core/bloc/paperless_statistics_cubit.dart'; import 'package:paperless_mobile/core/model/error_message.dart'; import 'package:paperless_mobile/core/type/types.dart'; import 'package:paperless_mobile/di_initializer.dart'; diff --git a/lib/features/scan/view/scanner_page.dart b/lib/features/scan/view/scanner_page.dart index 024e4e6..2389fe6 100644 --- a/lib/features/scan/view/scanner_page.dart +++ b/lib/features/scan/view/scanner_page.dart @@ -8,7 +8,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:mime/mime.dart'; -import 'package:paperless_mobile/core/bloc/paperless_statistics_cubit.dart'; import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart'; import 'package:paperless_mobile/core/global/constants.dart'; import 'package:paperless_mobile/core/model/error_message.dart'; @@ -136,9 +135,6 @@ class _ScannerPageState extends State ], child: DocumentUploadPage( fileBytes: bytes, - onSuccessfullyConsumed: (_) => - BlocProvider.of(context) - .updateStatistics(), ), ), ), @@ -259,9 +255,6 @@ class _ScannerPageState extends State child: DocumentUploadPage( filename: filename, fileBytes: fileBytes, - onSuccessfullyConsumed: (_) => - BlocProvider.of(context) - .updateStatistics(), ), ), ), diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 449667a..a7c9384 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -188,7 +188,7 @@ "editLabelPageDeletionDialogText": "Dieser Kennzeichner wird von Dokumenten referenziert. Durch das Löschen dieses Kennzeichners werden alle Referenzen entfernt. Fortfahren?", "settingsPageStorageSettingsLabel": "Speicher", "settingsPageStorageSettingsDescriptionText": "Dateien und Speicherplatz verwalten", - "documentUpdateErrorMessage": "Dokument erfolgreich aktualisiert.", + "documentUpdateSuccessMessage": "Dokument erfolgreich aktualisiert.", "errorMessageMissingClientCertificate": "Ein Client Zerfitikat wurde erwartet, aber nicht gesendet. Bitte konfiguriere ein gültiges Zertifikat.", "serverInformationPaperlessVersionText": "Paperless Server-Version", "errorReportLabel": "MELDEN", diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 710460f..dfd91b2 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -189,7 +189,7 @@ "editLabelPageDeletionDialogText": "This label contains references to other documents. By deleting this label, all references will be removed. Continue?", "settingsPageStorageSettingsLabel": "Storage", "settingsPageStorageSettingsDescriptionText": "Manage files and storage space", - "documentUpdateErrorMessage": "Document successfully updated.", + "documentUpdateSuccessMessage": "Document successfully updated.", "errorMessageMissingClientCertificate": "A client certificate was expected but not sent. Please provide a valid client certificate.", "serverInformationPaperlessVersionText": "Paperless server version", "errorReportLabel": "REPORT", diff --git a/lib/main.dart b/lib/main.dart index 76a900a..00e05af 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,4 @@ +import 'dart:developer'; import 'dart:io'; import 'package:flutter/material.dart'; @@ -11,7 +12,6 @@ import 'package:intl/intl.dart'; import 'package:intl/intl_standalone.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; -import 'package:paperless_mobile/core/bloc/paperless_statistics_cubit.dart'; import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart'; import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart'; import 'package:paperless_mobile/core/global/asset_images.dart'; @@ -51,6 +51,13 @@ void main() async { await getIt().initialize(); await getIt().initialize(); + // Preload asset images + // WARNING: This seems to bloat up the app up to almost 200mb! + // await Future.forEach( + // AssetImages.values.map((e) => e.image), + // (img) => loadImage(img), + // ); + runApp(const PaperlessMobileEntrypoint()); } @@ -70,7 +77,6 @@ class _PaperlessMobileEntrypointState extends State { BlocProvider.value(value: getIt()), BlocProvider.value(value: getIt()), BlocProvider.value(value: getIt()), - BlocProvider.value(value: getIt()), BlocProvider.value(value: getIt()), ], child: BlocBuilder( @@ -229,7 +235,6 @@ class _AuthenticationWrapperState extends State { if (authentication.isAuthenticated) { return GlobalStateBlocProvider( additionalProviders: [ - BlocProvider.value(value: getIt()), BlocProvider.value(value: getIt()), ], child: const HomePage(), diff --git a/lib/util.dart b/lib/util.dart index bbcb86b..dee43e6 100644 --- a/lib/util.dart +++ b/lib/util.dart @@ -1,9 +1,13 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:developer'; import 'dart:io'; import 'dart:typed_data'; +import 'dart:ui'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart'; import 'package:paperless_mobile/core/model/error_message.dart'; import 'package:intl/intl.dart'; @@ -120,3 +124,35 @@ String formatLocalDate(BuildContext context, DateTime dateTime) { String extractFilenameFromPath(String path) { return path.split(RegExp('[./]')).reversed.skip(1).first; } + +// Taken from https://github.com/flutter/flutter/issues/26127#issuecomment-782083060 +Future loadImage(ImageProvider provider) { + final config = ImageConfiguration( + bundle: rootBundle, + devicePixelRatio: window.devicePixelRatio, + platform: defaultTargetPlatform, + ); + final Completer completer = Completer(); + final ImageStream stream = provider.resolve(config); + + late final ImageStreamListener listener; + + listener = ImageStreamListener((ImageInfo image, bool sync) { + debugPrint("Image ${image.debugLabel} finished loading"); + completer.complete(); + stream.removeListener(listener); + }, onError: (dynamic exception, StackTrace? stackTrace) { + completer.complete(); + stream.removeListener(listener); + FlutterError.reportError(FlutterErrorDetails( + context: ErrorDescription('image failed to load'), + library: 'image resource service', + exception: exception, + stack: stackTrace, + silent: true, + )); + }); + + stream.addListener(listener); + return completer.future; +} diff --git a/pubspec.yaml b/pubspec.yaml index f225b15..d020f8b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,5 @@ name: paperless_mobile -description: - Application to conveniently scan and share documents with a paperless-ng +description: Application to conveniently scan and share documents with a paperless-ng server. # The following line prevents the package from being accidentally published to @@ -139,6 +138,8 @@ flutter: # see https://flutter.dev/custom-fonts/#from-packages flutter_intl: enabled: true + localizely: + project_id: 84b4144d-a628-4ba6-a8d0-4f9917444057 flutter_native_splash: image: assets/logos/paperless_logo_green.png diff --git a/test/src/saved_view_test.dart b/test/src/saved_view_test.dart index b4fd792..f760dee 100644 --- a/test/src/saved_view_test.dart +++ b/test/src/saved_view_test.dart @@ -90,7 +90,7 @@ void main() { addedDateBefore: DateTime.parse("2022-09-26"), addedDateAfter: DateTime.parse("2000-01-01"), sortField: SortField.created, - sortOrder: SortOrder.ascending, + sortOrder: SortOrder.descending, queryText: "Never gonna give you up", queryType: QueryType.extended, ), @@ -106,7 +106,7 @@ void main() { "show_on_dashboard": false, "show_in_sidebar": false, "sort_field": SortField.created.name, - "sort_reverse": false, + "sort_reverse": true, "filter_rules": [], }).toDocumentFilter(), equals(DocumentFilter.initial), @@ -121,7 +121,7 @@ void main() { "show_on_dashboard": false, "show_in_sidebar": false, "sort_field": SortField.created.name, - "sort_reverse": false, + "sort_reverse": true, "filter_rules": [ { 'rule_type': FilterRule.correspondentRule, @@ -185,7 +185,7 @@ void main() { showOnDashboard: false, showInSidebar: false, sortField: SortField.added, - sortReverse: true, + sortReverse: false, filterRules: [ FilterRule(FilterRule.correspondentRule, "1"), FilterRule(FilterRule.documentTypeRule, "2"), @@ -232,7 +232,7 @@ void main() { showOnDashboard: false, showInSidebar: false, sortField: SortField.created, - sortReverse: false, + sortReverse: true, filterRules: [], ), ), @@ -248,7 +248,7 @@ void main() { storagePath: StoragePathQuery.notAssigned(), tags: OnlyNotAssignedTagsQuery(), sortField: SortField.created, - sortOrder: SortOrder.descending, + sortOrder: SortOrder.ascending, ), name: "test_name", showInSidebar: false,