From 5edbdabf26b5a9963ffce040de227f839eba32eb Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Thu, 24 Nov 2022 22:51:42 +0100 Subject: [PATCH] Started removing tight coupling --- .../documents/bloc/documents_cubit.dart | 67 +---- .../view/pages/document_details_page.dart | 7 +- .../view/pages/document_edit_page.dart | 2 +- .../documents/view/pages/documents_page.dart | 12 +- .../selection/documents_page_app_bar.dart | 2 +- lib/features/home/view/home_page.dart | 61 ++-- .../home/view/widget/info_drawer.dart | 45 +-- lib/features/inbox/bloc/inbox_cubit.dart | 115 ++++++++ .../inbox/bloc/state/inbox_state.dart | 17 ++ lib/features/inbox/view/inbox_page.dart | 263 ----------------- lib/features/inbox/view/pages/inbox_page.dart | 202 +++++++++++++ .../view/widgets/document_inbox_item.dart | 61 ++++ ...r.dart => global_state_bloc_provider.dart} | 10 +- .../labels/tags/view/pages/edit_tag_page.dart | 2 +- .../labels/view/pages/labels_page.dart | 266 +++++++++--------- .../labels/view/widgets/label_item.dart | 19 +- .../widgets/linked_documents_preview.dart | 85 ------ .../bloc/linked_documents_cubit.dart | 26 ++ .../bloc/state/linked_documents_state.dart | 15 + .../view/pages/linked_documents_page.dart | 100 +++++++ .../scan/bloc/document_scanner_cubit.dart | 34 ++- .../scan/view/document_upload_page.dart | 31 +- lib/features/scan/view/scanner_page.dart | 39 ++- lib/l10n/intl_de.arb | 2 +- lib/l10n/intl_en.arb | 2 +- lib/main.dart | 49 ++-- test/src/bloc/document_cubit_test.dart | 4 +- 27 files changed, 845 insertions(+), 693 deletions(-) create mode 100644 lib/features/inbox/bloc/inbox_cubit.dart create mode 100644 lib/features/inbox/bloc/state/inbox_state.dart delete mode 100644 lib/features/inbox/view/inbox_page.dart create mode 100644 lib/features/inbox/view/pages/inbox_page.dart create mode 100644 lib/features/inbox/view/widgets/document_inbox_item.dart rename lib/features/labels/bloc/{label_bloc_provider.dart => global_state_bloc_provider.dart} (80%) delete mode 100644 lib/features/labels/view/widgets/linked_documents_preview.dart create mode 100644 lib/features/linked_documents_preview/bloc/linked_documents_cubit.dart create mode 100644 lib/features/linked_documents_preview/bloc/state/linked_documents_state.dart create mode 100644 lib/features/linked_documents_preview/view/pages/linked_documents_page.dart diff --git a/lib/features/documents/bloc/documents_cubit.dart b/lib/features/documents/bloc/documents_cubit.dart index d3c552d..7dba9f4 100644 --- a/lib/features/documents/bloc/documents_cubit.dart +++ b/lib/features/documents/bloc/documents_cubit.dart @@ -1,15 +1,13 @@ import 'dart:typed_data'; 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/features/documents/bloc/documents_state.dart'; import 'package:paperless_mobile/features/documents/model/bulk_edit.model.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/paged_search_result.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:injectable/injectable.dart'; @singleton class DocumentsCubit extends Cubit { @@ -17,45 +15,20 @@ class DocumentsCubit extends Cubit { DocumentsCubit(this.documentRepository) : super(DocumentsState.initial); - Future addDocument( - Uint8List bytes, - String fileName, { - required String title, - required void Function(DocumentModel document) onConsumptionFinished, - int? documentType, - int? correspondent, - Iterable tags = const [], - DateTime? createdAt, - }) async { - await documentRepository.create( - bytes, - fileName, - title: title, - documentType: documentType, - correspondent: correspondent, - tags: tags, - createdAt: createdAt, - ); - - documentRepository - .waitForConsumptionFinished(fileName, title) - .then((value) => onConsumptionFinished(value)); - } - - Future removeDocument(DocumentModel document) async { + Future remove(DocumentModel document) async { await documentRepository.delete(document); - return await reloadDocuments(); + await reload(); } - Future bulkRemoveDocuments(List documents) async { + Future bulkRemove(List documents) async { await documentRepository.bulkAction( BulkDeleteAction(documents.map((doc) => doc.id)), ); - await reloadDocuments(); + await reload(); } Future bulkEditTags( - List documents, { + Iterable documents, { Iterable addTags = const [], Iterable removeTags = const [], }) async { @@ -64,15 +37,15 @@ class DocumentsCubit extends Cubit { addTags: addTags, removeTags: removeTags, )); - await reloadDocuments(); + await reload(); } - Future updateDocument(DocumentModel document) async { + Future update(DocumentModel document) async { await documentRepository.update(document); - await reloadDocuments(); + await reload(); } - Future loadDocuments() async { + Future load() async { final result = await documentRepository.find(state.filter); emit(DocumentsState( isLoaded: true, @@ -81,7 +54,7 @@ class DocumentsCubit extends Cubit { )); } - Future reloadDocuments() async { + Future reload() async { if (state.currentPageNumber >= 5) { return _bulkReloadDocuments(); } @@ -113,7 +86,7 @@ class DocumentsCubit extends Cubit { Future assignAsn(DocumentModel document) async { if (document.archiveSerialNumber == null) { final int asn = await documentRepository.findNextAsn(); - updateDocument(document.copyWith(archiveSerialNumber: asn)); + update(document.copyWith(archiveSerialNumber: asn)); } } @@ -151,22 +124,6 @@ class DocumentsCubit extends Cubit { } } - /// - /// Updates the given document with the inbox tags removed and returns the remoed inbox tags. - /// - Future> removeInboxTags( - DocumentModel document, final Iterable inboxTags) async { - final tagsToRemove = document.tags.toSet().intersection(inboxTags.toSet()); - final updatedTags = {...document.tags}..removeAll(tagsToRemove); - await updateDocument( - document.copyWith( - tags: updatedTags, - overwriteTags: true, - ), - ); - return tagsToRemove; - } - void resetSelection() { emit(state.copyWith(selection: [])); } diff --git a/lib/features/documents/view/pages/document_details_page.dart b/lib/features/documents/view/pages/document_details_page.dart index 20c54da..d0db7e0 100644 --- a/lib/features/documents/view/pages/document_details_page.dart +++ b/lib/features/documents/view/pages/document_details_page.dart @@ -6,7 +6,7 @@ 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/label_bloc_provider.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:paperless_mobile/core/model/error_message.dart'; import 'package:paperless_mobile/core/widgets/highlighted_text.dart'; @@ -52,7 +52,6 @@ class _DocumentDetailsPageState extends State { DateFormat("MMM d, yyyy HH:mm:ss"); bool _isDownloadPending = false; - bool _isAssignAsnPending = false; @override Widget build(BuildContext context) { @@ -350,7 +349,7 @@ class _DocumentDetailsPageState extends State { final wasUpdated = await Navigator.push( context, MaterialPageRoute( - builder: (_) => LabelBlocProvider( + builder: (_) => GlobalStateBlocProvider( child: DocumentEditPage(document: document), ), maintainState: true, @@ -412,7 +411,7 @@ class _DocumentDetailsPageState extends State { false; if (delete) { try { - await BlocProvider.of(context).removeDocument(document); + await BlocProvider.of(context).remove(document); showSnackBar(context, S.of(context).documentDeleteSuccessMessage); } on ErrorMessage catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); diff --git a/lib/features/documents/view/pages/document_edit_page.dart b/lib/features/documents/view/pages/document_edit_page.dart index 7ca453a..f2cf832 100644 --- a/lib/features/documents/view/pages/document_edit_page.dart +++ b/lib/features/documents/view/pages/document_edit_page.dart @@ -83,7 +83,7 @@ class _DocumentEditPageState extends State { }); bool wasUpdated = false; try { - await getIt().updateDocument(updatedDocument); + await getIt().update(updatedDocument); showSnackBar(context, S.of(context).documentUpdateErrorMessage); wasUpdated = true; } on ErrorMessage catch (error, stackTrace) { diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index 44cd76c..4b30ef6 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -44,15 +44,13 @@ class _DocumentsPageState extends State { @override void initState() { super.initState(); - if (!BlocProvider.of(context).state.isLoaded) { - _initDocuments(); - } + _initDocuments(); _pagingController.addPageRequestListener(_loadNewPage); } Future _initDocuments() async { try { - BlocProvider.of(context).loadDocuments(); + BlocProvider.of(context).load(); } on ErrorMessage catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } @@ -113,7 +111,7 @@ class _DocumentsPageState extends State { previous != ConnectivityState.connected && current == ConnectivityState.connected, listener: (context, state) { - BlocProvider.of(context).loadDocuments(); + BlocProvider.of(context).load(); }, builder: (context, connectivityState) { return Scaffold( @@ -241,9 +239,7 @@ class _DocumentsPageState extends State { BlocProvider.value( value: BlocProvider.of(context)), ], - child: DocumentDetailsPage( - documentId: model.id, - ), + child: DocumentDetailsPage(documentId: model.id), ), ), ); diff --git a/lib/features/documents/view/widgets/selection/documents_page_app_bar.dart b/lib/features/documents/view/widgets/selection/documents_page_app_bar.dart index 53e2e29..7d40660 100644 --- a/lib/features/documents/view/widgets/selection/documents_page_app_bar.dart +++ b/lib/features/documents/view/widgets/selection/documents_page_app_bar.dart @@ -99,7 +99,7 @@ class _DocumentsPageAppBarState extends State { if (shouldDelete) { try { await BlocProvider.of(context) - .bulkRemoveDocuments(documentsState.selection); + .bulkRemove(documentsState.selection); showSnackBar( context, S.of(context).documentsPageBulkDeleteSuccessfulText, diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index fc05393..7fef3bf 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -1,10 +1,6 @@ -import 'dart:developer'; -import 'dart:isolate'; - import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_native_splash/flutter_native_splash.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'; @@ -13,12 +9,12 @@ 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/repository/document_repository.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'; -import 'package:paperless_mobile/features/inbox/view/inbox_page.dart'; -import 'package:paperless_mobile/features/labels/bloc/label_bloc_provider.dart'; +import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart'; +import 'package:paperless_mobile/features/inbox/view/pages/inbox_page.dart'; +import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.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'; @@ -26,7 +22,6 @@ 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/scan/bloc/document_scanner_cubit.dart'; import 'package:paperless_mobile/features/scan/view/scanner_page.dart'; -import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; import 'package:paperless_mobile/util.dart'; class HomePage extends StatefulWidget { @@ -42,30 +37,7 @@ class _HomePageState extends State { @override void initState() { super.initState(); - _initializeData(context).then( - (_) async { - FlutterNativeSplash.remove(); - if (BlocProvider.of(context) - .state - .showInboxOnStartup) { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => BlocProvider.value( - value: getIt(), - child: LabelBlocProvider( - child: BlocProvider.value( - value: DocumentsCubit(getIt()), - child: const InboxPage(), - ), - ), - ), - ), - ); - getIt().reloadDocuments(); - } - }, - ); + _initializeData(context); } @override @@ -89,15 +61,22 @@ class _HomePageState extends State { ), drawer: const InfoDrawer(), body: [ - BlocProvider.value( - value: getIt(), + MultiBlocProvider( + providers: [ + BlocProvider.value( + value: getIt(), + ), + ], child: const DocumentsPage(), ), BlocProvider.value( value: getIt(), child: const ScannerPage(), ), - const LabelsPage(), + BlocProvider.value( + value: getIt(), + child: const LabelsPage(), + ), ][_currentIndex], ); }, @@ -109,12 +88,12 @@ class _HomePageState extends State { return Future.wait([ BlocProvider.of(context) .updateInformtion(), - BlocProvider.of(context).updateStatistics(), - BlocProvider.of(context).initialize(), - BlocProvider.of(context).initialize(), - BlocProvider.of(context).initialize(), - BlocProvider.of(context).initialize(), - BlocProvider.of(context).initialize(), + getIt().updateStatistics(), + getIt().initialize(), + getIt().initialize(), + getIt().initialize(), + getIt().initialize(), + getIt().initialize(), ]); } on ErrorMessage catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); diff --git a/lib/features/home/view/widget/info_drawer.dart b/lib/features/home/view/widget/info_drawer.dart index 72e839d..89bdb8d 100644 --- a/lib/features/home/view/widget/info_drawer.dart +++ b/lib/features/home/view/widget/info_drawer.dart @@ -3,14 +3,15 @@ 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/labels/bloc/label_bloc_provider.dart'; +import 'package:paperless_mobile/features/inbox/bloc/inbox_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/model/error_message.dart'; import 'package:paperless_mobile/core/model/paperless_server_information.dart'; import 'package:paperless_mobile/core/model/paperless_statistics.dart'; import 'package:paperless_mobile/core/service/paperless_statistics_service.dart'; import 'package:paperless_mobile/features/documents/repository/document_repository.dart'; -import 'package:paperless_mobile/features/inbox/view/inbox_page.dart'; +import 'package:paperless_mobile/features/inbox/view/pages/inbox_page.dart'; import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart'; @@ -127,24 +128,7 @@ class InfoDrawer extends StatelessWidget { trailing: state.isLoaded ? Text(state.statistics!.documentsInInbox.toString()) : null, - onTap: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => BlocProvider.value( - value: getIt(), - child: LabelBlocProvider( - child: BlocProvider.value( - value: - DocumentsCubit(getIt()), - child: const InboxPage(), - ), - ), - ), - ), - ); - getIt().reloadDocuments(); - }, + onTap: () => _onOpenInbox(context), ); }, ), @@ -228,6 +212,27 @@ class InfoDrawer extends StatelessWidget { ); } + Future _onOpenInbox(BuildContext context) { + return Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => GlobalStateBlocProvider( + additionalProviders: [ + BlocProvider.value( + value: BlocProvider.of(context), + ), + BlocProvider.value( + value: getIt()..initialize(), + ), + BlocProvider.value( + value: getIt(), + ), + ], + child: const InboxPage(), + ), + ), + ); + } + Link _buildOnboardingImageCredits() { return Link( uri: Uri.parse( diff --git a/lib/features/inbox/bloc/inbox_cubit.dart b/lib/features/inbox/bloc/inbox_cubit.dart new file mode 100644 index 0000000..3fe2148 --- /dev/null +++ b/lib/features/inbox/bloc/inbox_cubit.dart @@ -0,0 +1,115 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:injectable/injectable.dart'; +import 'package:paperless_mobile/features/documents/model/bulk_edit.model.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/sort_field.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/inbox/bloc/state/inbox_state.dart'; +import 'package:paperless_mobile/features/labels/repository/label_repository.dart'; + +@injectable +class InboxCubit extends Cubit { + final LabelRepository _labelRepository; + final DocumentRepository _documentRepository; + + InboxCubit(this._labelRepository, this._documentRepository) + : super(const InboxState()); + + /// + /// Fetches inbox tag ids and loads the inbox items (documents). + /// + Future initialize() async { + final inboxTags = await _labelRepository.getTags().then( + (value) => value.where((t) => t.isInboxTag ?? false).map((t) => t.id!)); + final inboxDocuments = await _documentRepository + .find(DocumentFilter( + tags: AnyAssignedTagsQuery(tagIds: inboxTags), + sortField: SortField.added, + )) + .then((psr) => psr.results); + final newState = InboxState( + isLoaded: true, + inboxItems: inboxDocuments, + inboxTags: inboxTags, + ); + emit(newState); + } + + Future reloadInbox() async { + if (!state.isLoaded) { + throw "State has not yet loaded. Ensure the state is loaded when calling this method!"; + } + final inboxDocuments = await _documentRepository + .find(DocumentFilter( + tags: AnyAssignedTagsQuery(tagIds: state.inboxTags), + sortField: SortField.added, + )) + .then((psr) => psr.results); + emit(InboxState( + isLoaded: true, + inboxItems: inboxDocuments, + inboxTags: state.inboxTags, + )); + } + + /// + /// Updates the document with all inbox tags removed and removes the document + /// from the currently loaded inbox documents. + /// + Future> remove(DocumentModel document) async { + if (!state.isLoaded) { + throw "State has not yet loaded. 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, + overwriteTags: true, + ), + ); + emit( + InboxState( + isLoaded: true, + inboxTags: state.inboxTags, + inboxItems: state.inboxItems.where((doc) => doc.id != document.id), + ), + ); + + return tagsToRemove; + } + + Future undoRemove( + 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, + )); + } + + /// + /// 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)); + emit( + InboxState( + isLoaded: true, + inboxTags: state.inboxTags, + ), + ); + } +} diff --git a/lib/features/inbox/bloc/state/inbox_state.dart b/lib/features/inbox/bloc/state/inbox_state.dart new file mode 100644 index 0000000..9853dbb --- /dev/null +++ b/lib/features/inbox/bloc/state/inbox_state.dart @@ -0,0 +1,17 @@ +import 'package:equatable/equatable.dart'; +import 'package:paperless_mobile/features/documents/model/document.model.dart'; + +class InboxState with EquatableMixin { + final bool isLoaded; + final Iterable inboxTags; + final Iterable inboxItems; + + const InboxState({ + this.isLoaded = false, + this.inboxTags = const [], + this.inboxItems = const [], + }); + + @override + List get props => [isLoaded, inboxTags, inboxItems]; +} diff --git a/lib/features/inbox/view/inbox_page.dart b/lib/features/inbox/view/inbox_page.dart deleted file mode 100644 index 8cd7c42..0000000 --- a/lib/features/inbox/view/inbox_page.dart +++ /dev/null @@ -1,263 +0,0 @@ -import 'dart:developer'; - -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:intl/date_symbol_data_local.dart'; -import 'package:intl/intl.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/model/paperless_statistics_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/model/document.model.dart'; -import 'package:paperless_mobile/features/documents/model/document_filter.dart'; -import 'package:paperless_mobile/features/documents/model/query_parameters/tags_query.dart'; -import 'package:paperless_mobile/features/documents/view/pages/document_details_page.dart'; -import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart'; -import 'package:paperless_mobile/features/labels/bloc/label_bloc_provider.dart'; -import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.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'; - -class InboxPage extends StatefulWidget { - const InboxPage({super.key}); - - @override - State createState() => _InboxPageState(); -} - -class _InboxPageState extends State { - static const _a4AspectRatio = 1 / 1.4142; - - final GlobalKey _listKey = GlobalKey(); - Iterable _inboxTags = []; - @override - void initState() { - super.initState(); - initializeDateFormatting(); - _initInbox(); - } - - Future _initInbox() async { - final tags = BlocProvider.of(context).state.labels; - log("Loading documents with tags...${tags.values.join(",")}"); - _inboxTags = - tags.values.where((t) => t.isInboxTag ?? false).map((t) => t.id!); - final filter = - DocumentFilter(tags: AnyAssignedTagsQuery(tagIds: _inboxTags)); - return BlocProvider.of(context).updateFilter( - filter: filter, - ); - } - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, documentState) { - return Scaffold( - appBar: AppBar( - title: - BlocBuilder( - builder: (context, state) { - return Text( - S.of(context).bottomNavInboxPageLabel + - (state.isLoaded - ? ' (${state.statistics!.documentsInInbox})' - : ''), - ); - }, - ), - leading: IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.pop(context), - ), - ), - floatingActionButton: documentState.documents.isNotEmpty - ? FloatingActionButton.extended( - label: Text("Mark all as seen"), - icon: const Icon(Icons.done_all), - onPressed: () => - _onMarkAllAsSeen(documentState.documents, _inboxTags), - ) - : null, - body: Builder( - builder: (context) { - if (!documentState.isLoaded) { - return const Center(child: CircularProgressIndicator()); - } - if (documentState.documents.isEmpty) { - return Text( - "You do not have new documents in your inbox.", - textAlign: TextAlign.center, - ) // TODO: INTL - .padded(); - } - return Column( - children: [ - Text( - 'Hint: Swipe left to mark a document as read. This will remove all inbox tags from the document.', //TODO: INTL - style: Theme.of(context).textTheme.caption, - ).padded( - const EdgeInsets.only( - top: 4.0, - left: 8.0, - right: 8.0, - bottom: 8.0, - ), - ), - Expanded( - child: AnimatedList( - key: _listKey, - initialItemCount: documentState.documents.length, - itemBuilder: (context, index, animation) { - final doc = documentState.documents[index]; - return _buildListItem(context, doc); - }, - ), - ), - ], - ); - }, - ), - ); - }, - ); - } - - Widget _buildListItem(BuildContext context, DocumentModel doc) { - return Dismissible( - direction: DismissDirection.endToStart, - background: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Icon( - Icons.done, - color: Theme.of(context).colorScheme.primary, - ).padded(), - Text( - 'Mark as read', //TODO: INTL - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - ), - ), - ], - ).padded(), - confirmDismiss: (_) => _onItemDismissed(doc), - key: ObjectKey(doc.id), - child: ListTile( - title: Text(doc.title), - isThreeLine: true, - leading: AspectRatio( - aspectRatio: _a4AspectRatio, - child: DocumentPreview( - id: doc.id, - fit: BoxFit.cover, - alignment: Alignment.topCenter, - ), - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(DateFormat().format(doc.added)), - TagsWidget(tagIds: doc.tags.where((id) => _inboxTags.contains(id))) - ], - ), - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (_) => LabelBlocProvider( - child: BlocProvider.value( - value: BlocProvider.of(context), - child: DocumentDetailsPage( - documentId: doc.id, - allowEdit: false, - isLabelClickable: false, - ), - ), - ), - ), - ), - ), - ); - } - - 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( - List documents, - Iterable inboxTags, - ) async { - for (int i = documents.length - 1; i >= 0; i--) { - final doc = documents[i]; - _listKey.currentState?.removeItem( - 0, - (context, animation) => _buildSlideAnimation( - context, - animation, - _buildListItem(context, doc), - ), - ); - await Future.delayed(const Duration(milliseconds: 75)); - } - await BlocProvider.of(context) - .bulkEditTags(documents, removeTags: inboxTags); - BlocProvider.of(context).resetInboxCount(); - } - - Future _onItemDismissed(DocumentModel doc) async { - try { - final removedTags = await BlocProvider.of(context) - .removeInboxTags(doc, _inboxTags); - 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), - ), - ); - return true; - } on ErrorMessage catch (error, stackTrace) { - showErrorMessage(context, error, stackTrace); - return false; - } catch (error) { - showErrorMessage( - context, - const ErrorMessage.unknown(), - ); - return false; - } - } - - Future _onUndoMarkAsSeen( - DocumentModel doc, Iterable removedTags) async { - try { - await BlocProvider.of(context).updateDocument( - doc.copyWith( - tags: {...doc.tags, ...removedTags}, - overwriteTags: true, - ), - ); - BlocProvider.of(context).incrementInboxCount(); - BlocProvider.of(context).reloadDocuments(); - } on ErrorMessage catch (error, stackTrace) { - showErrorMessage(context, error, stackTrace); - } - } -} diff --git a/lib/features/inbox/view/pages/inbox_page.dart b/lib/features/inbox/view/pages/inbox_page.dart new file mode 100644 index 0000000..0eb8a86 --- /dev/null +++ b/lib/features/inbox/view/pages/inbox_page.dart @@ -0,0 +1,202 @@ +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'; +import 'package:paperless_mobile/features/inbox/view/widgets/document_inbox_item.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:paperless_mobile/util.dart'; + +class InboxPage extends StatefulWidget { + const InboxPage({super.key}); + + @override + State createState() => _InboxPageState(); +} + +class _InboxPageState extends State { + final GlobalKey _listKey = GlobalKey(); + + @override + void initState() { + super.initState(); + initializeDateFormatting(); + } + + @override + Widget build(BuildContext context) { + final bloc = BlocProvider.of(context); + return Scaffold( + appBar: AppBar( + title: Text(S.of(context).bottomNavInboxPageLabel), + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.pop(context), + ), + ), + floatingActionButton: BlocBuilder( + builder: (context, state) { + return FloatingActionButton.extended( + label: Text("Mark all as seen"), + icon: const Icon(Icons.done_all), + onPressed: state.isLoaded && state.inboxItems.isNotEmpty + ? () => _onMarkAllAsSeen( + bloc.state.inboxItems, + bloc.state.inboxTags, + ) + : null, + ); + }, + ), + body: BlocBuilder( + builder: (context, state) { + if (!state.isLoaded) { + return const DocumentsListLoadingWidget(); + } + + if (state.inboxItems.isEmpty) { + return Text( + "You do not have new documents in your inbox.", + textAlign: TextAlign.center, + ).padded(); + } + return RefreshIndicator( + onRefresh: () => BlocProvider.of(context).reloadInbox(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + 'Hint: Swipe left to mark a document as seen. This will remove all inbox tags from the document.', //TODO: INTL + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.caption, + ).padded( + const EdgeInsets.only( + top: 4.0, + left: 8.0, + right: 8.0, + bottom: 8.0, + ), + ), + Expanded( + child: AnimatedList( + key: _listKey, + initialItemCount: state.inboxItems.length, + itemBuilder: (context, index, animation) { + final doc = state.inboxItems.elementAt(index); + return _buildListItem(context, doc); + }, + ), + ), + ], + ), + ); + }, + ), + ); + } + + Widget _buildListItem(BuildContext context, DocumentModel doc) { + return Dismissible( + direction: DismissDirection.endToStart, + background: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Icon( + Icons.done, + color: Theme.of(context).colorScheme.primary, + ).padded(), + Text( + 'Mark as read', //TODO: INTL + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + ), + ), + ], + ).padded(), + confirmDismiss: (_) => _onItemDismissed(doc), + key: ObjectKey(doc.id), + 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)); + } + 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), + ), + ); + return true; + } on ErrorMessage catch (error, stackTrace) { + showErrorMessage(context, error, stackTrace); + return false; + } catch (error) { + showErrorMessage( + context, + const ErrorMessage.unknown(), + ); + return false; + } + } + + Future _onUndoMarkAsSeen( + DocumentModel document, + Iterable removedTags, + ) async { + 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 new file mode 100644 index 0000000..7527bc5 --- /dev/null +++ b/lib/features/inbox/view/widgets/document_inbox_item.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:intl/intl.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/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'; + +class DocumentInboxItem extends StatelessWidget { + final DocumentModel document; + + const DocumentInboxItem({ + super.key, + required this.document, + }); + static const _a4AspectRatio = 1 / 1.4142; + @override + Widget build(BuildContext context) { + return ListTile( + title: Text(document.title), + isThreeLine: true, + leading: AspectRatio( + aspectRatio: _a4AspectRatio, + child: DocumentPreview( + id: document.id, + fit: BoxFit.cover, + alignment: Alignment.topCenter, + ), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(DateFormat().format(document.added)), + TagsWidget( + tagIds: document.tags, + isMultiLine: false, + isClickable: false, + ), + ], + ), + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => GlobalStateBlocProvider( + additionalProviders: [ + BlocProvider.value( + value: BlocProvider.of(context)), + ], + child: DocumentDetailsPage( + documentId: document.id, + allowEdit: false, + isLabelClickable: false, + ), + ), + ), + ), + ); + } +} diff --git a/lib/features/labels/bloc/label_bloc_provider.dart b/lib/features/labels/bloc/global_state_bloc_provider.dart similarity index 80% rename from lib/features/labels/bloc/label_bloc_provider.dart rename to lib/features/labels/bloc/global_state_bloc_provider.dart index 78d56f9..3de0a76 100644 --- a/lib/features/labels/bloc/label_bloc_provider.dart +++ b/lib/features/labels/bloc/global_state_bloc_provider.dart @@ -7,9 +7,14 @@ 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'; -class LabelBlocProvider extends StatelessWidget { +class GlobalStateBlocProvider extends StatelessWidget { + final List additionalProviders; final Widget child; - const LabelBlocProvider({super.key, required this.child}); + const GlobalStateBlocProvider({ + super.key, + this.additionalProviders = const [], + required this.child, + }); @override Widget build(BuildContext context) { @@ -20,6 +25,7 @@ class LabelBlocProvider extends StatelessWidget { BlocProvider.value(value: getIt()), BlocProvider.value(value: getIt()), BlocProvider.value(value: getIt()), + ...additionalProviders, ], child: child, ); 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 e9e9d2b..df5a8f5 100644 --- a/lib/features/labels/tags/view/pages/edit_tag_page.dart +++ b/lib/features/labels/tags/view/pages/edit_tag_page.dart @@ -24,7 +24,7 @@ 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 documetns in inbox may increase/decrease. + //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), diff --git a/lib/features/labels/view/pages/labels_page.dart b/lib/features/labels/view/pages/labels_page.dart index 165e305..a359f24 100644 --- a/lib/features/labels/view/pages/labels_page.dart +++ b/lib/features/labels/view/pages/labels_page.dart @@ -1,7 +1,7 @@ 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/label_bloc_provider.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'; import 'package:paperless_mobile/features/documents/model/document_filter.dart'; @@ -54,135 +54,130 @@ class _LabelsPageState extends State @override Widget build(BuildContext context) { - return BlocProvider.value( - value: getIt(), - child: DefaultTabController( - length: 3, - child: Scaffold( - drawer: const InfoDrawer(), - appBar: AppBar( - title: Text( - [ - S.of(context).labelsPageCorrespondentsTitleText, - S.of(context).labelsPageDocumentTypesTitleText, - S.of(context).labelsPageTagsTitleText, - S.of(context).labelsPageStoragePathTitleText - ][_currentIndex], - ), - actions: [ - IconButton( - onPressed: _onAddPressed, - icon: const Icon(Icons.add), - ) - ], - bottom: PreferredSize( - preferredSize: const Size.fromHeight(kToolbarHeight), - child: ColoredBox( - color: Theme.of(context).bottomAppBarColor, - child: TabBar( - indicatorColor: Theme.of(context).colorScheme.primary, - controller: _tabController, - tabs: [ - Tab( - icon: Icon( - Icons.person_outline, - color: Theme.of(context).colorScheme.onPrimaryContainer, - ), + return DefaultTabController( + length: 3, + child: Scaffold( + drawer: const InfoDrawer(), + appBar: AppBar( + title: Text( + [ + S.of(context).labelsPageCorrespondentsTitleText, + S.of(context).labelsPageDocumentTypesTitleText, + S.of(context).labelsPageTagsTitleText, + S.of(context).labelsPageStoragePathTitleText + ][_currentIndex], + ), + actions: [ + IconButton( + onPressed: _onAddPressed, + icon: const Icon(Icons.add), + ) + ], + bottom: PreferredSize( + preferredSize: const Size.fromHeight(kToolbarHeight), + child: ColoredBox( + color: Theme.of(context).bottomAppBarColor, + child: TabBar( + indicatorColor: Theme.of(context).colorScheme.primary, + controller: _tabController, + tabs: [ + Tab( + icon: Icon( + Icons.person_outline, + color: Theme.of(context).colorScheme.onPrimaryContainer, ), - Tab( - icon: Icon( - Icons.description_outlined, - color: Theme.of(context).colorScheme.onPrimaryContainer, - ), + ), + Tab( + icon: Icon( + Icons.description_outlined, + color: Theme.of(context).colorScheme.onPrimaryContainer, ), - Tab( - icon: Icon( - Icons.label_outline, - color: Theme.of(context).colorScheme.onPrimaryContainer, - ), + ), + Tab( + icon: Icon( + Icons.label_outline, + color: Theme.of(context).colorScheme.onPrimaryContainer, ), - Tab( - icon: Icon( - Icons.folder_open, - color: Theme.of(context).colorScheme.onPrimaryContainer, - ), - ) - ], - ), + ), + Tab( + icon: Icon( + Icons.folder_open, + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), + ) + ], ), ), ), - body: TabBarView( - controller: _tabController, - children: [ - LabelTabView( - cubit: BlocProvider.of(context), - filterBuilder: (label) => DocumentFilter( - correspondent: CorrespondentQuery.fromId(label.id), - pageSize: label.documentCount ?? 0, - ), - onOpenEditPage: _openEditCorrespondentPage, - emptyStateActionButtonLabel: - S.of(context).labelsPageCorrespondentEmptyStateAddNewLabel, - emptyStateDescription: S - .of(context) - .labelsPageCorrespondentEmptyStateDescriptionText, - onOpenAddNewPage: _onAddPressed, + ), + body: TabBarView( + controller: _tabController, + children: [ + LabelTabView( + cubit: BlocProvider.of(context), + filterBuilder: (label) => DocumentFilter( + correspondent: CorrespondentQuery.fromId(label.id), + pageSize: label.documentCount ?? 0, ), - LabelTabView( - cubit: BlocProvider.of(context), - filterBuilder: (label) => DocumentFilter( - documentType: DocumentTypeQuery.fromId(label.id), - pageSize: label.documentCount ?? 0, - ), - onOpenEditPage: _openEditDocumentTypePage, - emptyStateActionButtonLabel: - S.of(context).labelsPageDocumentTypeEmptyStateAddNewLabel, - emptyStateDescription: S - .of(context) - .labelsPageDocumentTypeEmptyStateDescriptionText, - onOpenAddNewPage: _onAddPressed, + onOpenEditPage: _openEditCorrespondentPage, + emptyStateActionButtonLabel: + S.of(context).labelsPageCorrespondentEmptyStateAddNewLabel, + emptyStateDescription: S + .of(context) + .labelsPageCorrespondentEmptyStateDescriptionText, + onOpenAddNewPage: _onAddPressed, + ), + LabelTabView( + cubit: BlocProvider.of(context), + filterBuilder: (label) => DocumentFilter( + documentType: DocumentTypeQuery.fromId(label.id), + pageSize: label.documentCount ?? 0, ), - LabelTabView( - cubit: BlocProvider.of(context), - filterBuilder: (label) => DocumentFilter( - tags: IdsTagsQuery.fromIds([label.id!]), - pageSize: label.documentCount ?? 0, - ), - onOpenEditPage: _openEditTagPage, - leadingBuilder: (t) => CircleAvatar( - backgroundColor: t.color, - child: t.isInboxTag ?? false - ? Icon( - Icons.inbox, - color: t.textColor, - ) - : null, - ), - contentBuilder: (t) => Text(t.match ?? ''), - emptyStateActionButtonLabel: - S.of(context).labelsPageTagsEmptyStateAddNewLabel, - emptyStateDescription: - S.of(context).labelsPageTagsEmptyStateDescriptionText, - onOpenAddNewPage: _onAddPressed, + onOpenEditPage: _openEditDocumentTypePage, + emptyStateActionButtonLabel: + S.of(context).labelsPageDocumentTypeEmptyStateAddNewLabel, + emptyStateDescription: + S.of(context).labelsPageDocumentTypeEmptyStateDescriptionText, + onOpenAddNewPage: _onAddPressed, + ), + LabelTabView( + cubit: BlocProvider.of(context), + filterBuilder: (label) => DocumentFilter( + tags: IdsTagsQuery.fromIds([label.id!]), + pageSize: label.documentCount ?? 0, ), - LabelTabView( - cubit: BlocProvider.of(context), - onOpenEditPage: _openEditStoragePathPage, - filterBuilder: (label) => DocumentFilter( - storagePath: StoragePathQuery.fromId(label.id), - pageSize: label.documentCount ?? 0, - ), - contentBuilder: (path) => Text(path.path ?? ""), - emptyStateActionButtonLabel: - S.of(context).labelsPageStoragePathEmptyStateAddNewLabel, - emptyStateDescription: S - .of(context) - .labelsPageStoragePathEmptyStateDescriptionText, - onOpenAddNewPage: _onAddPressed, + onOpenEditPage: _openEditTagPage, + leadingBuilder: (t) => CircleAvatar( + backgroundColor: t.color, + child: t.isInboxTag ?? false + ? Icon( + Icons.inbox, + color: t.textColor, + ) + : null, ), - ], - ), + contentBuilder: (t) => Text(t.match ?? ''), + emptyStateActionButtonLabel: + S.of(context).labelsPageTagsEmptyStateAddNewLabel, + emptyStateDescription: + S.of(context).labelsPageTagsEmptyStateDescriptionText, + onOpenAddNewPage: _onAddPressed, + ), + LabelTabView( + cubit: BlocProvider.of(context), + onOpenEditPage: _openEditStoragePathPage, + filterBuilder: (label) => DocumentFilter( + storagePath: StoragePathQuery.fromId(label.id), + pageSize: label.documentCount ?? 0, + ), + contentBuilder: (path) => Text(path.path ?? ""), + emptyStateActionButtonLabel: + S.of(context).labelsPageStoragePathEmptyStateAddNewLabel, + emptyStateDescription: + S.of(context).labelsPageStoragePathEmptyStateDescriptionText, + onOpenAddNewPage: _onAddPressed, + ), + ], ), ), ); @@ -192,11 +187,9 @@ class _LabelsPageState extends State Navigator.push( context, MaterialPageRoute( - builder: (_) => MultiBlocProvider( - providers: [ - BlocProvider.value(value: getIt()), - BlocProvider.value( - value: BlocProvider.of(context)), + builder: (_) => GlobalStateBlocProvider( + additionalProviders: [ + BlocProvider.value(value: BlocProvider.of(context)), ], child: EditCorrespondentPage(correspondent: correspondent), ), @@ -208,11 +201,9 @@ class _LabelsPageState extends State Navigator.push( context, MaterialPageRoute( - builder: (_) => MultiBlocProvider( - providers: [ - BlocProvider.value(value: getIt()), - BlocProvider.value( - value: BlocProvider.of(context)), + builder: (_) => GlobalStateBlocProvider( + additionalProviders: [ + BlocProvider.value(value: BlocProvider.of(context)), ], child: EditDocumentTypePage(documentType: docType), ), @@ -224,10 +215,9 @@ class _LabelsPageState extends State Navigator.push( context, MaterialPageRoute( - builder: (_) => MultiBlocProvider( - providers: [ - BlocProvider.value(value: getIt()), - BlocProvider.value(value: BlocProvider.of(context)), + builder: (_) => GlobalStateBlocProvider( + additionalProviders: [ + BlocProvider.value(value: BlocProvider.of(context)), BlocProvider.value(value: getIt()), ], child: EditTagPage(tag: tag), @@ -240,11 +230,9 @@ class _LabelsPageState extends State Navigator.push( context, MaterialPageRoute( - builder: (_) => MultiBlocProvider( - providers: [ + builder: (_) => GlobalStateBlocProvider( + additionalProviders: [ BlocProvider.value(value: getIt()), - BlocProvider.value( - value: BlocProvider.of(context)), ], child: EditStoragePathPage(storagePath: path), ), @@ -269,7 +257,7 @@ class _LabelsPageState extends State case 3: page = const AddStoragePathPage(); } - return LabelBlocProvider(child: page); + return GlobalStateBlocProvider(child: page); }, )); } diff --git a/lib/features/labels/view/widgets/label_item.dart b/lib/features/labels/view/widgets/label_item.dart index 7104ced..4dd15b7 100644 --- a/lib/features/labels/view/widgets/label_item.dart +++ b/lib/features/labels/view/widgets/label_item.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_mobile/features/labels/bloc/label_bloc_provider.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:paperless_mobile/core/model/error_message.dart'; import 'package:paperless_mobile/di_initializer.dart'; @@ -8,7 +8,8 @@ import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/model/document_filter.dart'; import 'package:paperless_mobile/features/documents/repository/document_repository.dart'; import 'package:paperless_mobile/features/labels/model/label.model.dart'; -import 'package:paperless_mobile/features/labels/view/widgets/linked_documents_preview.dart'; +import 'package:paperless_mobile/features/linked_documents_preview/bloc/linked_documents_cubit.dart'; +import 'package:paperless_mobile/features/linked_documents_preview/view/pages/linked_documents_page.dart'; class LabelItem extends StatelessWidget { final T label; @@ -50,13 +51,13 @@ class LabelItem extends StatelessWidget { Navigator.push( context, MaterialPageRoute( - builder: (context) => LabelBlocProvider( - child: BlocProvider( - create: (context) => - DocumentsCubit(getIt()) - ..updateFilter(filter: filter), - child: LinkedDocumentsPreview(filter: filter), - ), + builder: (context) => GlobalStateBlocProvider( + additionalProviders: [ + BlocProvider.value( + value: getIt() + ..initialize(filter)), + ], + child: const LinkedDocumentsPage(), ), ), ); diff --git a/lib/features/labels/view/widgets/linked_documents_preview.dart b/lib/features/labels/view/widgets/linked_documents_preview.dart deleted file mode 100644 index 8ab6c6d..0000000 --- a/lib/features/labels/view/widgets/linked_documents_preview.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_mobile/features/labels/bloc/label_bloc_provider.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/model/document_filter.dart'; -import 'package:paperless_mobile/features/documents/view/pages/document_details_page.dart'; -import 'package:paperless_mobile/features/documents/view/widgets/list/document_list.dart'; -import 'package:paperless_mobile/generated/l10n.dart'; -import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; - -class LinkedDocumentsPreview extends StatefulWidget { - final DocumentFilter filter; - - const LinkedDocumentsPreview({super.key, required this.filter}); - - @override - State createState() => _LinkedDocumentsPreviewState(); -} - -class _LinkedDocumentsPreviewState extends State { - final _pagingController = - PagingController(firstPageKey: 1); - - @override - void initState() { - super.initState(); - _pagingController.nextPageKey = null; - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(S.of(context).linkedDocumentsPageTitle), - ), - body: BlocBuilder( - builder: (context, state) { - _pagingController.itemList = state.documents; - return Column( - children: [ - Text( - S.of(context).referencedDocumentsReadOnlyHintText, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.caption, - ), - Expanded( - child: CustomScrollView( - slivers: [ - DocumentListView( - isLabelClickable: false, - onTap: (doc) { - Navigator.push( - context, - MaterialPageRoute( - builder: (ctxt) => LabelBlocProvider( - child: BlocProvider.value( - value: BlocProvider.of(context), - child: DocumentDetailsPage( - documentId: doc.id, - allowEdit: false, - isLabelClickable: false, - ), - ), - ), - ), - ); - }, - pagingController: _pagingController, - state: state, - onSelected: BlocProvider.of(context) - .toggleDocumentSelection, - hasInternetConnection: true, - ), - ], - ), - ), - ], - ); - }, - ), - ); - } -} diff --git a/lib/features/linked_documents_preview/bloc/linked_documents_cubit.dart b/lib/features/linked_documents_preview/bloc/linked_documents_cubit.dart new file mode 100644 index 0000000..2f23c5c --- /dev/null +++ b/lib/features/linked_documents_preview/bloc/linked_documents_cubit.dart @@ -0,0 +1,26 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:injectable/injectable.dart'; +import 'package:paperless_mobile/features/documents/model/document_filter.dart'; +import 'package:paperless_mobile/features/documents/repository/document_repository.dart'; +import 'package:paperless_mobile/features/linked_documents_preview/bloc/state/linked_documents_state.dart'; + +@injectable +class LinkedDocumentsCubit extends Cubit { + final DocumentRepository _documentRepository; + + LinkedDocumentsCubit(this._documentRepository) + : super(LinkedDocumentsState()); + + Future initialize(DocumentFilter filter) async { + final documents = await _documentRepository.find( + filter.copyWith( + pageSize: 100, + ), + ); + emit(LinkedDocumentsState( + isLoaded: true, + documents: documents, + filter: filter, + )); + } +} diff --git a/lib/features/linked_documents_preview/bloc/state/linked_documents_state.dart b/lib/features/linked_documents_preview/bloc/state/linked_documents_state.dart new file mode 100644 index 0000000..76f5ace --- /dev/null +++ b/lib/features/linked_documents_preview/bloc/state/linked_documents_state.dart @@ -0,0 +1,15 @@ +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/paged_search_result.dart'; + +class LinkedDocumentsState { + final bool isLoaded; + final PagedSearchResult? documents; + final DocumentFilter? filter; + + LinkedDocumentsState({ + this.filter, + this.isLoaded = false, + this.documents, + }); +} 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 new file mode 100644 index 0000000..bf7a6a5 --- /dev/null +++ b/lib/features/linked_documents_preview/view/pages/linked_documents_page.dart @@ -0,0 +1,100 @@ +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/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/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'; +import 'package:paperless_mobile/features/linked_documents_preview/bloc/linked_documents_cubit.dart'; +import 'package:paperless_mobile/features/linked_documents_preview/bloc/state/linked_documents_state.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; + +class LinkedDocumentsPage extends StatefulWidget { + const LinkedDocumentsPage({super.key}); + + @override + State createState() => _LinkedDocumentsPageState(); +} + +class _LinkedDocumentsPageState extends State { + final _pagingController = + PagingController(firstPageKey: 1); + + @override + void initState() { + super.initState(); + _pagingController.nextPageKey = null; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(S.of(context).linkedDocumentsPageTitle), + ), + body: BlocBuilder( + builder: (context, state) { + if (!state.isLoaded) { + return const DocumentsListLoadingWidget(); + } + + _pagingController.itemList = state.documents!.results; + return Column( + children: [ + Text( + S.of(context).referencedDocumentsReadOnlyHintText, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.caption, + ), + Expanded( + child: CustomScrollView( + slivers: [ + PagedSliverList( + pagingController: _pagingController, + builderDelegate: PagedChildBuilderDelegate( + animateTransitions: true, + itemBuilder: (context, document, index) { + return DocumentListItem( + isLabelClickable: false, + document: document, + onTap: (doc) { + Navigator.push( + context, + MaterialPageRoute( + builder: (ctxt) => GlobalStateBlocProvider( + additionalProviders: [ + BlocProvider.value( + value: BlocProvider.of( + context, + ), + ), + ], + child: DocumentDetailsPage( + documentId: doc.id, + allowEdit: false, + isLabelClickable: false, + ), + ), + ), + ); + }, + isSelected: false, + isAtLeastOneSelected: false, + ); + }, + ), + ), + ], + ), + ), + ], + ); + }, + ), + ); + } +} diff --git a/lib/features/scan/bloc/document_scanner_cubit.dart b/lib/features/scan/bloc/document_scanner_cubit.dart index 92f263d..3ea1144 100644 --- a/lib/features/scan/bloc/document_scanner_cubit.dart +++ b/lib/features/scan/bloc/document_scanner_cubit.dart @@ -6,12 +6,16 @@ import 'package:flutter/material.dart'; 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/features/documents/model/document.model.dart'; +import 'package:paperless_mobile/features/documents/repository/document_repository.dart'; -@singleton +@injectable class DocumentScannerCubit extends Cubit> { + final DocumentRepository documentRepository; + static List initialState = []; - DocumentScannerCubit() : super(initialState); + DocumentScannerCubit(this.documentRepository) : super(initialState); void addScan(File file) => emit([...state, file]); @@ -40,4 +44,30 @@ class DocumentScannerCubit extends Cubit> { throw const ErrorMessage(ErrorCode.scanRemoveFailed); } } + + Future uploadDocument( + Uint8List bytes, + String fileName, { + required String title, + required void Function(DocumentModel document)? onConsumptionFinished, + int? documentType, + int? correspondent, + Iterable tags = const [], + DateTime? createdAt, + }) async { + await documentRepository.create( + bytes, + fileName, + title: title, + documentType: documentType, + correspondent: correspondent, + tags: tags, + createdAt: createdAt, + ); + if (onConsumptionFinished != null) { + documentRepository + .waitForConsumptionFinished(fileName, title) + .then((value) => onConsumptionFinished(value)); + } + } } diff --git a/lib/features/scan/view/document_upload_page.dart b/lib/features/scan/view/document_upload_page.dart index ed9a3fd..8306759 100644 --- a/lib/features/scan/view/document_upload_page.dart +++ b/lib/features/scan/view/document_upload_page.dart @@ -35,6 +35,7 @@ class DocumentUploadPage extends StatefulWidget { final String? title; final String? filename; final void Function()? afterUpload; + final void Function(DocumentModel)? onSuccessfullyConsumed; const DocumentUploadPage({ Key? key, @@ -42,6 +43,7 @@ class DocumentUploadPage extends StatefulWidget { this.afterUpload, this.title, this.filename, + this.onSuccessfullyConsumed, }) : super(key: key); @override @@ -229,6 +231,7 @@ class _DocumentUploadPageState extends State { void _onSubmit() async { if (_formKey.currentState?.saveAndValidate() ?? false) { + final cubit = BlocProvider.of(context); try { setState(() => _isUploadLoading = true); @@ -240,17 +243,19 @@ class _DocumentUploadPageState extends State { final tags = fv[DocumentModel.tagsKey] as IdsTagsQuery; final correspondent = fv[DocumentModel.correspondentKey] as IdQueryParameter; - await BlocProvider.of(context).addDocument( + + await cubit.uploadDocument( widget.fileBytes, _padWithPdfExtension(_formKey.currentState?.value[fkFileName]), - onConsumptionFinished: _onConsumptionFinished, + onConsumptionFinished: widget.onSuccessfullyConsumed, title: title, documentType: docType.id, correspondent: correspondent.id, tags: tags.ids, createdAt: createdAt, ); - getIt().reset(); //TODO: Access via provider + + cubit.reset(); //TODO: Access via provider showSnackBar(context, S.of(context).documentUploadSuccessText); Navigator.pop(context); widget.afterUpload?.call(); @@ -275,24 +280,4 @@ class _DocumentUploadPageState extends State { String _formatFilename(String source) { return source.replaceAll(RegExp(r"[\W_]"), "_"); } - - void _onConsumptionFinished(DocumentModel document) { - // ScaffoldMessenger.of(rootScaffoldKey.currentContext!).showSnackBar( - // SnackBar( - // action: SnackBarAction( - // onPressed: () async { - // try { - // getIt().reloadDocuments(); - // } on ErrorMessage catch (error, stackTrace) { - // showErrorMessage(context, error, stackTrace); - // } - // }, - // label: - // S.of(context).documentUploadProcessingSuccessfulReloadActionText, - // ), - // content: Text(S.of(context).documentUploadProcessingSuccessfulText), - // ), - // ); - getIt().incrementInboxCount(); - } } diff --git a/lib/features/scan/view/scanner_page.dart b/lib/features/scan/view/scanner_page.dart index 12ffcba..024e4e6 100644 --- a/lib/features/scan/view/scanner_page.dart +++ b/lib/features/scan/view/scanner_page.dart @@ -8,7 +8,8 @@ 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/features/labels/bloc/label_bloc_provider.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'; import 'package:paperless_mobile/core/service/file_service.dart'; @@ -127,12 +128,17 @@ class _ScannerPageState extends State final bytes = await doc.save(); Navigator.of(context).push( MaterialPageRoute( - builder: (context) => BlocProvider.value( - value: getIt(), - child: LabelBlocProvider( - child: DocumentUploadPage( - fileBytes: bytes, + builder: (_) => GlobalStateBlocProvider( + additionalProviders: [ + BlocProvider.value( + value: BlocProvider.of(context), ), + ], + child: DocumentUploadPage( + fileBytes: bytes, + onSuccessfullyConsumed: (_) => + BlocProvider.of(context) + .updateStatistics(), ), ), ), @@ -242,17 +248,20 @@ class _ScannerPageState extends State // pdf fileBytes = file.readAsBytesSync(); } - - Navigator.push( - context, + Navigator.of(context).push( MaterialPageRoute( - builder: (context) => BlocProvider.value( - value: getIt(), - child: LabelBlocProvider( - child: DocumentUploadPage( - filename: filename, - fileBytes: fileBytes, + builder: (_) => GlobalStateBlocProvider( + additionalProviders: [ + BlocProvider.value( + value: BlocProvider.of(context), ), + ], + 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 d788459..449667a 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -183,7 +183,7 @@ "labelsPageTagsEmptyStateDescriptionText": "Es wurden noch keine Tags angelegt.", "labelsPageStoragePathEmptyStateAddNewLabel": "Erstelle neuen Speicherpfad", "labelsPageStoragePathEmptyStateDescriptionText": "Es wurden noch keine Speicherpfade angelegt.", - "referencedDocumentsReadOnlyHintText": "Dies ist eine schreibgeschützte Ansicht! Dokumente können nicht bearbeitet oder entfernt werden.", + "referencedDocumentsReadOnlyHintText": "Dies ist eine schreibgeschützte Ansicht! Dokumente können nicht bearbeitet oder entfernt werden. Es werden maximal 100 referenzierte Dokumente geladen.", "editLabelPageConfirmDeletionDialogTitle": "Löschen bestätigen", "editLabelPageDeletionDialogText": "Dieser Kennzeichner wird von Dokumenten referenziert. Durch das Löschen dieses Kennzeichners werden alle Referenzen entfernt. Fortfahren?", "settingsPageStorageSettingsLabel": "Speicher", diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 8460fff..710460f 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -184,7 +184,7 @@ "labelsPageTagsEmptyStateDescriptionText": "You don't seem to have any tags set up.", "labelsPageStoragePathEmptyStateAddNewLabel": "Add new storage path", "labelsPageStoragePathEmptyStateDescriptionText": "You don't seem to have any storage paths set up.", - "referencedDocumentsReadOnlyHintText": "This is a read-only view! You cannot edit or remove documents.", + "referencedDocumentsReadOnlyHintText": "This is a read-only view! You cannot edit or remove documents. A maximum of 100 referenced documents will be loaded.", "editLabelPageConfirmDeletionDialogTitle": "Confirm deletion", "editLabelPageDeletionDialogText": "This label contains references to other documents. By deleting this label, all references will be removed. Continue?", "settingsPageStorageSettingsLabel": "Storage", diff --git a/lib/main.dart b/lib/main.dart index 720a9ea..76a900a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,7 +12,7 @@ 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/label_bloc_provider.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'; import 'package:paperless_mobile/core/global/constants.dart'; @@ -26,6 +26,7 @@ import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/home/view/home_page.dart'; import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart'; import 'package:paperless_mobile/features/login/view/login_page.dart'; +import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart'; import 'package:paperless_mobile/features/scan/view/document_upload_page.dart'; import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; @@ -50,17 +51,18 @@ void main() async { await getIt().initialize(); await getIt().initialize(); - runApp(const MyApp()); + runApp(const PaperlessMobileEntrypoint()); } -class MyApp extends StatefulWidget { - const MyApp({Key? key}) : super(key: key); +class PaperlessMobileEntrypoint extends StatefulWidget { + const PaperlessMobileEntrypoint({Key? key}) : super(key: key); @override - State createState() => _MyAppState(); + State createState() => + _PaperlessMobileEntrypointState(); } -class _MyAppState extends State { +class _PaperlessMobileEntrypointState extends State { @override Widget build(BuildContext context) { return MultiBlocProvider( @@ -68,6 +70,7 @@ class _MyAppState extends State { BlocProvider.value(value: getIt()), BlocProvider.value(value: getIt()), BlocProvider.value(value: getIt()), + BlocProvider.value(value: getIt()), BlocProvider.value(value: getIt()), ], child: BlocBuilder( @@ -172,20 +175,26 @@ class _AuthenticationWrapperState extends State { Navigator.push( context, MaterialPageRoute( - builder: (context) => BlocProvider.value( - value: getIt(), - child: LabelBlocProvider( - child: DocumentUploadPage( - fileBytes: bytes, - afterUpload: () => SystemNavigator.pop(), - filename: filename, - ), + builder: (context) => GlobalStateBlocProvider( + additionalProviders: [ + BlocProvider.value(value: getIt()), + ], + child: DocumentUploadPage( + fileBytes: bytes, + afterUpload: SystemNavigator.pop, + filename: filename, ), ), ), ); } + @override + void didChangeDependencies() { + FlutterNativeSplash.remove(); + super.didChangeDependencies(); + } + @override void initState() { super.initState(); @@ -218,14 +227,14 @@ class _AuthenticationWrapperState extends State { }, builder: (context, authentication) { if (authentication.isAuthenticated) { - return BlocProvider.value( - value: getIt(), - child: const LabelBlocProvider( - child: HomePage(), - ), + return GlobalStateBlocProvider( + additionalProviders: [ + BlocProvider.value(value: getIt()), + BlocProvider.value(value: getIt()), + ], + child: const HomePage(), ); } else { - FlutterNativeSplash.remove(); return const LoginPage(); } }, diff --git a/test/src/bloc/document_cubit_test.dart b/test/src/bloc/document_cubit_test.dart index 3a1ea64..452caa9 100644 --- a/test/src/bloc/document_cubit_test.dart +++ b/test/src/bloc/document_cubit_test.dart @@ -54,7 +54,7 @@ void main() async { ), build: () => DocumentsCubit(documentRepository), seed: () => DocumentsState.initial, - act: (bloc) => bloc.loadDocuments(), + act: (bloc) => bloc.load(), expect: () => [ DocumentsState( isLoaded: true, @@ -83,7 +83,7 @@ void main() async { ), build: () => DocumentsCubit(documentRepository), seed: () => DocumentsState.initial, - act: (bloc) => bloc.loadDocuments(), + act: (bloc) => bloc.load(), expect: () => [ DocumentsState( isLoaded: true,