From 4d7fab18392d2649ffe6259eeb8e3588cf322e65 Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Mon, 6 Feb 2023 01:04:13 +0100 Subject: [PATCH] Cleaned up code, implemented message queue to notify subscribers of document updates. --- android/app/build.gradle | 10 +- ios/Podfile.lock | 44 ++- ios/Runner.xcodeproj/project.pbxproj | 5 +- ios/Runner/Info.plist | 4 +- .../notifier/document_changed_notifier.dart | 34 +- lib/core/repository/base_repository.dart | 22 +- .../impl/correspondent_repository_impl.dart | 17 +- .../impl/document_type_repository_impl.dart | 17 +- .../impl/saved_view_repository_impl.dart | 10 +- .../impl/storage_path_repository_impl.dart | 16 +- .../repository/impl/tag_repository_impl.dart | 18 +- lib/core/repository/label_repository.dart | 7 +- .../provider/label_repositories_provider.dart | 12 +- .../repository/saved_view_repository.dart | 4 +- .../impl/correspondent_repository_state.dart | 4 +- .../correspondent_repository_state.g.dart | 2 +- .../impl/document_type_repository_state.dart | 11 +- .../document_type_repository_state.g.dart | 2 +- .../impl/saved_view_repository_state.dart | 4 +- .../impl/saved_view_repository_state.g.dart | 2 +- .../impl/storage_path_repository_state.dart | 11 +- .../impl/storage_path_repository_state.g.dart | 2 +- .../state/impl/tag_repository_state.dart | 9 +- .../state/impl/tag_repository_state.g.dart | 2 +- .../state/indexed_repository_state.dart | 16 + .../repository/state/repository_state.dart | 16 - lib/core/service/github_issue_service.dart | 2 +- .../widgets/material/search/m3_search.dart | 6 +- lib/core/widgets/shimmer_placeholder.dart | 21 ++ .../bloc/document_details_cubit.dart | 26 +- .../view/pages/document_details_page.dart | 33 +- .../cubit/document_search_cubit.dart | 14 +- .../view/document_search_page.dart | 10 +- .../cubit/document_upload_cubit.dart | 16 +- .../document_upload_preparation_page.dart | 14 +- .../documents/bloc/documents_cubit.dart | 29 +- .../documents/bloc/documents_state.dart | 7 +- .../view/pages/document_edit_page.dart | 9 +- .../documents/view/pages/documents_page.dart | 33 +- .../view/widgets/adaptive_documents_view.dart | 27 +- .../widgets/document_grid_loading_widget.dart | 102 ++++++ .../documents_list_loading_widget.dart | 93 +++-- .../widgets/items/document_list_item.dart | 2 +- .../document_item_placeholder.dart | 30 ++ .../widgets/placeholder/tags_placeholder.dart | 37 ++ .../widgets/placeholder/text_placeholder.dart | 26 ++ .../view/widgets/sort_documents_button.dart | 8 +- .../cubit/edit_document_cubit.dart | 37 +- .../edit_label/cubit/edit_label_cubit.dart | 6 +- .../edit_label/view/add_label_page.dart | 5 +- .../edit_label/view/edit_label_page.dart | 5 +- .../view/impl/add_correspondent_page.dart | 3 +- .../view/impl/add_document_type_page.dart | 3 +- .../view/impl/add_storage_path_page.dart | 3 +- .../edit_label/view/impl/add_tag_page.dart | 2 +- .../view/impl/edit_correspondent_page.dart | 3 +- .../view/impl/edit_document_type_page.dart | 3 +- .../view/impl/edit_storage_path_page.dart | 3 +- .../edit_label/view/impl/edit_tag_page.dart | 2 +- lib/features/home/view/home_page.dart | 34 +- .../view/widget/verify_identity_page.dart | 14 +- lib/features/inbox/bloc/inbox_cubit.dart | 87 ++--- .../inbox/bloc/state/inbox_state.dart | 4 +- lib/features/inbox/view/pages/inbox_page.dart | 199 +++++------ .../view/widgets/inbox_empty_widget.dart | 2 +- .../inbox/view/widgets/inbox_item.dart | 33 +- lib/features/labels/bloc/label_cubit.dart | 8 +- .../correspondent_bloc_provider.dart | 8 +- .../document_type_bloc_provider.dart | 3 +- .../bloc/providers/labels_bloc_provider.dart | 11 +- .../providers/storage_path_bloc_provider.dart | 3 +- .../bloc/providers/tag_bloc_provider.dart | 2 +- .../tags/view/widgets/tags_form_field.dart | 3 +- .../labels/view/pages/labels_page.dart | 320 ++++++++---------- .../labels/view/widgets/label_item.dart | 3 +- .../labels/view/widgets/label_tab_view.dart | 107 +++--- .../labels/view/widgets/label_text.dart | 7 +- .../bloc/linked_documents_cubit.dart | 19 +- .../view/pages/linked_documents_page.dart | 13 +- .../services/local_notification_service.dart | 3 +- .../paged_documents_mixin.dart | 66 ++-- .../saved_view/cubit/saved_view_cubit.dart | 4 +- .../cubit/saved_view_details_cubit.dart | 12 +- .../saved_view/view/saved_view_list.dart | 1 + .../saved_view/view/saved_view_page.dart | 10 +- lib/features/scan/view/scanner_page.dart | 70 ++-- .../view/dialogs/account_settings_dialog.dart | 23 +- .../cubit/similar_documents_cubit.dart | 17 +- .../view}/similar_documents_view.dart | 36 +- lib/l10n/intl_cs.arb | 6 + lib/l10n/intl_de.arb | 6 + lib/l10n/intl_en.arb | 6 + lib/l10n/intl_pl.arb | 6 + lib/l10n/intl_tr.arb | 6 + lib/main.dart | 14 +- lib/routes/document_details_route.dart | 5 +- .../lib/src/models/document_filter.dart | 15 +- .../lib/src/models/paged_search_result.dart | 8 +- .../absolute_date_range_query.dart | 13 + .../date_range_queries/date_range_query.dart | 2 + .../relative_date_range_query.dart | 20 ++ .../unset_date_range_query.dart | 3 + .../query_parameters/id_query_parameter.dart | 12 +- .../tags_query/any_assigned_tags_query.dart | 5 + .../tags_query/ids_tags_query.dart | 8 +- .../only_not_assigned_tags_query.dart | 5 + .../tags_query/tags_query.dart | 2 + .../models/query_parameters/text_query.dart | 20 ++ packages/paperless_api/pubspec.yaml | 1 + pubspec.lock | 234 +++++++------ pubspec.yaml | 1 - 111 files changed, 1412 insertions(+), 1029 deletions(-) create mode 100644 lib/core/repository/state/indexed_repository_state.dart delete mode 100644 lib/core/repository/state/repository_state.dart create mode 100644 lib/core/widgets/shimmer_placeholder.dart create mode 100644 lib/features/documents/view/widgets/placeholder/document_item_placeholder.dart create mode 100644 lib/features/documents/view/widgets/placeholder/tags_placeholder.dart create mode 100644 lib/features/documents/view/widgets/placeholder/text_placeholder.dart rename lib/features/{document_details/view/pages => similar_documents/view}/similar_documents_view.dart (77%) diff --git a/android/app/build.gradle b/android/app/build.gradle index c7a26c2..43710e2 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -68,11 +68,11 @@ android { storePassword keystoreProperties['storePassword'] } } - buildTypes { - release { - signingConfig signingConfigs.release - } - } + buildTypes { + release { + signingConfig signingConfigs.debug + } + } } diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 842ea55..2c66854 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -35,7 +35,7 @@ PODS: - DKPhotoGallery/Resource (0.0.17): - SDWebImage - SwiftyGif - - edge_detection (1.0.9): + - edge_detection (1.1.1): - Flutter - WeScan - file_picker (0.0.1): @@ -44,6 +44,8 @@ PODS: - Flutter (1.0.0) - flutter_keyboard_visibility (0.0.1): - Flutter + - flutter_local_notifications (0.0.1): + - Flutter - flutter_native_splash (0.0.1): - Flutter - fluttertoast (0.0.2): @@ -56,10 +58,13 @@ PODS: - Flutter - local_auth_ios (0.0.1): - Flutter + - open_filex (0.0.2): + - Flutter - package_info_plus (0.4.5): - Flutter - - path_provider_ios (0.0.1): + - path_provider_foundation (0.0.1): - Flutter + - FlutterMacOS - pdfx (1.0.0): - Flutter - permission_handler_apple (9.0.4): @@ -72,8 +77,9 @@ PODS: - SDWebImage/Core (5.13.5) - share_plus (0.0.1): - Flutter - - shared_preferences_ios (0.0.1): + - shared_preferences_foundation (0.0.1): - Flutter + - FlutterMacOS - sqflite (0.0.2): - Flutter - FMDB (>= 2.7.5) @@ -90,17 +96,19 @@ DEPENDENCIES: - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) + - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`) - local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`) + - open_filex (from `.symlinks/plugins/open_filex/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - pdfx (from `.symlinks/plugins/pdfx/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - - shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite (from `.symlinks/plugins/sqflite/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) @@ -128,6 +136,8 @@ EXTERNAL SOURCES: :path: Flutter flutter_keyboard_visibility: :path: ".symlinks/plugins/flutter_keyboard_visibility/ios" + flutter_local_notifications: + :path: ".symlinks/plugins/flutter_local_notifications/ios" flutter_native_splash: :path: ".symlinks/plugins/flutter_native_splash/ios" fluttertoast: @@ -136,10 +146,12 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/integration_test/ios" local_auth_ios: :path: ".symlinks/plugins/local_auth_ios/ios" + open_filex: + :path: ".symlinks/plugins/open_filex/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" - path_provider_ios: - :path: ".symlinks/plugins/path_provider_ios/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" pdfx: :path: ".symlinks/plugins/pdfx/ios" permission_handler_apple: @@ -148,8 +160,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/receive_sharing_intent/ios" share_plus: :path: ".symlinks/plugins/share_plus/ios" - shared_preferences_ios: - :path: ".symlinks/plugins/shared_preferences_ios/ios" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" sqflite: :path: ".symlinks/plugins/sqflite/ios" url_launcher_ios: @@ -160,28 +172,30 @@ SPEC CHECKSUMS: device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 - edge_detection: 9bc5ee35073b5a17c0b3b679908f01017ce3062a - file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1 + edge_detection: fa02aa120e00d87ada0ca2430b6c6087a501b1e9 + file_picker: ce3938a0df3cc1ef404671531facef740d03f920 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 + flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743 flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef - fluttertoast: 74526702fea2c060ea55dde75895b7e1bde1c86b + fluttertoast: eb263d302cc92e04176c053d2385237e9f43fad0 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5 local_auth_ios: 0d333dde7780f669e66f19d2ff6005f3ea84008d + open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4 package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e - path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 + path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852 pdfx: 7b876b09de8b7a0bf444a4f82b439ffcff4ee1ec permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1 SDWebImage: 23d714cd599354ee7906dbae26dff89b421c4370 share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 - shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad + shared_preferences_foundation: 297b3ebca31b34ec92be11acd7fb0ba932c822ca sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 - url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de + url_launcher_ios: ae1517e5e344f5544fb090b079e11f399dfbe4d2 WeScan: fed582f6c38014d529afb5aa9ffd1bad38fc72b7 PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 664648c..1653b15 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -321,10 +321,12 @@ }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -335,6 +337,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 6cb665f..a837d1a 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -65,5 +65,7 @@ CADisableMinimumFrameDurationOnPhone - + UIApplicationSupportsIndirectInputEvents + + diff --git a/lib/core/notifier/document_changed_notifier.dart b/lib/core/notifier/document_changed_notifier.dart index 6597d6f..c53dedc 100644 --- a/lib/core/notifier/document_changed_notifier.dart +++ b/lib/core/notifier/document_changed_notifier.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'dart:developer'; +import 'package:flutter/foundation.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:rxdart/subjects.dart'; @@ -9,26 +11,40 @@ class DocumentChangedNotifier { final Subject _updated = PublishSubject(); final Subject _deleted = PublishSubject(); + final Map> _subscribers = {}; + void notifyUpdated(DocumentModel updated) { + debugPrint("Notifying updated document ${updated.id}"); _updated.add(updated); } void notifyDeleted(DocumentModel deleted) { + debugPrint("Notifying deleted document ${deleted.id}"); _deleted.add(deleted); } - List listen({ + void subscribe( + dynamic subscriber, { DocumentChangedCallback? onUpdated, DocumentChangedCallback? onDeleted, }) { - return [ - _updated.listen((value) { - onUpdated?.call(value); - }), - _updated.listen((value) { - onDeleted?.call(value); - }), - ]; + _subscribers.putIfAbsent( + subscriber, + () => [ + _updated.listen((value) { + onUpdated?.call(value); + }), + _deleted.listen((value) { + onDeleted?.call(value); + }), + ], + ); + } + + void unsubscribe(dynamic subscriber) { + _subscribers[subscriber]?.forEach((element) { + element.cancel(); + }); } void close() { diff --git a/lib/core/repository/base_repository.dart b/lib/core/repository/base_repository.dart index f0c4589..1eb57c4 100644 --- a/lib/core/repository/base_repository.dart +++ b/lib/core/repository/base_repository.dart @@ -1,30 +1,30 @@ import 'package:hydrated_bloc/hydrated_bloc.dart'; -import 'package:paperless_mobile/core/repository/state/repository_state.dart'; +import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart'; import 'package:rxdart/subjects.dart'; /// /// Base repository class which all repositories should implement /// -abstract class BaseRepository - extends Cubit with HydratedMixin { - final State _initialState; +abstract class BaseRepository extends Cubit> + with HydratedMixin { + final IndexedRepositoryState _initialState; BaseRepository(this._initialState) : super(_initialState) { hydrate(); } - Stream get values => + Stream?> get values => BehaviorSubject.seeded(state)..addStream(super.stream); - State? get current => state; + IndexedRepositoryState? get current => state; bool get isInitialized => state.hasLoaded; - Future create(Type object); - Future find(int id); - Future> findAll([Iterable? ids]); - Future update(Type object); - Future delete(Type object); + Future create(T object); + Future find(int id); + Future> findAll([Iterable? ids]); + Future update(T object); + Future delete(T object); @override Future clear() async { diff --git a/lib/core/repository/impl/correspondent_repository_impl.dart b/lib/core/repository/impl/correspondent_repository_impl.dart index 4b676ac..7227c58 100644 --- a/lib/core/repository/impl/correspondent_repository_impl.dart +++ b/lib/core/repository/impl/correspondent_repository_impl.dart @@ -3,10 +3,8 @@ import 'dart:async'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart'; -import 'package:paperless_mobile/core/repository/state/repository_state.dart'; -class CorrespondentRepositoryImpl - extends LabelRepository { +class CorrespondentRepositoryImpl extends LabelRepository { final PaperlessLabelsApi _api; CorrespondentRepositoryImpl(this._api) @@ -15,7 +13,7 @@ class CorrespondentRepositoryImpl @override Future create(Correspondent correspondent) async { final created = await _api.saveCorrespondent(correspondent); - final updatedState = {...state.values} + final updatedState = {...state.values ?? {}} ..putIfAbsent(created.id!, () => created); emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true)); return created; @@ -24,7 +22,7 @@ class CorrespondentRepositoryImpl @override Future delete(Correspondent correspondent) async { await _api.deleteCorrespondent(correspondent); - final updatedState = {...state.values} + final updatedState = {...state.values ?? {}} ..removeWhere((k, v) => k == correspondent.id); emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true)); return correspondent.id!; @@ -34,7 +32,7 @@ class CorrespondentRepositoryImpl Future find(int id) async { final correspondent = await _api.getCorrespondent(id); if (correspondent != null) { - final updatedState = {...state.values}..[id] = correspondent; + final updatedState = {...state.values ?? {}}..[id] = correspondent; emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true)); return correspondent; } @@ -44,7 +42,7 @@ class CorrespondentRepositoryImpl @override Future> findAll([Iterable? ids]) async { final correspondents = await _api.getCorrespondents(ids); - final updatedState = {...state.values} + final updatedState = {...state.values ?? {}} ..addEntries(correspondents.map((e) => MapEntry(e.id!, e))); emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true)); return correspondents; @@ -53,7 +51,8 @@ class CorrespondentRepositoryImpl @override Future update(Correspondent correspondent) async { final updated = await _api.updateCorrespondent(correspondent); - final updatedState = {...state.values}..update(updated.id!, (_) => updated); + final updatedState = {...state.values ?? {}} + ..update(updated.id!, (_) => updated); emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true)); return updated; } @@ -64,7 +63,7 @@ class CorrespondentRepositoryImpl } @override - Map toJson(CorrespondentRepositoryState state) { + Map toJson(covariant CorrespondentRepositoryState state) { return state.toJson(); } } diff --git a/lib/core/repository/impl/document_type_repository_impl.dart b/lib/core/repository/impl/document_type_repository_impl.dart index 1e1ae92..5fd7a87 100644 --- a/lib/core/repository/impl/document_type_repository_impl.dart +++ b/lib/core/repository/impl/document_type_repository_impl.dart @@ -1,10 +1,8 @@ import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart'; -import 'package:rxdart/rxdart.dart' show BehaviorSubject; -class DocumentTypeRepositoryImpl - extends LabelRepository { +class DocumentTypeRepositoryImpl extends LabelRepository { final PaperlessLabelsApi _api; DocumentTypeRepositoryImpl(this._api) @@ -13,7 +11,7 @@ class DocumentTypeRepositoryImpl @override Future create(DocumentType documentType) async { final created = await _api.saveDocumentType(documentType); - final updatedState = {...state.values} + final updatedState = {...state.values ?? {}} ..putIfAbsent(created.id!, () => created); emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true)); return created; @@ -22,7 +20,7 @@ class DocumentTypeRepositoryImpl @override Future delete(DocumentType documentType) async { await _api.deleteDocumentType(documentType); - final updatedState = {...state.values} + final updatedState = {...state.values ?? {}} ..removeWhere((k, v) => k == documentType.id); emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true)); return documentType.id!; @@ -32,7 +30,7 @@ class DocumentTypeRepositoryImpl Future find(int id) async { final documentType = await _api.getDocumentType(id); if (documentType != null) { - final updatedState = {...state.values}..[id] = documentType; + final updatedState = {...state.values ?? {}}..[id] = documentType; emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true)); return documentType; } @@ -42,7 +40,7 @@ class DocumentTypeRepositoryImpl @override Future> findAll([Iterable? ids]) async { final documentTypes = await _api.getDocumentTypes(ids); - final updatedState = {...state.values} + final updatedState = {...state.values ?? {}} ..addEntries(documentTypes.map((e) => MapEntry(e.id!, e))); emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true)); return documentTypes; @@ -51,7 +49,8 @@ class DocumentTypeRepositoryImpl @override Future update(DocumentType documentType) async { final updated = await _api.updateDocumentType(documentType); - final updatedState = {...state.values}..update(updated.id!, (_) => updated); + final updatedState = {...state.values ?? {}} + ..update(updated.id!, (_) => updated); emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true)); return updated; } @@ -62,7 +61,7 @@ class DocumentTypeRepositoryImpl } @override - Map toJson(DocumentTypeRepositoryState state) { + Map toJson(covariant DocumentTypeRepositoryState state) { return state.toJson(); } } diff --git a/lib/core/repository/impl/saved_view_repository_impl.dart b/lib/core/repository/impl/saved_view_repository_impl.dart index b5e03aa..18eceed 100644 --- a/lib/core/repository/impl/saved_view_repository_impl.dart +++ b/lib/core/repository/impl/saved_view_repository_impl.dart @@ -10,7 +10,7 @@ class SavedViewRepositoryImpl extends SavedViewRepository { @override Future create(SavedView object) async { final created = await _api.save(object); - final updatedState = {...state.values} + final updatedState = {...state.values ?? {}} ..putIfAbsent(created.id!, () => created); emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true)); return created; @@ -19,7 +19,7 @@ class SavedViewRepositoryImpl extends SavedViewRepository { @override Future delete(SavedView view) async { await _api.delete(view); - final updatedState = {...state.values}..remove(view.id); + final updatedState = {...state.values ?? {}}..remove(view.id); emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true)); return view.id!; } @@ -27,7 +27,7 @@ class SavedViewRepositoryImpl extends SavedViewRepository { @override Future find(int id) async { final found = await _api.find(id); - final updatedState = {...state.values} + final updatedState = {...state.values ?? {}} ..update(id, (_) => found, ifAbsent: () => found); emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true)); return found; @@ -37,7 +37,7 @@ class SavedViewRepositoryImpl extends SavedViewRepository { Future> findAll([Iterable? ids]) async { final found = await _api.findAll(ids); final updatedState = { - ...state.values, + ...state.values ?? {}, ...{for (final view in found) view.id!: view}, }; emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true)); @@ -56,7 +56,7 @@ class SavedViewRepositoryImpl extends SavedViewRepository { } @override - Map toJson(SavedViewRepositoryState state) { + Map toJson(covariant SavedViewRepositoryState state) { return state.toJson(); } } diff --git a/lib/core/repository/impl/storage_path_repository_impl.dart b/lib/core/repository/impl/storage_path_repository_impl.dart index b738827..1fb54ea 100644 --- a/lib/core/repository/impl/storage_path_repository_impl.dart +++ b/lib/core/repository/impl/storage_path_repository_impl.dart @@ -3,8 +3,7 @@ import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart'; import 'package:rxdart/rxdart.dart' show BehaviorSubject; -class StoragePathRepositoryImpl - extends LabelRepository { +class StoragePathRepositoryImpl extends LabelRepository { final PaperlessLabelsApi _api; StoragePathRepositoryImpl(this._api) @@ -13,7 +12,7 @@ class StoragePathRepositoryImpl @override Future create(StoragePath storagePath) async { final created = await _api.saveStoragePath(storagePath); - final updatedState = {...state.values} + final updatedState = {...state.values ?? {}} ..putIfAbsent(created.id!, () => created); emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true)); return created; @@ -22,7 +21,7 @@ class StoragePathRepositoryImpl @override Future delete(StoragePath storagePath) async { await _api.deleteStoragePath(storagePath); - final updatedState = {...state.values} + final updatedState = {...state.values ?? {}} ..removeWhere((k, v) => k == storagePath.id); emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true)); return storagePath.id!; @@ -32,7 +31,7 @@ class StoragePathRepositoryImpl Future find(int id) async { final storagePath = await _api.getStoragePath(id); if (storagePath != null) { - final updatedState = {...state.values}..[id] = storagePath; + final updatedState = {...state.values ?? {}}..[id] = storagePath; emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true)); return storagePath; } @@ -42,7 +41,7 @@ class StoragePathRepositoryImpl @override Future> findAll([Iterable? ids]) async { final storagePaths = await _api.getStoragePaths(ids); - final updatedState = {...state.values} + final updatedState = {...state.values ?? {}} ..addEntries(storagePaths.map((e) => MapEntry(e.id!, e))); emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true)); return storagePaths; @@ -51,7 +50,8 @@ class StoragePathRepositoryImpl @override Future update(StoragePath storagePath) async { final updated = await _api.updateStoragePath(storagePath); - final updatedState = {...state.values}..update(updated.id!, (_) => updated); + final updatedState = {...state.values ?? {}} + ..update(updated.id!, (_) => updated); emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true)); return updated; } @@ -62,7 +62,7 @@ class StoragePathRepositoryImpl } @override - Map toJson(StoragePathRepositoryState state) { + Map toJson(covariant StoragePathRepositoryState state) { return state.toJson(); } } diff --git a/lib/core/repository/impl/tag_repository_impl.dart b/lib/core/repository/impl/tag_repository_impl.dart index 09f6061..a39a77b 100644 --- a/lib/core/repository/impl/tag_repository_impl.dart +++ b/lib/core/repository/impl/tag_repository_impl.dart @@ -1,10 +1,8 @@ import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; -import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart'; import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart'; -import 'package:paperless_mobile/core/repository/state/repository_state.dart'; -class TagRepositoryImpl extends LabelRepository { +class TagRepositoryImpl extends LabelRepository { final PaperlessLabelsApi _api; TagRepositoryImpl(this._api) : super(const TagRepositoryState()); @@ -12,7 +10,7 @@ class TagRepositoryImpl extends LabelRepository { @override Future create(Tag object) async { final created = await _api.saveTag(object); - final updatedState = {...state.values} + final updatedState = {...state.values ?? {}} ..putIfAbsent(created.id!, () => created); emit(TagRepositoryState(values: updatedState, hasLoaded: true)); return created; @@ -21,7 +19,8 @@ class TagRepositoryImpl extends LabelRepository { @override Future delete(Tag tag) async { await _api.deleteTag(tag); - final updatedState = {...state.values}..removeWhere((k, v) => k == tag.id); + final updatedState = {...state.values ?? {}} + ..removeWhere((k, v) => k == tag.id); emit(TagRepositoryState(values: updatedState, hasLoaded: true)); return tag.id!; } @@ -30,7 +29,7 @@ class TagRepositoryImpl extends LabelRepository { Future find(int id) async { final tag = await _api.getTag(id); if (tag != null) { - final updatedState = {...state.values}..[id] = tag; + final updatedState = {...state.values ?? {}}..[id] = tag; emit(TagRepositoryState(values: updatedState, hasLoaded: true)); return tag; } @@ -40,7 +39,7 @@ class TagRepositoryImpl extends LabelRepository { @override Future> findAll([Iterable? ids]) async { final tags = await _api.getTags(ids); - final updatedState = {...state.values} + final updatedState = {...state.values ?? {}} ..addEntries(tags.map((e) => MapEntry(e.id!, e))); emit(TagRepositoryState(values: updatedState, hasLoaded: true)); return tags; @@ -49,7 +48,8 @@ class TagRepositoryImpl extends LabelRepository { @override Future update(Tag tag) async { final updated = await _api.updateTag(tag); - final updatedState = {...state.values}..update(updated.id!, (_) => updated); + final updatedState = {...state.values ?? {}} + ..update(updated.id!, (_) => updated); emit(TagRepositoryState(values: updatedState, hasLoaded: true)); return updated; } @@ -60,7 +60,7 @@ class TagRepositoryImpl extends LabelRepository { } @override - Map? toJson(TagRepositoryState state) { + Map? toJson(covariant TagRepositoryState state) { return state.toJson(); } } diff --git a/lib/core/repository/label_repository.dart b/lib/core/repository/label_repository.dart index c2aa3bc..8fe3458 100644 --- a/lib/core/repository/label_repository.dart +++ b/lib/core/repository/label_repository.dart @@ -1,8 +1,7 @@ import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/base_repository.dart'; -import 'package:paperless_mobile/core/repository/state/repository_state.dart'; +import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart'; -abstract class LabelRepository - extends BaseRepository { - LabelRepository(State initial) : super(initial); +abstract class LabelRepository extends BaseRepository { + LabelRepository(IndexedRepositoryState initial) : super(initial); } diff --git a/lib/core/repository/provider/label_repositories_provider.dart b/lib/core/repository/provider/label_repositories_provider.dart index d45c792..e9634be 100644 --- a/lib/core/repository/provider/label_repositories_provider.dart +++ b/lib/core/repository/provider/label_repositories_provider.dart @@ -17,20 +17,16 @@ class LabelRepositoriesProvider extends StatelessWidget { return MultiRepositoryProvider( providers: [ RepositoryProvider( - create: (context) => context.read< - LabelRepository>(), + create: (context) => context.read>(), ), RepositoryProvider( - create: (context) => context.read< - LabelRepository>(), + create: (context) => context.read>(), ), RepositoryProvider( - create: (context) => context - .read>(), + create: (context) => context.read>(), ), RepositoryProvider( - create: (context) => - context.read>(), + create: (context) => context.read>(), ), ], child: child, diff --git a/lib/core/repository/saved_view_repository.dart b/lib/core/repository/saved_view_repository.dart index 644f367..bb1c4e3 100644 --- a/lib/core/repository/saved_view_repository.dart +++ b/lib/core/repository/saved_view_repository.dart @@ -1,8 +1,8 @@ import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/base_repository.dart'; import 'package:paperless_mobile/core/repository/state/impl/saved_view_repository_state.dart'; +import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart'; -abstract class SavedViewRepository - extends BaseRepository { +abstract class SavedViewRepository extends BaseRepository { SavedViewRepository(super.initialState); } diff --git a/lib/core/repository/state/impl/correspondent_repository_state.dart b/lib/core/repository/state/impl/correspondent_repository_state.dart index 5fb88ee..fce9efb 100644 --- a/lib/core/repository/state/impl/correspondent_repository_state.dart +++ b/lib/core/repository/state/impl/correspondent_repository_state.dart @@ -1,13 +1,13 @@ import 'package:flutter/foundation.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/repository/state/repository_state.dart'; +import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart'; part 'correspondent_repository_state.g.dart'; @JsonSerializable() class CorrespondentRepositoryState - extends RepositoryState> { + extends IndexedRepositoryState { const CorrespondentRepositoryState({ super.values = const {}, super.hasLoaded, diff --git a/lib/core/repository/state/impl/correspondent_repository_state.g.dart b/lib/core/repository/state/impl/correspondent_repository_state.g.dart index 08e2976..405f4ff 100644 --- a/lib/core/repository/state/impl/correspondent_repository_state.g.dart +++ b/lib/core/repository/state/impl/correspondent_repository_state.g.dart @@ -20,6 +20,6 @@ CorrespondentRepositoryState _$CorrespondentRepositoryStateFromJson( Map _$CorrespondentRepositoryStateToJson( CorrespondentRepositoryState instance) => { - 'values': instance.values.map((k, e) => MapEntry(k.toString(), e)), + 'values': instance.values?.map((k, e) => MapEntry(k.toString(), e)), 'hasLoaded': instance.hasLoaded, }; diff --git a/lib/core/repository/state/impl/document_type_repository_state.dart b/lib/core/repository/state/impl/document_type_repository_state.dart index 7ce5188..4a4ab1f 100644 --- a/lib/core/repository/state/impl/document_type_repository_state.dart +++ b/lib/core/repository/state/impl/document_type_repository_state.dart @@ -1,20 +1,21 @@ import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/repository/state/repository_state.dart'; +import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart'; import 'package:json_annotation/json_annotation.dart'; part 'document_type_repository_state.g.dart'; @JsonSerializable() -class DocumentTypeRepositoryState - extends RepositoryState> { +class DocumentTypeRepositoryState extends IndexedRepositoryState { const DocumentTypeRepositoryState({ super.values = const {}, super.hasLoaded, }); @override - DocumentTypeRepositoryState copyWith( - {Map? values, bool? hasLoaded}) { + DocumentTypeRepositoryState copyWith({ + Map? values, + bool? hasLoaded, + }) { return DocumentTypeRepositoryState( values: values ?? this.values, hasLoaded: hasLoaded ?? this.hasLoaded, diff --git a/lib/core/repository/state/impl/document_type_repository_state.g.dart b/lib/core/repository/state/impl/document_type_repository_state.g.dart index 6868bd6..3528b96 100644 --- a/lib/core/repository/state/impl/document_type_repository_state.g.dart +++ b/lib/core/repository/state/impl/document_type_repository_state.g.dart @@ -20,6 +20,6 @@ DocumentTypeRepositoryState _$DocumentTypeRepositoryStateFromJson( Map _$DocumentTypeRepositoryStateToJson( DocumentTypeRepositoryState instance) => { - 'values': instance.values.map((k, e) => MapEntry(k.toString(), e)), + 'values': instance.values?.map((k, e) => MapEntry(k.toString(), e)), 'hasLoaded': instance.hasLoaded, }; diff --git a/lib/core/repository/state/impl/saved_view_repository_state.dart b/lib/core/repository/state/impl/saved_view_repository_state.dart index ecd9e49..9cd7672 100644 --- a/lib/core/repository/state/impl/saved_view_repository_state.dart +++ b/lib/core/repository/state/impl/saved_view_repository_state.dart @@ -1,11 +1,11 @@ import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/repository/state/repository_state.dart'; +import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart'; import 'package:json_annotation/json_annotation.dart'; part 'saved_view_repository_state.g.dart'; @JsonSerializable() -class SavedViewRepositoryState extends RepositoryState> { +class SavedViewRepositoryState extends IndexedRepositoryState { const SavedViewRepositoryState({ super.values = const {}, super.hasLoaded = false, diff --git a/lib/core/repository/state/impl/saved_view_repository_state.g.dart b/lib/core/repository/state/impl/saved_view_repository_state.g.dart index 4cc61b9..bfcc949 100644 --- a/lib/core/repository/state/impl/saved_view_repository_state.g.dart +++ b/lib/core/repository/state/impl/saved_view_repository_state.g.dart @@ -20,6 +20,6 @@ SavedViewRepositoryState _$SavedViewRepositoryStateFromJson( Map _$SavedViewRepositoryStateToJson( SavedViewRepositoryState instance) => { - 'values': instance.values.map((k, e) => MapEntry(k.toString(), e)), + 'values': instance.values?.map((k, e) => MapEntry(k.toString(), e)), 'hasLoaded': instance.hasLoaded, }; diff --git a/lib/core/repository/state/impl/storage_path_repository_state.dart b/lib/core/repository/state/impl/storage_path_repository_state.dart index 366db8e..b9ed856 100644 --- a/lib/core/repository/state/impl/storage_path_repository_state.dart +++ b/lib/core/repository/state/impl/storage_path_repository_state.dart @@ -1,20 +1,21 @@ import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/repository/state/repository_state.dart'; +import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart'; import 'package:json_annotation/json_annotation.dart'; part 'storage_path_repository_state.g.dart'; @JsonSerializable() -class StoragePathRepositoryState - extends RepositoryState> { +class StoragePathRepositoryState extends IndexedRepositoryState { const StoragePathRepositoryState({ super.values = const {}, super.hasLoaded = false, }); @override - StoragePathRepositoryState copyWith( - {Map? values, bool? hasLoaded}) { + StoragePathRepositoryState copyWith({ + Map? values, + bool? hasLoaded, + }) { return StoragePathRepositoryState( values: values ?? this.values, hasLoaded: hasLoaded ?? this.hasLoaded, diff --git a/lib/core/repository/state/impl/storage_path_repository_state.g.dart b/lib/core/repository/state/impl/storage_path_repository_state.g.dart index 4be8ad5..75ac365 100644 --- a/lib/core/repository/state/impl/storage_path_repository_state.g.dart +++ b/lib/core/repository/state/impl/storage_path_repository_state.g.dart @@ -20,6 +20,6 @@ StoragePathRepositoryState _$StoragePathRepositoryStateFromJson( Map _$StoragePathRepositoryStateToJson( StoragePathRepositoryState instance) => { - 'values': instance.values.map((k, e) => MapEntry(k.toString(), e)), + 'values': instance.values?.map((k, e) => MapEntry(k.toString(), e)), 'hasLoaded': instance.hasLoaded, }; diff --git a/lib/core/repository/state/impl/tag_repository_state.dart b/lib/core/repository/state/impl/tag_repository_state.dart index 6e0e261..4558bfe 100644 --- a/lib/core/repository/state/impl/tag_repository_state.dart +++ b/lib/core/repository/state/impl/tag_repository_state.dart @@ -1,18 +1,21 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/repository/state/repository_state.dart'; +import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart'; part 'tag_repository_state.g.dart'; @JsonSerializable() -class TagRepositoryState extends RepositoryState> { +class TagRepositoryState extends IndexedRepositoryState { const TagRepositoryState({ super.values = const {}, super.hasLoaded = false, }); @override - TagRepositoryState copyWith({Map? values, bool? hasLoaded}) { + TagRepositoryState copyWith({ + Map? values, + bool? hasLoaded, + }) { return TagRepositoryState( values: values ?? this.values, hasLoaded: hasLoaded ?? this.hasLoaded, diff --git a/lib/core/repository/state/impl/tag_repository_state.g.dart b/lib/core/repository/state/impl/tag_repository_state.g.dart index 02e8bd0..09e04ee 100644 --- a/lib/core/repository/state/impl/tag_repository_state.g.dart +++ b/lib/core/repository/state/impl/tag_repository_state.g.dart @@ -18,6 +18,6 @@ TagRepositoryState _$TagRepositoryStateFromJson(Map json) => Map _$TagRepositoryStateToJson(TagRepositoryState instance) => { - 'values': instance.values.map((k, e) => MapEntry(k.toString(), e)), + 'values': instance.values?.map((k, e) => MapEntry(k.toString(), e)), 'hasLoaded': instance.hasLoaded, }; diff --git a/lib/core/repository/state/indexed_repository_state.dart b/lib/core/repository/state/indexed_repository_state.dart new file mode 100644 index 0000000..d3caee5 --- /dev/null +++ b/lib/core/repository/state/indexed_repository_state.dart @@ -0,0 +1,16 @@ +abstract class IndexedRepositoryState { + final Map? values; + final bool hasLoaded; + + const IndexedRepositoryState({ + required this.values, + this.hasLoaded = false, + }) : assert(!(values == null) || !hasLoaded); + + IndexedRepositoryState.loaded(this.values) : hasLoaded = true; + + IndexedRepositoryState copyWith({ + Map? values, + bool? hasLoaded, + }); +} diff --git a/lib/core/repository/state/repository_state.dart b/lib/core/repository/state/repository_state.dart deleted file mode 100644 index 7498a33..0000000 --- a/lib/core/repository/state/repository_state.dart +++ /dev/null @@ -1,16 +0,0 @@ -abstract class RepositoryState { - final T values; - final bool hasLoaded; - - const RepositoryState({ - required this.values, - this.hasLoaded = false, - }); - - RepositoryState.loaded(this.values) : hasLoaded = true; - - RepositoryState copyWith({ - T? values, - bool? hasLoaded, - }); -} diff --git a/lib/core/service/github_issue_service.dart b/lib/core/service/github_issue_service.dart index 38568b9..b4e1d79 100644 --- a/lib/core/service/github_issue_service.dart +++ b/lib/core/service/github_issue_service.dart @@ -27,7 +27,7 @@ class GithubIssueService { ..tryPutIfAbsent('assignees', () => assignees?.join(',')) ..tryPutIfAbsent('project', () => project), ); - log("[GitHubIssueService] Creating GitHub issue: " + uri.toString()); + debugPrint("[GitHubIssueService] Creating GitHub issue: " + uri.toString()); launchUrl( uri, mode: LaunchMode.externalApplication, diff --git a/lib/core/widgets/material/search/m3_search.dart b/lib/core/widgets/material/search/m3_search.dart index 43572dc..dc01985 100644 --- a/lib/core/widgets/material/search/m3_search.dart +++ b/lib/core/widgets/material/search/m3_search.dart @@ -4,6 +4,7 @@ // found in the LICENSE file. import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; /// Shows a full screen search page and returns the search result selected by /// the user when the page is closed. @@ -221,12 +222,13 @@ abstract class SearchDelegate { final ColorScheme colorScheme = theme.colorScheme; return theme.copyWith( appBarTheme: AppBarTheme( - brightness: colorScheme.brightness, + systemOverlayStyle: colorScheme.brightness == Brightness.light + ? SystemUiOverlayStyle.light + : SystemUiOverlayStyle.dark, backgroundColor: colorScheme.brightness == Brightness.dark ? Colors.grey[900] : Colors.white, iconTheme: theme.primaryIconTheme.copyWith(color: Colors.grey), - textTheme: theme.textTheme, ), inputDecorationTheme: searchFieldDecorationTheme ?? InputDecorationTheme( diff --git a/lib/core/widgets/shimmer_placeholder.dart b/lib/core/widgets/shimmer_placeholder.dart new file mode 100644 index 0000000..d3b41be --- /dev/null +++ b/lib/core/widgets/shimmer_placeholder.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; + +class ShimmerPlaceholder extends StatelessWidget { + final Widget child; + + const ShimmerPlaceholder({super.key, required this.child}); + + @override + Widget build(BuildContext context) { + return Shimmer.fromColors( + baseColor: Theme.of(context).brightness == Brightness.light + ? Colors.grey[300]! + : Colors.grey[900]!, + highlightColor: Theme.of(context).brightness == Brightness.light + ? Colors.grey[100]! + : Colors.grey[600]!, + child: child, + ); + } +} diff --git a/lib/features/document_details/bloc/document_details_cubit.dart b/lib/features/document_details/bloc/document_details_cubit.dart index 68772c8..eaed8d2 100644 --- a/lib/features/document_details/bloc/document_details_cubit.dart +++ b/lib/features/document_details/bloc/document_details_cubit.dart @@ -1,23 +1,32 @@ +import 'dart:async'; import 'dart:io'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:open_filex/open_filex.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/service/file_service.dart'; part 'document_details_state.dart'; class DocumentDetailsCubit extends Cubit { final PaperlessDocumentsApi _api; + final DocumentChangedNotifier _notifier; - DocumentDetailsCubit(this._api, DocumentModel initialDocument) - : super(DocumentDetailsState(document: initialDocument)) { + final List _subscriptions = []; + DocumentDetailsCubit( + this._api, + this._notifier, { + required DocumentModel initialDocument, + }) : super(DocumentDetailsState(document: initialDocument)) { + _notifier.subscribe(this, onUpdated: replace); loadSuggestions(); } Future delete(DocumentModel document) async { await _api.delete(document); + _notifier.notifyDeleted(document); } Future loadSuggestions() async { @@ -41,7 +50,7 @@ class DocumentDetailsCubit extends Cubit { final int asn = await _api.findNextAsn(); final updatedDocument = await _api.update(document.copyWith(archiveSerialNumber: asn)); - emit(state.copyWith(document: updatedDocument)); + _notifier.notifyUpdated(updatedDocument); } } @@ -60,7 +69,16 @@ class DocumentDetailsCubit extends Cubit { ); } - void replaceDocument(DocumentModel document) { + void replace(DocumentModel document) { emit(state.copyWith(document: document)); } + + @override + Future close() { + for (final element in _subscriptions) { + element.cancel(); + } + _notifier.unsubscribe(this); + return super.close(); + } } diff --git a/lib/features/document_details/view/pages/document_details_page.dart b/lib/features/document_details/view/pages/document_details_page.dart index 203ac30..648d70a 100644 --- a/lib/features/document_details/view/pages/document_details_page.dart +++ b/lib/features/document_details/view/pages/document_details_page.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'dart:math'; +import 'package:badges/badges.dart' as b; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -8,13 +8,11 @@ import 'package:intl/intl.dart'; import 'package:open_filex/open_filex.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; -import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart'; import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart'; import 'package:paperless_mobile/core/widgets/highlighted_text.dart'; import 'package:paperless_mobile/core/widgets/offline_widget.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; -import 'package:paperless_mobile/features/document_details/view/pages/similar_documents_view.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/document_download_button.dart'; import 'package:paperless_mobile/features/documents/view/pages/document_edit_page.dart'; import 'package:paperless_mobile/features/documents/view/pages/document_view.dart'; @@ -30,9 +28,7 @@ import 'package:paperless_mobile/helpers/format_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; -import 'package:badges/badges.dart' as b; - -import '../../../../core/repository/state/impl/document_type_repository_state.dart'; +import 'package:paperless_mobile/features/similar_documents/view/similar_documents_view.dart'; //TODO: Refactor this into several widgets class DocumentDetailsPage extends StatefulWidget { @@ -79,16 +75,7 @@ class _DocumentDetailsPageState extends State { 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... - ), - onPressed: () => Navigator.of(context).pop( - context.read().state.document, - ), - ), + leading: const BackButton(), floating: true, pinned: true, expandedHeight: 200.0, @@ -153,6 +140,7 @@ class _DocumentDetailsPageState extends State { builder: (context, state) { return BlocProvider( create: (context) => SimilarDocumentsCubit( + context.read(), context.read(), documentId: state.document.id, ), @@ -168,7 +156,7 @@ class _DocumentDetailsPageState extends State { _buildDocumentMetaDataView( state.document, ), - _buildSimilarDocumentsView(), + const SimilarDocumentsView(), ], ), ).paddedSymmetrically(horizontal: 8); @@ -284,6 +272,7 @@ class _DocumentDetailsPageState extends State { documentTypeRepository: context.read(), storagePathRepository: context.read(), tagRepository: context.read(), + notifier: context.read(), ), ), BlocProvider.value( @@ -294,7 +283,7 @@ class _DocumentDetailsPageState extends State { listenWhen: (previous, current) => previous.document != current.document, listener: (context, state) { - cubit.replaceDocument(state.document); + cubit.replace(state.document); }, child: BlocBuilder( builder: (context, state) { @@ -461,7 +450,7 @@ class _DocumentDetailsPageState extends State { visible: document.documentType != null, child: _DetailsItem( label: S.of(context).documentDocumentTypePropertyLabel, - content: LabelText( + content: LabelText( style: Theme.of(context).textTheme.bodyLarge, id: document.documentType, ), @@ -471,7 +460,7 @@ class _DocumentDetailsPageState extends State { visible: document.correspondent != null, child: _DetailsItem( label: S.of(context).documentCorrespondentPropertyLabel, - content: LabelText( + content: LabelText( style: Theme.of(context).textTheme.bodyLarge, id: document.correspondent, ), @@ -555,10 +544,6 @@ class _DocumentDetailsPageState extends State { ), ); } - - Widget _buildSimilarDocumentsView() { - return const SimilarDocumentsView(); - } } class _DetailsItem extends StatelessWidget { diff --git a/lib/features/document_search/cubit/document_search_cubit.dart b/lib/features/document_search/cubit/document_search_cubit.dart index 79300f2..616d46f 100644 --- a/lib/features/document_search/cubit/document_search_cubit.dart +++ b/lib/features/document_search/cubit/document_search_cubit.dart @@ -13,7 +13,13 @@ class DocumentSearchCubit extends HydratedCubit final DocumentChangedNotifier notifier; DocumentSearchCubit(this.api, this.notifier) - : super(const DocumentSearchState()); + : super(const DocumentSearchState()) { + notifier.subscribe( + this, + onDeleted: remove, + onUpdated: replace, + ); + } Future search(String query) async { emit(state.copyWith( @@ -61,6 +67,12 @@ class DocumentSearchCubit extends HydratedCubit )); } + @override + Future close() { + notifier.unsubscribe(this); + return super.close(); + } + @override DocumentSearchState? fromJson(Map json) { return DocumentSearchState.fromJson(json); diff --git a/lib/features/document_search/view/document_search_page.dart b/lib/features/document_search/view/document_search_page.dart index 2f0cf21..ed2ee3b 100644 --- a/lib/features/document_search/view/document_search_page.dart +++ b/lib/features/document_search/view/document_search_page.dart @@ -158,18 +158,16 @@ class _DocumentSearchPageState extends State { isLabelClickable: false, isLoading: state.isLoading, hasLoaded: state.hasLoaded, - onTap: (document) async { - final updatedDocument = await Navigator.pushNamed( + enableHeroAnimation: false, + onTap: (document) { + Navigator.pushNamed( context, DocumentDetailsRoute.routeName, arguments: DocumentDetailsRouteArguments( document: document, isLabelClickable: false, ), - ) as DocumentModel?; - if (updatedDocument != document) { - context.read().reload(); - } + ); }, ) ], diff --git a/lib/features/document_upload/cubit/document_upload_cubit.dart b/lib/features/document_upload/cubit/document_upload_cubit.dart index dec42e2..3ec4326 100644 --- a/lib/features/document_upload/cubit/document_upload_cubit.dart +++ b/lib/features/document_upload/cubit/document_upload_cubit.dart @@ -14,21 +14,17 @@ part 'document_upload_state.dart'; class DocumentUploadCubit extends Cubit { final PaperlessDocumentsApi _documentApi; - final LabelRepository _tagRepository; - final LabelRepository - _correspondentRepository; - final LabelRepository - _documentTypeRepository; + final LabelRepository _tagRepository; + final LabelRepository _correspondentRepository; + final LabelRepository _documentTypeRepository; final List _subs = []; DocumentUploadCubit({ required PaperlessDocumentsApi documentApi, - required LabelRepository tagRepository, - required LabelRepository - correspondentRepository, - required LabelRepository - documentTypeRepository, + required LabelRepository tagRepository, + required LabelRepository correspondentRepository, + required LabelRepository documentTypeRepository, }) : _documentApi = documentApi, _tagRepository = tagRepository, _correspondentRepository = correspondentRepository, diff --git a/lib/features/document_upload/view/document_upload_preparation_page.dart b/lib/features/document_upload/view/document_upload_preparation_page.dart index dc91d9e..49dca21 100644 --- a/lib/features/document_upload/view/document_upload_preparation_page.dart +++ b/lib/features/document_upload/view/document_upload_preparation_page.dart @@ -8,10 +8,7 @@ import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/intl.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; -import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart'; -import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart'; import 'package:paperless_mobile/core/type/types.dart'; -import 'package:paperless_mobile/core/widgets/hint_card.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart'; import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart'; @@ -20,7 +17,6 @@ import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_fie import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; -import 'package:paperless_mobile/constants.dart'; class DocumentUploadPreparationPage extends StatefulWidget { final Uint8List fileBytes; @@ -173,9 +169,8 @@ class _DocumentUploadPreparationPageState formBuilderState: _formKey.currentState, labelCreationWidgetBuilder: (initialName) => RepositoryProvider( - create: (context) => context.read< - LabelRepository>(), + create: (context) => + context.read>(), child: AddDocumentTypePage(initialName: initialName), ), textFieldLabel: @@ -189,9 +184,8 @@ class _DocumentUploadPreparationPageState formBuilderState: _formKey.currentState, labelCreationWidgetBuilder: (initialName) => RepositoryProvider( - create: (context) => context.read< - LabelRepository>(), + create: (context) => + context.read>(), child: AddCorrespondentPage(initialName: initialName), ), textFieldLabel: diff --git a/lib/features/documents/bloc/documents_cubit.dart b/lib/features/documents/bloc/documents_cubit.dart index bc7bd04..b251a0f 100644 --- a/lib/features/documents/bloc/documents_cubit.dart +++ b/lib/features/documents/bloc/documents_cubit.dart @@ -1,10 +1,10 @@ import 'dart:async'; import 'dart:developer'; +import 'package:flutter/foundation.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; -import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart'; @@ -17,14 +17,21 @@ class DocumentsCubit extends HydratedCubit final DocumentChangedNotifier notifier; DocumentsCubit(this.api, this.notifier) : super(const DocumentsState()) { - reload(); + notifier.subscribe( + this, + onUpdated: replace, + onDeleted: remove, + ); } - Future bulkRemove(List documents) async { - log("[DocumentsCubit] bulkRemove"); + Future bulkDelete(List documents) async { + debugPrint("[DocumentsCubit] bulkRemove"); await api.bulkAction( BulkDeleteAction(documents.map((doc) => doc.id)), ); + for (final deletedDoc in documents) { + notifier.notifyDeleted(deletedDoc); + } await reload(); } @@ -33,7 +40,7 @@ class DocumentsCubit extends HydratedCubit Iterable addTags = const [], Iterable removeTags = const [], }) async { - log("[DocumentsCubit] bulkEditTags"); + debugPrint("[DocumentsCubit] bulkEditTags"); await api.bulkAction(BulkModifyTagsAction( documents.map((doc) => doc.id), addTags: addTags, @@ -43,7 +50,7 @@ class DocumentsCubit extends HydratedCubit } void toggleDocumentSelection(DocumentModel model) { - log("[DocumentsCubit] toggleSelection"); + debugPrint("[DocumentsCubit] toggleSelection"); if (state.selectedIds.contains(model.id)) { emit( state.copyWith( @@ -58,12 +65,12 @@ class DocumentsCubit extends HydratedCubit } void resetSelection() { - log("[DocumentsCubit] resetSelection"); + debugPrint("[DocumentsCubit] resetSelection"); emit(state.copyWith(selection: [])); } void reset() { - log("[DocumentsCubit] reset"); + debugPrint("[DocumentsCubit] reset"); emit(const DocumentsState()); } @@ -81,4 +88,10 @@ class DocumentsCubit extends HydratedCubit Map? toJson(DocumentsState state) { return state.toJson(); } + + @override + Future close() { + notifier.unsubscribe(this); + return super.close(); + } } diff --git a/lib/features/documents/bloc/documents_state.dart b/lib/features/documents/bloc/documents_state.dart index 1e080a5..371c729 100644 --- a/lib/features/documents/bloc/documents_state.dart +++ b/lib/features/documents/bloc/documents_state.dart @@ -3,7 +3,7 @@ import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart'; class DocumentsState extends PagedDocumentsState { - @JsonKey(ignore: true) + @JsonKey(includeFromJson: true, includeToJson: false) final List selection; const DocumentsState({ @@ -34,11 +34,8 @@ class DocumentsState extends PagedDocumentsState { @override List get props => [ - hasLoaded, - filter, - value, selection, - isLoading, + ...super.props, ]; Map toJson() { diff --git a/lib/features/documents/view/pages/document_edit_page.dart b/lib/features/documents/view/pages/document_edit_page.dart index 9238a3e..b9b8c80 100644 --- a/lib/features/documents/view/pages/document_edit_page.dart +++ b/lib/features/documents/view/pages/document_edit_page.dart @@ -160,8 +160,7 @@ class _DocumentEditPageState extends State { notAssignedSelectable: false, formBuilderState: _formKey.currentState, labelCreationWidgetBuilder: (initialValue) => RepositoryProvider( - create: (context) => context.read< - LabelRepository>(), + create: (context) => context.read>(), child: AddStoragePathPage(initalValue: initialValue), ), textFieldLabel: S.of(context).documentStoragePathPropertyLabel, @@ -182,8 +181,7 @@ class _DocumentEditPageState extends State { notAssignedSelectable: false, formBuilderState: _formKey.currentState, labelCreationWidgetBuilder: (initialValue) => RepositoryProvider( - create: (context) => context.read< - LabelRepository>(), + create: (context) => context.read>(), child: AddCorrespondentPage(initialName: initialValue), ), textFieldLabel: S.of(context).documentCorrespondentPropertyLabel, @@ -215,8 +213,7 @@ class _DocumentEditPageState extends State { notAssignedSelectable: false, formBuilderState: _formKey.currentState, labelCreationWidgetBuilder: (currentInput) => RepositoryProvider( - create: (context) => context.read< - LabelRepository>(), + create: (context) => context.read>(), child: AddDocumentTypePage( initialName: currentInput, ), diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index 0889c9a..6074712 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -249,7 +249,7 @@ class _DocumentsPageState extends State Builder( builder: (context) { return RefreshIndicator( - edgeOffset: kToolbarHeight, + edgeOffset: kToolbarHeight + kTextTabBarHeight, onRefresh: _onReloadDocuments, notificationPredicate: (_) => connectivityState.isConnected, @@ -263,13 +263,14 @@ class _DocumentsPageState extends State ), _buildViewActions(), BlocBuilder( - buildWhen: (previous, current) => - !const ListEquality().equals( - previous.documents, - current.documents, - ) || - previous.selectedIds != - current.selectedIds, + // Not required anymore since saved views are now handled separately + // buildWhen: (previous, current) => + // !const ListEquality().equals( + // previous.documents, + // current.documents, + // ) || + // previous.selectedIds != + // current.selectedIds, builder: (context, state) { if (state.hasLoaded && state.documents.isEmpty) { @@ -323,7 +324,7 @@ class _DocumentsPageState extends State Builder( builder: (context) { return RefreshIndicator( - edgeOffset: kToolbarHeight, + edgeOffset: kToolbarHeight + kTextTabBarHeight, onRefresh: _onReloadSavedViews, notificationPredicate: (_) => connectivityState.isConnected, @@ -390,7 +391,7 @@ class _DocumentsPageState extends State try { await context .read() - .bulkRemove(documentsState.selection); + .bulkDelete(documentsState.selection); showSnackBar( context, S.of(context).documentsPageBulkDeleteSuccessfulText, @@ -467,20 +468,14 @@ class _DocumentsPageState extends State } } - Future _openDetails(DocumentModel document) async { - final updatedModel = await Navigator.pushNamed( + void _openDetails(DocumentModel document) { + Navigator.pushNamed( context, DocumentDetailsRoute.routeName, arguments: DocumentDetailsRouteArguments( document: document, ), - ) as DocumentModel?; - // final updatedModel = await Navigator.of(context).push( - // _buildDetailsPageRoute(document), - // ); - if (updatedModel != document) { - context.read().reload(); - } + ); } void _addTagToFilter(int tagId) { diff --git a/lib/features/documents/view/widgets/adaptive_documents_view.dart b/lib/features/documents/view/widgets/adaptive_documents_view.dart index ee1d343..eb997a5 100644 --- a/lib/features/documents/view/widgets/adaptive_documents_view.dart +++ b/lib/features/documents/view/widgets/adaptive_documents_view.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/document_grid_loading_widget.dart'; import 'package:paperless_mobile/features/documents/view/widgets/documents_list_loading_widget.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; import 'package:paperless_mobile/features/documents/view/widgets/items/document_grid_item.dart'; import 'package:paperless_mobile/features/documents/view/widgets/items/document_list_item.dart'; import 'package:paperless_mobile/features/settings/model/view_type.dart'; @@ -23,6 +23,7 @@ abstract class AdaptiveDocumentsView extends StatelessWidget { final void Function(int? id)? onDocumentTypeSelected; final void Function(int? id)? onStoragePathSelected; + bool get showLoadingPlaceholder => (!hasLoaded && isLoading); const AdaptiveDocumentsView({ super.key, this.selectedDocumentIds = const [], @@ -56,6 +57,7 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView { super.onTap, super.selectedDocumentIds, super.viewType, + super.enableHeroAnimation, required super.isLoading, required super.hasLoaded, }); @@ -71,8 +73,8 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView { } Widget _buildListView() { - if (!hasLoaded && isLoading) { - return const DocumentsListLoadingWidget(); + if (showLoadingPlaceholder) { + return DocumentsListLoadingWidget.sliver(); } return SliverList( delegate: SliverChildBuilderDelegate( @@ -91,6 +93,7 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView { onCorrespondentSelected: onCorrespondentSelected, onDocumentTypeSelected: onDocumentTypeSelected, onStoragePathSelected: onStoragePathSelected, + enableHeroAnimation: enableHeroAnimation, ), ); }, @@ -99,8 +102,8 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView { } Widget _buildGridView() { - if (!hasLoaded && isLoading) { - return const DocumentsListLoadingWidget(); + if (showLoadingPlaceholder) { + return DocumentGridLoadingWidget.sliver(); } return SliverGrid.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( @@ -162,10 +165,8 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView { } Widget _buildListView() { - if (!hasLoaded && isLoading) { - return const CustomScrollView(slivers: [ - DocumentsListLoadingWidget(), - ]); + if (showLoadingPlaceholder) { + return DocumentsListLoadingWidget(); } return ListView.builder( @@ -194,12 +195,8 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView { } Widget _buildGridView() { - if (!hasLoaded && isLoading) { - return const CustomScrollView( - slivers: [ - DocumentsListLoadingWidget(), - ], - ); //TODO: Build grid skeleton + if (showLoadingPlaceholder) { + return DocumentGridLoadingWidget(); } return GridView.builder( controller: scrollController, diff --git a/lib/features/documents/view/widgets/document_grid_loading_widget.dart b/lib/features/documents/view/widgets/document_grid_loading_widget.dart index e69de29..d18d5d9 100644 --- a/lib/features/documents/view/widgets/document_grid_loading_widget.dart +++ b/lib/features/documents/view/widgets/document_grid_loading_widget.dart @@ -0,0 +1,102 @@ +import 'dart:math'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart'; +import 'package:paperless_mobile/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/placeholder/document_item_placeholder.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/placeholder/tags_placeholder.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/placeholder/text_placeholder.dart'; +import 'package:shimmer/shimmer.dart'; + +class DocumentGridLoadingWidget extends StatelessWidget + with DocumentItemPlaceholder { + final bool _isSliver; + @override + final Random random = Random(1257195195); + DocumentGridLoadingWidget({super.key}) : _isSliver = false; + + DocumentGridLoadingWidget.sliver({super.key}) : _isSliver = true; + + @override + Widget build(BuildContext context) { + const delegate = SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 4, + crossAxisSpacing: 4, + childAspectRatio: 1 / 2, + ); + if (_isSliver) { + return SliverGrid.builder( + gridDelegate: delegate, + itemBuilder: (context, index) => _buildPlaceholderGridItem(context), + ); + } + return GridView.builder( + gridDelegate: delegate, + itemBuilder: (context, index) => _buildPlaceholderGridItem(context), + ); + } + + Widget _buildPlaceholderGridItem(BuildContext context) { + final values = nextValues; + + return Padding( + padding: const EdgeInsets.all(8.0), + child: Card( + elevation: 1.0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ShimmerPlaceholder( + child: AspectRatio( + aspectRatio: 1, + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Container( + color: Colors.white, + ), + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: ShimmerPlaceholder( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextPlaceholder( + length: values.correspondentLength, + fontSize: 16, + ).padded(1), + TextPlaceholder( + length: values.titleLength, + fontSize: 16, + ), + if (values.tagCount > 0) ...[ + const Spacer(), + TagsPlaceholder( + count: values.tagCount, + dense: true, + ), + ], + const Spacer(), + TextPlaceholder( + length: 100, + fontSize: + Theme.of(context).textTheme.bodySmall!.fontSize!, + ), + ], + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/documents/view/widgets/documents_list_loading_widget.dart b/lib/features/documents/view/widgets/documents_list_loading_widget.dart index 433a607..034c074 100644 --- a/lib/features/documents/view/widgets/documents_list_loading_widget.dart +++ b/lib/features/documents/view/widgets/documents_list_loading_widget.dart @@ -1,42 +1,42 @@ import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; -import 'package:shimmer/shimmer.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/placeholder/document_item_placeholder.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/placeholder/tags_placeholder.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/placeholder/text_placeholder.dart'; -class DocumentsListLoadingWidget extends StatelessWidget { - static const _tags = [" ", " ", " "]; - static const _titleLengths = [double.infinity, 150.0, 200.0]; - static const _correspondentLengths = [200.0, 300.0, 150.0]; - static const _fontSize = 16.0; +class DocumentsListLoadingWidget extends StatelessWidget + with DocumentItemPlaceholder { + final bool _isSliver; + DocumentsListLoadingWidget({super.key}) : _isSliver = false; - const DocumentsListLoadingWidget({super.key - }); + DocumentsListLoadingWidget.sliver({super.key}) : _isSliver = true; + + @override + final Random random = Random(1209571050); @override Widget build(BuildContext context) { - final _random = Random(); - return SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - return _buildFakeListItem(context, _random); - }, - ), - ); + if (_isSliver) { + return SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) => _buildFakeListItem(context), + ), + ); + } else { + return ListView.builder( + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) => _buildFakeListItem(context), + ); + } } - Widget _buildFakeListItem(BuildContext context, Random random) { - final tagCount = random.nextInt(_tags.length + 1); - final correspondentLength = - _correspondentLengths[random.nextInt(_correspondentLengths.length - 1)]; - final titleLength = _titleLengths[random.nextInt(_titleLengths.length - 1)]; - return Shimmer.fromColors( - baseColor: Theme.of(context).brightness == Brightness.light - ? Colors.grey[300]! - : Colors.grey[900]!, - highlightColor: Theme.of(context).brightness == Brightness.light - ? Colors.grey[100]! - : Colors.grey[600]!, + Widget _buildFakeListItem(BuildContext context) { + const fontSize = 14.0; + final values = nextValues; + return ShimmerPlaceholder( child: ListTile( contentPadding: const EdgeInsets.all(8), dense: true, @@ -45,15 +45,17 @@ class DocumentsListLoadingWidget extends StatelessWidget { borderRadius: BorderRadius.circular(8), child: Container( color: Colors.white, - height: 50, + height: double.infinity, width: 35, ), ), - title: Container( - padding: const EdgeInsets.symmetric(vertical: 2.0), - width: correspondentLength, - height: _fontSize, - color: Colors.white, + title: Row( + children: [ + TextPlaceholder( + length: values.correspondentLength, + fontSize: fontSize, + ), + ], ), subtitle: Padding( padding: const EdgeInsets.symmetric(vertical: 2.0), @@ -61,21 +63,16 @@ class DocumentsListLoadingWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - Container( - padding: const EdgeInsets.symmetric(vertical: 2.0), - height: _fontSize, - width: titleLength, - color: Colors.white, + TextPlaceholder( + length: values.titleLength, + fontSize: fontSize, + ), + if (values.tagCount > 0) + TagsPlaceholder(count: values.tagCount, dense: true), + TextPlaceholder( + length: 100, + fontSize: Theme.of(context).textTheme.labelSmall!.fontSize!, ), - Wrap( - spacing: 2.0, - children: List.generate( - tagCount, - (index) => InputChip( - label: Text(_tags[random.nextInt(_tags.length)]), - ), - ), - ).paddedOnly(top: 4), ], ), ), diff --git a/lib/features/documents/view/widgets/items/document_list_item.dart b/lib/features/documents/view/widgets/items/document_list_item.dart index e76b802..6732d38 100644 --- a/lib/features/documents/view/widgets/items/document_list_item.dart +++ b/lib/features/documents/view/widgets/items/document_list_item.dart @@ -56,7 +56,7 @@ class DocumentListItem extends DocumentItem { Text( document.title, overflow: TextOverflow.ellipsis, - maxLines: document.tags.isEmpty ? 2 : 1, + maxLines: 1, ), AbsorbPointer( absorbing: isSelectionActive, diff --git a/lib/features/documents/view/widgets/placeholder/document_item_placeholder.dart b/lib/features/documents/view/widgets/placeholder/document_item_placeholder.dart new file mode 100644 index 0000000..951e5ff --- /dev/null +++ b/lib/features/documents/view/widgets/placeholder/document_item_placeholder.dart @@ -0,0 +1,30 @@ +import 'dart:math'; + +mixin DocumentItemPlaceholder { + static const _tags = [" ", " ", " "]; + static const _titleLengths = [double.infinity, 150.0, 200.0]; + static const _correspondentLengths = [120.0, 80.0, 40.0]; + + Random get random; + + RandomDocumentItemPlaceholderValues get nextValues { + return RandomDocumentItemPlaceholderValues( + tagCount: random.nextInt(_tags.length + 1), + correspondentLength: _correspondentLengths[ + random.nextInt(_correspondentLengths.length - 1)], + titleLength: _titleLengths[random.nextInt(_titleLengths.length - 1)], + ); + } +} + +class RandomDocumentItemPlaceholderValues { + final int tagCount; + final double correspondentLength; + final double titleLength; + + RandomDocumentItemPlaceholderValues({ + required this.tagCount, + required this.correspondentLength, + required this.titleLength, + }); +} diff --git a/lib/features/documents/view/widgets/placeholder/tags_placeholder.dart b/lib/features/documents/view/widgets/placeholder/tags_placeholder.dart new file mode 100644 index 0000000..757f3ef --- /dev/null +++ b/lib/features/documents/view/widgets/placeholder/tags_placeholder.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; + +class TagsPlaceholder extends StatelessWidget { + static const _lengths = [24, 36, 16, 48]; + final int count; + final bool dense; + const TagsPlaceholder({ + super.key, + required this.count, + required this.dense, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 32, + child: ListView.separated( + itemCount: count, + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) => FilterChip( + labelPadding: + dense ? const EdgeInsets.symmetric(horizontal: 2) : null, + padding: dense ? const EdgeInsets.all(4) : null, + visualDensity: const VisualDensity(vertical: -2), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + side: BorderSide.none, + onSelected: (_) {}, + selected: false, + label: Text( + List.filled(_lengths[index], " ").join(), + ), + ), + separatorBuilder: (context, _) => const SizedBox(width: 4), + ), + ); + } +} diff --git a/lib/features/documents/view/widgets/placeholder/text_placeholder.dart b/lib/features/documents/view/widgets/placeholder/text_placeholder.dart new file mode 100644 index 0000000..ef02729 --- /dev/null +++ b/lib/features/documents/view/widgets/placeholder/text_placeholder.dart @@ -0,0 +1,26 @@ +import 'dart:math'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:paperless_mobile/extensions/flutter_extensions.dart'; + +class TextPlaceholder extends StatelessWidget { + final double length; + final double fontSize; + + const TextPlaceholder({ + super.key, + required this.length, + required this.fontSize, + }); + + @override + Widget build(BuildContext context) { + return Container( + color: Colors.white, + width: length, + height: fontSize, + ); + } +} diff --git a/lib/features/documents/view/widgets/sort_documents_button.dart b/lib/features/documents/view/widgets/sort_documents_button.dart index a4610a8..d87466b 100644 --- a/lib/features/documents/view/widgets/sort_documents_button.dart +++ b/lib/features/documents/view/widgets/sort_documents_button.dart @@ -44,16 +44,12 @@ class SortDocumentsButton extends StatelessWidget { providers: [ BlocProvider( create: (context) => LabelCubit( - context.read< - LabelRepository>(), + context.read>(), ), ), BlocProvider( create: (context) => LabelCubit( - context.read< - LabelRepository>(), + context.read>(), ), ), ], diff --git a/lib/features/edit_document/cubit/edit_document_cubit.dart b/lib/features/edit_document/cubit/edit_document_cubit.dart index 85be2d1..6942d0b 100644 --- a/lib/features/edit_document/cubit/edit_document_cubit.dart +++ b/lib/features/edit_document/cubit/edit_document_cubit.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:collection/collection.dart'; import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart'; @@ -16,31 +17,28 @@ class EditDocumentCubit extends Cubit { final DocumentModel _initialDocument; final PaperlessDocumentsApi _docsApi; - final LabelRepository - _correspondentRepository; - final LabelRepository - _documentTypeRepository; - final LabelRepository - _storagePathRepository; - final LabelRepository _tagRepository; - + final DocumentChangedNotifier _notifier; + final LabelRepository _correspondentRepository; + final LabelRepository _documentTypeRepository; + final LabelRepository _storagePathRepository; + final LabelRepository _tagRepository; final List _subscriptions = []; + EditDocumentCubit( DocumentModel document, { required PaperlessDocumentsApi documentsApi, - required LabelRepository - correspondentRepository, - required LabelRepository - documentTypeRepository, - required LabelRepository - storagePathRepository, - required LabelRepository tagRepository, + required LabelRepository correspondentRepository, + required LabelRepository documentTypeRepository, + required LabelRepository storagePathRepository, + required LabelRepository tagRepository, + required DocumentChangedNotifier notifier, }) : _initialDocument = document, _docsApi = documentsApi, _correspondentRepository = correspondentRepository, _documentTypeRepository = documentTypeRepository, _storagePathRepository = storagePathRepository, _tagRepository = tagRepository, + _notifier = notifier, super( EditDocumentState( document: document, @@ -50,6 +48,7 @@ class EditDocumentCubit extends Cubit { tags: tagRepository.current?.values ?? {}, ), ) { + _notifier.subscribe(this, onUpdated: replace); _subscriptions.add( _correspondentRepository.values .listen((v) => emit(state.copyWith(correspondents: v?.values))), @@ -71,6 +70,8 @@ class EditDocumentCubit extends Cubit { Future updateDocument(DocumentModel document) async { final updated = await _docsApi.update(document); + _notifier.notifyUpdated(updated); + // Reload changed labels (documentCount property changes with removal/add) if (document.documentType != _initialDocument.documentType) { _documentTypeRepository @@ -88,7 +89,10 @@ class EditDocumentCubit extends Cubit { .equals(document.tags, _initialDocument.tags)) { _tagRepository.findAll(document.tags); } - emit(state.copyWith(document: updated)); + } + + void replace(DocumentModel document) { + emit(state.copyWith(document: document)); } @override @@ -96,6 +100,7 @@ class EditDocumentCubit extends Cubit { for (final sub in _subscriptions) { sub.cancel(); } + _notifier.unsubscribe(this); return super.close(); } } diff --git a/lib/features/edit_label/cubit/edit_label_cubit.dart b/lib/features/edit_label/cubit/edit_label_cubit.dart index 248ca9d..9ec07e8 100644 --- a/lib/features/edit_label/cubit/edit_label_cubit.dart +++ b/lib/features/edit_label/cubit/edit_label_cubit.dart @@ -3,15 +3,15 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; -import 'package:paperless_mobile/core/repository/state/repository_state.dart'; +import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart'; import 'package:paperless_mobile/features/edit_label/cubit/edit_label_state.dart'; class EditLabelCubit extends Cubit> { - final LabelRepository>> _repository; + final LabelRepository _repository; StreamSubscription? _subscription; - EditLabelCubit(LabelRepository>> repository) + EditLabelCubit(LabelRepository repository) : _repository = repository, super(const EditLabelInitial()) { _subscription = repository.values.listen( diff --git a/lib/features/edit_label/view/add_label_page.dart b/lib/features/edit_label/view/add_label_page.dart index 227e032..5b0e593 100644 --- a/lib/features/edit_label/view/add_label_page.dart +++ b/lib/features/edit_label/view/add_label_page.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; -import 'package:paperless_mobile/core/repository/state/repository_state.dart'; +import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart'; import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart'; import 'package:paperless_mobile/features/edit_label/view/label_form.dart'; import 'package:paperless_mobile/generated/l10n.dart'; @@ -25,8 +25,7 @@ class AddLabelPage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => EditLabelCubit( - context - .read>>>(), + context.read>(), ), child: AddLabelFormWidget( pageTitle: pageTitle, diff --git a/lib/features/edit_label/view/edit_label_page.dart b/lib/features/edit_label/view/edit_label_page.dart index 28d2273..8fe7d27 100644 --- a/lib/features/edit_label/view/edit_label_page.dart +++ b/lib/features/edit_label/view/edit_label_page.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; -import 'package:paperless_mobile/core/repository/state/repository_state.dart'; +import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart'; import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart'; import 'package:paperless_mobile/features/edit_label/view/label_form.dart'; import 'package:paperless_mobile/generated/l10n.dart'; @@ -28,8 +28,7 @@ class EditLabelPage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => EditLabelCubit( - context - .read>>>(), + context.read>(), ), child: EditLabelForm( label: label, diff --git a/lib/features/edit_label/view/impl/add_correspondent_page.dart b/lib/features/edit_label/view/impl/add_correspondent_page.dart index 9df0cd4..08d4c77 100644 --- a/lib/features/edit_label/view/impl/add_correspondent_page.dart +++ b/lib/features/edit_label/view/impl/add_correspondent_page.dart @@ -15,8 +15,7 @@ class AddCorrespondentPage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => EditLabelCubit( - context.read< - LabelRepository>(), + context.read>(), ), child: AddLabelPage( pageTitle: Text(S.of(context).addCorrespondentPageTitle), diff --git a/lib/features/edit_label/view/impl/add_document_type_page.dart b/lib/features/edit_label/view/impl/add_document_type_page.dart index 1fc30ca..e3c19e9 100644 --- a/lib/features/edit_label/view/impl/add_document_type_page.dart +++ b/lib/features/edit_label/view/impl/add_document_type_page.dart @@ -18,8 +18,7 @@ class AddDocumentTypePage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => EditLabelCubit( - context - .read>(), + context.read>(), ), child: AddLabelPage( pageTitle: Text(S.of(context).addDocumentTypePageTitle), diff --git a/lib/features/edit_label/view/impl/add_storage_path_page.dart b/lib/features/edit_label/view/impl/add_storage_path_page.dart index c5926e4..3ab343c 100644 --- a/lib/features/edit_label/view/impl/add_storage_path_page.dart +++ b/lib/features/edit_label/view/impl/add_storage_path_page.dart @@ -16,8 +16,7 @@ class AddStoragePathPage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => EditLabelCubit( - context - .read>(), + context.read>(), ), child: AddLabelPage( pageTitle: Text(S.of(context).addStoragePathPageTitle), diff --git a/lib/features/edit_label/view/impl/add_tag_page.dart b/lib/features/edit_label/view/impl/add_tag_page.dart index 157db6a..76257eb 100644 --- a/lib/features/edit_label/view/impl/add_tag_page.dart +++ b/lib/features/edit_label/view/impl/add_tag_page.dart @@ -19,7 +19,7 @@ class AddTagPage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => EditLabelCubit( - context.read>(), + context.read>(), ), child: AddLabelPage( pageTitle: Text(S.of(context).addTagPageTitle), diff --git a/lib/features/edit_label/view/impl/edit_correspondent_page.dart b/lib/features/edit_label/view/impl/edit_correspondent_page.dart index e620db9..5c01408 100644 --- a/lib/features/edit_label/view/impl/edit_correspondent_page.dart +++ b/lib/features/edit_label/view/impl/edit_correspondent_page.dart @@ -14,8 +14,7 @@ class EditCorrespondentPage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => EditLabelCubit( - context.read< - LabelRepository>(), + context.read>(), ), child: EditLabelPage( label: correspondent, diff --git a/lib/features/edit_label/view/impl/edit_document_type_page.dart b/lib/features/edit_label/view/impl/edit_document_type_page.dart index a3a7a9b..d698aec 100644 --- a/lib/features/edit_label/view/impl/edit_document_type_page.dart +++ b/lib/features/edit_label/view/impl/edit_document_type_page.dart @@ -14,8 +14,7 @@ class EditDocumentTypePage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => EditLabelCubit( - context - .read>(), + context.read>(), ), child: EditLabelPage( label: documentType, diff --git a/lib/features/edit_label/view/impl/edit_storage_path_page.dart b/lib/features/edit_label/view/impl/edit_storage_path_page.dart index 2994796..73a66e0 100644 --- a/lib/features/edit_label/view/impl/edit_storage_path_page.dart +++ b/lib/features/edit_label/view/impl/edit_storage_path_page.dart @@ -15,8 +15,7 @@ class EditStoragePathPage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => EditLabelCubit( - context - .read>(), + context.read>(), ), child: EditLabelPage( label: storagePath, diff --git a/lib/features/edit_label/view/impl/edit_tag_page.dart b/lib/features/edit_label/view/impl/edit_tag_page.dart index 686873d..678b944 100644 --- a/lib/features/edit_label/view/impl/edit_tag_page.dart +++ b/lib/features/edit_label/view/impl/edit_tag_page.dart @@ -18,7 +18,7 @@ class EditTagPage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => EditLabelCubit( - context.read>(), + context.read>(), ), child: EditLabelPage( label: tag, diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index 0e9729f..c6e9f9f 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -23,6 +23,7 @@ import 'package:paperless_mobile/features/home/view/route_description.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/pages/inbox_page.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; import 'package:paperless_mobile/features/labels/view/pages/labels_page.dart'; import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; @@ -59,6 +60,7 @@ class _HomePageState extends State { context.read(), context.read(), context.read(), + context.read(), ); context.read().reload(); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { @@ -228,7 +230,23 @@ class _HomePageState extends State { value: _scannerCubit, child: const ScannerPage(), ), - const LabelsPage(), + MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => LabelCubit(context.read()), + ), + BlocProvider( + create: (context) => LabelCubit(context.read()), + ), + BlocProvider( + create: (context) => LabelCubit(context.read()), + ), + BlocProvider( + create: (context) => LabelCubit(context.read()), + ), + ], + child: const LabelsPage(), + ), BlocProvider.value( value: _inboxCubit, child: const InboxPage(), @@ -302,16 +320,10 @@ class _HomePageState extends State { void _initializeData(BuildContext context) { try { - context.read>().findAll(); - context - .read>() - .findAll(); - context - .read>() - .findAll(); - context - .read>() - .findAll(); + context.read>().findAll(); + context.read>().findAll(); + context.read>().findAll(); + context.read>().findAll(); context.read().findAll(); context.read().updateInformtion(); } on PaperlessServerException catch (error, stackTrace) { diff --git a/lib/features/home/view/widget/verify_identity_page.dart b/lib/features/home/view/widget/verify_identity_page.dart index 78814d7..a518d57 100644 --- a/lib/features/home/view/widget/verify_identity_page.dart +++ b/lib/features/home/view/widget/verify_identity_page.dart @@ -70,16 +70,10 @@ class VerifyIdentityPage extends StatelessWidget { void _logout(BuildContext context) { context.read().logout(); - context.read>().clear(); - context - .read>() - .clear(); - context - .read>() - .clear(); - context - .read>() - .clear(); + context.read>().clear(); + context.read>().clear(); + context.read>().clear(); + context.read>().clear(); context.read().clear(); HydratedBloc.storage.clear(); } diff --git a/lib/features/inbox/bloc/inbox_cubit.dart b/lib/features/inbox/bloc/inbox_cubit.dart index 8ad1c81..b408f04 100644 --- a/lib/features/inbox/bloc/inbox_cubit.dart +++ b/lib/features/inbox/bloc/inbox_cubit.dart @@ -1,23 +1,20 @@ import 'dart:async'; +import 'package:collection/collection.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; -import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart'; -import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart'; -import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart'; import 'package:paperless_mobile/features/inbox/bloc/state/inbox_state.dart'; import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart'; class InboxCubit extends HydratedCubit with PagedDocumentsMixin { - final LabelRepository _tagsRepository; - final LabelRepository - _correspondentRepository; - final LabelRepository - _documentTypeRepository; + final LabelRepository _tagsRepository; + final LabelRepository _correspondentRepository; + final LabelRepository _documentTypeRepository; final PaperlessDocumentsApi _documentsApi; + @override final DocumentChangedNotifier notifier; @@ -28,7 +25,6 @@ class InboxCubit extends HydratedCubit with PagedDocumentsMixin { @override PaperlessDocumentsApi get api => _documentsApi; - Timer? _taskTimer; InboxCubit( this._tagsRepository, this._documentsApi, @@ -45,11 +41,20 @@ class InboxCubit extends HydratedCubit with PagedDocumentsMixin { availableTags: _tagsRepository.current?.values ?? {}, ), ) { - _subscriptions.addAll( - notifier.listen( - onDeleted: remove, - onUpdated: replace, - ), + notifier.subscribe( + this, + onDeleted: remove, + onUpdated: (document) { + if (document.tags + .toSet() + .intersection(state.inboxTags.toSet()) + .isEmpty) { + remove(document); + emit(state.copyWith(itemsInInboxCount: state.itemsInInboxCount - 1)); + } else { + replace(document); + } + }, ); _subscriptions.add( _tagsRepository.values.listen((event) { @@ -74,21 +79,35 @@ class InboxCubit extends HydratedCubit with PagedDocumentsMixin { } }), ); - //TODO: Do this properly in a background task. - _taskTimer = Timer.periodic(const Duration(seconds: 5), (timer) { + + refreshItemsInInboxCount(false); + loadInbox(); + + Timer.periodic(const Duration(seconds: 5), (timer) { + if (isClosed) { + timer.cancel(); + } refreshItemsInInboxCount(); }); } - void refreshItemsInInboxCount() async { + void refreshItemsInInboxCount([bool shouldLoadInbox = true]) async { final stats = await _statsApi.getServerStatistics(); - emit(state.copyWith(itemsInInboxCount: stats.documentsInInbox)); + + if (stats.documentsInInbox != state.itemsInInboxCount && shouldLoadInbox) { + loadInbox(); + } + emit( + state.copyWith( + itemsInInboxCount: stats.documentsInInbox, + ), + ); } /// /// Fetches inbox tag ids and loads the inbox items (documents). /// - Future initializeInbox() async { + Future loadInbox() async { final inboxTags = await _tagsRepository.findAll().then( (tags) => tags.where((t) => t.isInboxTag ?? false).map((t) => t.id!), ); @@ -104,7 +123,7 @@ class InboxCubit extends HydratedCubit with PagedDocumentsMixin { ); } emit(state.copyWith(inboxTags: inboxTags)); - return updateFilter( + updateFilter( filter: DocumentFilter( sortField: SortField.added, tags: IdsTagsQuery.fromIds(inboxTags), @@ -121,11 +140,12 @@ class InboxCubit extends HydratedCubit with PagedDocumentsMixin { document.tags.toSet().intersection(state.inboxTags.toSet()); final updatedTags = {...document.tags}..removeAll(tagsToRemove); - await api.update( + final updatedDocument = await api.update( document.copyWith(tags: updatedTags), ); - await remove(document); - emit(state.copyWith(itemsInInboxCount: state.itemsInInboxCount - 1)); + // Remove first so document is not replaced first. + remove(document); + notifier.notifyUpdated(updatedDocument); return tagsToRemove; } @@ -136,10 +156,12 @@ class InboxCubit extends HydratedCubit with PagedDocumentsMixin { DocumentModel document, Iterable removedTags, ) async { - final updatedDoc = document.copyWith( - tags: {...document.tags, ...removedTags}, + final updatedDocument = await _documentsApi.update( + document.copyWith( + tags: {...document.tags, ...removedTags}, + ), ); - await _documentsApi.update(updatedDoc); + notifier.notifyUpdated(updatedDocument); emit(state.copyWith(itemsInInboxCount: state.itemsInInboxCount + 1)); return reload(); } @@ -166,22 +188,12 @@ class InboxCubit extends HydratedCubit with PagedDocumentsMixin { } } - void replaceUpdatedDocument(DocumentModel document) { - if (document.tags.any((id) => state.inboxTags.contains(id))) { - // If replaced document still has inbox tag assigned: - replace(document); - } else { - // Remove document from inbox. - remove(document); - emit(state.copyWith(itemsInInboxCount: state.itemsInInboxCount - 1)); - } - } - Future assignAsn(DocumentModel document) async { if (document.archiveSerialNumber == null) { final int asn = await _documentsApi.findNextAsn(); final updatedDocument = await _documentsApi .update(document.copyWith(archiveSerialNumber: asn)); + replace(updatedDocument); } } @@ -202,7 +214,6 @@ class InboxCubit extends HydratedCubit with PagedDocumentsMixin { @override Future close() { - _taskTimer?.cancel(); for (var sub in _subscriptions) { sub.cancel(); } diff --git a/lib/features/inbox/bloc/state/inbox_state.dart b/lib/features/inbox/bloc/state/inbox_state.dart index a2a5814..8546bac 100644 --- a/lib/features/inbox/bloc/state/inbox_state.dart +++ b/lib/features/inbox/bloc/state/inbox_state.dart @@ -4,9 +4,7 @@ import 'package:paperless_mobile/features/paged_document_view/model/paged_docume part 'inbox_state.g.dart'; -@JsonSerializable( - ignoreUnannotated: true, -) +@JsonSerializable(ignoreUnannotated: true) class InboxState extends PagedDocumentsState { final Iterable inboxTags; diff --git a/lib/features/inbox/view/pages/inbox_page.dart b/lib/features/inbox/view/pages/inbox_page.dart index 11cb7d2..92fbc1d 100644 --- a/lib/features/inbox/view/pages/inbox_page.dart +++ b/lib/features/inbox/view/pages/inbox_page.dart @@ -31,7 +31,7 @@ class _InboxPageState extends State { @override void initState() { super.initState(); - context.read().initializeInbox(); + context.read().loadInbox(); _scrollController.addListener(_listenForLoadNewData); } @@ -57,6 +57,12 @@ class _InboxPageState extends State { @override Widget build(BuildContext context) { + final safeAreaPadding = MediaQuery.of(context).padding; + final availableHeight = MediaQuery.of(context).size.height - + kToolbarHeight - + kBottomNavigationBarHeight - + safeAreaPadding.top - + safeAreaPadding.bottom; return Scaffold( drawer: const AppDrawer(), floatingActionButton: BlocBuilder( @@ -76,97 +82,105 @@ class _InboxPageState extends State { ); }, ), - body: RefreshIndicator( - edgeOffset: 78, - onRefresh: () => context.read().initializeInbox(), - child: NestedScrollView( - headerSliverBuilder: (context, innerBoxIsScrolled) => [ - SearchAppBar( - hintText: S.of(context).documentSearchSearchDocuments, - onOpenSearch: showDocumentSearchPage, - ), - ], - body: BlocBuilder( - builder: (context, state) { - if (!state.hasLoaded) { - return const CustomScrollView( - physics: NeverScrollableScrollPhysics(), - slivers: [DocumentsListLoadingWidget()], - ); - } - - if (state.documents.isEmpty) { - return InboxEmptyWidget( - emptyStateRefreshIndicatorKey: _emptyStateRefreshIndicatorKey, - ); - } - - // Build a list of slivers alternating between SliverToBoxAdapter - // (group header) and a SliverList (inbox items). - final List slivers = _groupByDate(state.documents) - .entries - .map( - (entry) => [ - SliverToBoxAdapter( - child: Align( - alignment: Alignment.centerLeft, - child: ClipRRect( - borderRadius: BorderRadius.circular(32.0), - child: Text( - entry.key, - style: Theme.of(context).textTheme.bodySmall, - textAlign: TextAlign.center, - ).padded(), - ), - ).paddedOnly(top: 8.0), - ), - SliverList( - delegate: SliverChildBuilderDelegate( - childCount: entry.value.length, - (context, index) { - if (index < entry.value.length - 1) { - return Column( - children: [ - _buildListItem( - entry.value[index], - ), - const Divider( - indent: 16, - endIndent: 16, - ), - ], - ); - } - return _buildListItem( - entry.value[index], - ); - }, + body: BlocBuilder( + builder: (context, state) { + return SafeArea( + top: true, + child: Builder( + builder: (context) { + // Build a list of slivers alternating between SliverToBoxAdapter + // (group header) and a SliverList (inbox items). + final List slivers = _groupByDate(state.documents) + .entries + .map( + (entry) => [ + SliverToBoxAdapter( + child: Align( + alignment: Alignment.centerLeft, + child: ClipRRect( + borderRadius: BorderRadius.circular(32.0), + child: Text( + entry.key, + style: Theme.of(context).textTheme.bodySmall, + textAlign: TextAlign.center, + ).padded(), + ), + ).paddedOnly(top: 8.0), ), - ), - ], - ) - .flattened - .toList() - ..add(const SliverToBoxAdapter(child: SizedBox(height: 78))); - // edgeOffset: kToolbarHeight, + SliverList( + delegate: SliverChildBuilderDelegate( + childCount: entry.value.length, + (context, index) { + if (index < entry.value.length - 1) { + return Column( + children: [ + _buildListItem( + entry.value[index], + ), + const Divider( + indent: 16, + endIndent: 16, + ), + ], + ); + } + return _buildListItem( + entry.value[index], + ); + }, + ), + ), + ], + ) + .flattened + .toList() + ..add(const SliverToBoxAdapter(child: SizedBox(height: 78))); + // edgeOffset: kToolbarHeight, - return CustomScrollView( - controller: _scrollController, - slivers: [ - SliverToBoxAdapter( - child: HintCard( - show: !state.isHintAcknowledged, - hintText: S.of(context).inboxPageUsageHintText, - onHintAcknowledged: () => - context.read().acknowledgeHint(), - ), + return RefreshIndicator( + edgeOffset: kToolbarHeight, + onRefresh: context.read().reload, + child: CustomScrollView( + physics: state.documents.isEmpty + ? const NeverScrollableScrollPhysics() + : const AlwaysScrollableScrollPhysics(), + controller: _scrollController, + slivers: [ + SearchAppBar( + hintText: S.of(context).documentSearchSearchDocuments, + onOpenSearch: showDocumentSearchPage, + ), + if (state.documents.isEmpty) + SliverToBoxAdapter( + child: SizedBox( + height: availableHeight, + child: Center( + child: InboxEmptyWidget( + emptyStateRefreshIndicatorKey: + _emptyStateRefreshIndicatorKey, + ), + ), + ), + ) + else if (!state.hasLoaded) + DocumentsListLoadingWidget() + else + SliverToBoxAdapter( + child: HintCard( + show: !state.isHintAcknowledged, + hintText: S.of(context).inboxPageUsageHintText, + onHintAcknowledged: () => + context.read().acknowledgeHint(), + ), + ), + ...slivers, + ], ), - ...slivers, - ], - ); - }, - ), - ), + ); + }, + ), + ); + }, ), ); } @@ -191,12 +205,7 @@ class _InboxPageState extends State { ).padded(), confirmDismiss: (_) => _onItemDismissed(doc), key: UniqueKey(), - child: InboxItem( - document: doc, - onDocumentUpdated: (document) { - context.read().replaceUpdatedDocument(document); - }, - ), + child: InboxItem(document: doc), ); } diff --git a/lib/features/inbox/view/widgets/inbox_empty_widget.dart b/lib/features/inbox/view/widgets/inbox_empty_widget.dart index bf79d2a..b23fce3 100644 --- a/lib/features/inbox/view/widgets/inbox_empty_widget.dart +++ b/lib/features/inbox/view/widgets/inbox_empty_widget.dart @@ -16,7 +16,7 @@ class InboxEmptyWidget extends StatelessWidget { Widget build(BuildContext context) { return RefreshIndicator( key: _emptyStateRefreshIndicatorKey, - onRefresh: () => context.read().initializeInbox(), + onRefresh: () => context.read().loadInbox(), child: Center( child: Column( mainAxisSize: MainAxisSize.max, diff --git a/lib/features/inbox/view/widgets/inbox_item.dart b/lib/features/inbox/view/widgets/inbox_item.dart index 0f5709e..59b4cab 100644 --- a/lib/features/inbox/view/widgets/inbox_item.dart +++ b/lib/features/inbox/view/widgets/inbox_item.dart @@ -1,13 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; -import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart'; -import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart'; import 'package:paperless_mobile/core/workarounds/colored_chip.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.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/view/widgets/delete_document_confirmation_dialog.dart'; import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart'; import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart'; @@ -19,12 +14,10 @@ import 'package:paperless_mobile/routes/document_details_route.dart'; class InboxItem extends StatefulWidget { static const _a4AspectRatio = 1 / 1.4142; - final void Function(DocumentModel model) onDocumentUpdated; final DocumentModel document; const InboxItem({ super.key, required this.document, - required this.onDocumentUpdated, }); @override @@ -41,17 +34,14 @@ class _InboxItemState extends State { return GestureDetector( behavior: HitTestBehavior.translucent, onTap: () async { - final updatedDocument = await Navigator.pushNamed( + Navigator.pushNamed( context, DocumentDetailsRoute.routeName, arguments: DocumentDetailsRouteArguments( document: widget.document, isLabelClickable: false, ), - ) as DocumentModel?; - if (updatedDocument != null) { - widget.onDocumentUpdated(updatedDocument); - } + ); }, child: SizedBox( height: 200, @@ -104,12 +94,12 @@ class _InboxItemState extends State { ); final actions = [ _buildAssignAsnAction(chipShape, context), - const SizedBox(width: 4.0), + const SizedBox(width: 8.0), ColoredChipWrapper( child: ActionChip( avatar: const Icon(Icons.delete_outline), shape: chipShape, - label: const Text("Delete document"), + label: Text(S.of(context).inboxActionDeleteDocument), onPressed: () async { final shouldDelete = await showDialog( context: context, @@ -124,6 +114,7 @@ class _InboxItemState extends State { ), ), ]; + // return FutureBuilder( // future: _fieldSuggestions, // builder: (context, snapshot) { @@ -151,12 +142,14 @@ class _InboxItemState extends State { mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.bolt_outlined), - SizedBox( - width: 40, + ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 50, + ), child: Text( S.of(context).inboxPageQuickActionsLabel, textAlign: TextAlign.center, - maxLines: 2, + maxLines: 3, style: Theme.of(context).textTheme.labelSmall, ), ), @@ -199,7 +192,7 @@ class _InboxItemState extends State { ? Text( '${S.of(context).documentArchiveSerialNumberPropertyShortLabel} #${widget.document.archiveSerialNumber}', ) - : const Text("Assign ASN"), + : Text(S.of(context).inboxActionAssignAsn), onPressed: !hasAsn ? () { setState(() { @@ -233,7 +226,7 @@ class _InboxItemState extends State { Icons.description_outlined, size: Theme.of(context).textTheme.bodyMedium?.fontSize, ), - LabelText( + LabelText( id: widget.document.documentType, style: Theme.of(context).textTheme.bodyMedium, placeholder: "-", @@ -247,7 +240,7 @@ class _InboxItemState extends State { Icons.person_outline, size: Theme.of(context).textTheme.bodyMedium?.fontSize, ), - LabelText( + LabelText( id: widget.document.correspondent, style: Theme.of(context).textTheme.bodyMedium, placeholder: "-", diff --git a/lib/features/labels/bloc/label_cubit.dart b/lib/features/labels/bloc/label_cubit.dart index 383900b..e1136b7 100644 --- a/lib/features/labels/bloc/label_cubit.dart +++ b/lib/features/labels/bloc/label_cubit.dart @@ -3,15 +3,14 @@ import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; -import 'package:paperless_mobile/core/repository/state/repository_state.dart'; import 'package:paperless_mobile/features/labels/bloc/label_state.dart'; class LabelCubit extends Cubit> { - final LabelRepository _repository; + final LabelRepository _repository; late StreamSubscription _subscription; - LabelCubit(LabelRepository repository) + LabelCubit(LabelRepository repository) : _repository = repository, super(LabelState( isLoaded: repository.isInitialized, @@ -22,7 +21,8 @@ class LabelCubit extends Cubit> { if (event == null) { emit(LabelState()); } - emit(LabelState(isLoaded: true, labels: event!.values)); + emit( + LabelState(isLoaded: event!.hasLoaded, labels: event.values ?? {})); }, ); } diff --git a/lib/features/labels/bloc/providers/correspondent_bloc_provider.dart b/lib/features/labels/bloc/providers/correspondent_bloc_provider.dart index 7a5e9d0..ffbf773 100644 --- a/lib/features/labels/bloc/providers/correspondent_bloc_provider.dart +++ b/lib/features/labels/bloc/providers/correspondent_bloc_provider.dart @@ -7,14 +7,16 @@ import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; class CorrespondentBlocProvider extends StatelessWidget { final Widget child; - const CorrespondentBlocProvider({super.key, required this.child}); + const CorrespondentBlocProvider({ + super.key, + required this.child, + }); @override Widget build(BuildContext context) { return BlocProvider( create: (context) => LabelCubit( - context.read< - LabelRepository>(), + context.read>(), ), child: child, ); diff --git a/lib/features/labels/bloc/providers/document_type_bloc_provider.dart b/lib/features/labels/bloc/providers/document_type_bloc_provider.dart index 3aa129f..6ebcd14 100644 --- a/lib/features/labels/bloc/providers/document_type_bloc_provider.dart +++ b/lib/features/labels/bloc/providers/document_type_bloc_provider.dart @@ -13,8 +13,7 @@ class DocumentTypeBlocProvider extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => LabelCubit( - context - .read>(), + context.read>(), ), child: child, ); diff --git a/lib/features/labels/bloc/providers/labels_bloc_provider.dart b/lib/features/labels/bloc/providers/labels_bloc_provider.dart index 1ec58c2..d4a90e2 100644 --- a/lib/features/labels/bloc/providers/labels_bloc_provider.dart +++ b/lib/features/labels/bloc/providers/labels_bloc_provider.dart @@ -18,25 +18,22 @@ class LabelsBlocProvider extends StatelessWidget { providers: [ BlocProvider>( create: (context) => LabelCubit( - context.read< - LabelRepository>(), + context.read>(), ), ), BlocProvider>( create: (context) => LabelCubit( - context.read< - LabelRepository>(), + context.read>(), ), ), BlocProvider>( create: (context) => LabelCubit( - context.read< - LabelRepository>(), + context.read>(), ), ), BlocProvider>( create: (context) => LabelCubit( - context.read>(), + context.read>(), ), ), ], diff --git a/lib/features/labels/bloc/providers/storage_path_bloc_provider.dart b/lib/features/labels/bloc/providers/storage_path_bloc_provider.dart index 1c03ee4..646c6ad 100644 --- a/lib/features/labels/bloc/providers/storage_path_bloc_provider.dart +++ b/lib/features/labels/bloc/providers/storage_path_bloc_provider.dart @@ -13,8 +13,7 @@ class StoragePathBlocProvider extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => LabelCubit( - context - .read>(), + context.read>(), ), child: child, ); diff --git a/lib/features/labels/bloc/providers/tag_bloc_provider.dart b/lib/features/labels/bloc/providers/tag_bloc_provider.dart index fc36546..4368075 100644 --- a/lib/features/labels/bloc/providers/tag_bloc_provider.dart +++ b/lib/features/labels/bloc/providers/tag_bloc_provider.dart @@ -13,7 +13,7 @@ class TagBlocProvider extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => LabelCubit( - context.read>(), + context.read>(), ), child: child, ); diff --git a/lib/features/labels/tags/view/widgets/tags_form_field.dart b/lib/features/labels/tags/view/widgets/tags_form_field.dart index 3ea2ae4..0244dad 100644 --- a/lib/features/labels/tags/view/widgets/tags_form_field.dart +++ b/lib/features/labels/tags/view/widgets/tags_form_field.dart @@ -241,8 +241,7 @@ class _TagFormFieldState extends State { final Tag? tag = await Navigator.of(context).push( MaterialPageRoute( builder: (_) => RepositoryProvider( - create: (context) => - context.read>(), + create: (context) => context.read>(), child: AddTagPage(initialValue: _textEditingController.text), ), ), diff --git a/lib/features/labels/view/pages/labels_page.dart b/lib/features/labels/view/pages/labels_page.dart index d64521c..1e5986d 100644 --- a/lib/features/labels/view/pages/labels_page.dart +++ b/lib/features/labels/view/pages/labels_page.dart @@ -82,7 +82,7 @@ class _LabelsPageState extends State context, ), sliver: SearchAppBar( - hintText: "Search documents", //TODO: INTL + hintText: S.of(context).documentSearchSearchDocuments, onOpenSearch: showDocumentSearchPage, bottom: TabBar( controller: _tabController, @@ -141,176 +141,138 @@ class _LabelsPageState extends State } return true; }, - child: MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => LabelCubit( - context.read< - LabelRepository>(), + child: RefreshIndicator( + edgeOffset: kToolbarHeight + kTextTabBarHeight, + notificationPredicate: (notification) => + connectedState.isConnected, + onRefresh: () => [ + context.read>(), + context.read>(), + context.read>(), + context.read>(), + ][_currentIndex] + .reload(), + child: TabBarView( + controller: _tabController, + children: [ + Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView + .sliverOverlapAbsorberHandleFor(context), + ), + LabelTabView( + filterBuilder: (label) => DocumentFilter( + correspondent: + IdQueryParameter.fromId(label.id), + pageSize: label.documentCount ?? 0, + ), + onEdit: _openEditCorrespondentPage, + emptyStateActionButtonLabel: S + .of(context) + .labelsPageCorrespondentEmptyStateAddNewLabel, + emptyStateDescription: S + .of(context) + .labelsPageCorrespondentEmptyStateDescriptionText, + onAddNew: _openAddCorrespondentPage, + ), + ], + ); + }, ), - ), - BlocProvider( - create: (context) => LabelCubit( - context.read< - LabelRepository>(), + Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView + .sliverOverlapAbsorberHandleFor(context), + ), + LabelTabView( + filterBuilder: (label) => DocumentFilter( + documentType: + IdQueryParameter.fromId(label.id), + pageSize: label.documentCount ?? 0, + ), + onEdit: _openEditDocumentTypePage, + emptyStateActionButtonLabel: S + .of(context) + .labelsPageDocumentTypeEmptyStateAddNewLabel, + emptyStateDescription: S + .of(context) + .labelsPageDocumentTypeEmptyStateDescriptionText, + onAddNew: _openAddDocumentTypePage, + ), + ], + ); + }, ), - ), - BlocProvider( - create: (context) => LabelCubit( - context - .read>(), + Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView + .sliverOverlapAbsorberHandleFor(context), + ), + LabelTabView( + filterBuilder: (label) => DocumentFilter( + tags: IdsTagsQuery.fromIds([label.id!]), + pageSize: label.documentCount ?? 0, + ), + onEdit: _openEditTagPage, + leadingBuilder: (t) => CircleAvatar( + backgroundColor: t.color, + child: t.isInboxTag ?? false + ? Icon( + Icons.inbox, + color: t.textColor, + ) + : null, + ), + emptyStateActionButtonLabel: S + .of(context) + .labelsPageTagsEmptyStateAddNewLabel, + emptyStateDescription: S + .of(context) + .labelsPageTagsEmptyStateDescriptionText, + onAddNew: _openAddTagPage, + ), + ], + ); + }, ), - ), - BlocProvider( - create: (context) => LabelCubit( - context.read< - LabelRepository>(), + Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView + .sliverOverlapAbsorberHandleFor(context), + ), + LabelTabView( + onEdit: _openEditStoragePathPage, + filterBuilder: (label) => DocumentFilter( + storagePath: + IdQueryParameter.fromId(label.id), + pageSize: label.documentCount ?? 0, + ), + contentBuilder: (path) => Text(path.path ?? ""), + emptyStateActionButtonLabel: S + .of(context) + .labelsPageStoragePathEmptyStateAddNewLabel, + emptyStateDescription: S + .of(context) + .labelsPageStoragePathEmptyStateDescriptionText, + onAddNew: _openAddStoragePathPage, + ), + ], + ); + }, ), - ), - ], - child: RefreshIndicator( - edgeOffset: kToolbarHeight, - notificationPredicate: (notification) => - connectedState.isConnected, - onRefresh: () => [ - context.read>(), - context.read>(), - context.read>(), - context.read>(), - ][_currentIndex] - .reload(), - child: TabBarView( - controller: _tabController, - children: [ - Builder( - builder: (context) { - return CustomScrollView( - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView - .sliverOverlapAbsorberHandleFor(context), - ), - LabelTabView( - filterBuilder: (label) => DocumentFilter( - correspondent: - IdQueryParameter.fromId(label.id), - pageSize: label.documentCount ?? 0, - ), - onEdit: _openEditCorrespondentPage, - emptyStateActionButtonLabel: S - .of(context) - .labelsPageCorrespondentEmptyStateAddNewLabel, - emptyStateDescription: S - .of(context) - .labelsPageCorrespondentEmptyStateDescriptionText, - onAddNew: _openAddCorrespondentPage, - ), - ], - ); - }, - ), - Builder( - builder: (context) { - return CustomScrollView( - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView - .sliverOverlapAbsorberHandleFor(context), - ), - DocumentTypeBlocProvider( - child: LabelTabView( - filterBuilder: (label) => DocumentFilter( - documentType: - IdQueryParameter.fromId(label.id), - pageSize: label.documentCount ?? 0, - ), - onEdit: _openEditDocumentTypePage, - emptyStateActionButtonLabel: S - .of(context) - .labelsPageDocumentTypeEmptyStateAddNewLabel, - emptyStateDescription: S - .of(context) - .labelsPageDocumentTypeEmptyStateDescriptionText, - onAddNew: _openAddDocumentTypePage, - ), - ), - ], - ); - }, - ), - Builder( - builder: (context) { - return CustomScrollView( - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView - .sliverOverlapAbsorberHandleFor(context), - ), - TagBlocProvider( - child: LabelTabView( - filterBuilder: (label) => DocumentFilter( - tags: IdsTagsQuery.fromIds([label.id!]), - pageSize: label.documentCount ?? 0, - ), - onEdit: _openEditTagPage, - leadingBuilder: (t) => CircleAvatar( - backgroundColor: t.color, - child: t.isInboxTag ?? false - ? Icon( - Icons.inbox, - color: t.textColor, - ) - : null, - ), - emptyStateActionButtonLabel: S - .of(context) - .labelsPageTagsEmptyStateAddNewLabel, - emptyStateDescription: S - .of(context) - .labelsPageTagsEmptyStateDescriptionText, - onAddNew: _openAddTagPage, - ), - ), - ], - ); - }, - ), - Builder( - builder: (context) { - return CustomScrollView( - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView - .sliverOverlapAbsorberHandleFor(context), - ), - StoragePathBlocProvider( - child: LabelTabView( - onEdit: _openEditStoragePathPage, - filterBuilder: (label) => DocumentFilter( - storagePath: - IdQueryParameter.fromId(label.id), - pageSize: label.documentCount ?? 0, - ), - contentBuilder: (path) => - Text(path.path ?? ""), - emptyStateActionButtonLabel: S - .of(context) - .labelsPageStoragePathEmptyStateAddNewLabel, - emptyStateDescription: S - .of(context) - .labelsPageStoragePathEmptyStateDescriptionText, - onAddNew: _openAddStoragePathPage, - ), - ), - ], - ); - }, - ), - ], - ), + ], ), ), ), @@ -326,8 +288,7 @@ class _LabelsPageState extends State context, MaterialPageRoute( builder: (_) => RepositoryProvider( - create: (context) => context.read< - LabelRepository>(), + create: (context) => context.read>(), child: EditCorrespondentPage(correspondent: correspondent), ), ), @@ -339,8 +300,7 @@ class _LabelsPageState extends State context, MaterialPageRoute( builder: (_) => RepositoryProvider( - create: (context) => context.read< - LabelRepository>(), + create: (context) => context.read>(), child: EditDocumentTypePage(documentType: docType), ), ), @@ -352,8 +312,7 @@ class _LabelsPageState extends State context, MaterialPageRoute( builder: (_) => RepositoryProvider( - create: (context) => - context.read>(), + create: (context) => context.read>(), child: EditTagPage(tag: tag), ), ), @@ -365,8 +324,7 @@ class _LabelsPageState extends State context, MaterialPageRoute( builder: (_) => RepositoryProvider( - create: (context) => context - .read>(), + create: (context) => context.read>(), child: EditStoragePathPage( storagePath: path, ), @@ -380,8 +338,7 @@ class _LabelsPageState extends State context, MaterialPageRoute( builder: (_) => RepositoryProvider( - create: (context) => context.read< - LabelRepository>(), + create: (context) => context.read>(), child: const AddCorrespondentPage(), ), ), @@ -393,8 +350,7 @@ class _LabelsPageState extends State context, MaterialPageRoute( builder: (_) => RepositoryProvider( - create: (context) => context.read< - LabelRepository>(), + create: (context) => context.read>(), child: const AddDocumentTypePage(), ), ), @@ -406,8 +362,7 @@ class _LabelsPageState extends State context, MaterialPageRoute( builder: (_) => RepositoryProvider( - create: (context) => - context.read>(), + create: (context) => context.read>(), child: const AddTagPage(), ), ), @@ -419,8 +374,7 @@ class _LabelsPageState extends State context, MaterialPageRoute( builder: (_) => RepositoryProvider( - create: (context) => context - .read>(), + create: (context) => context.read>(), child: const AddStoragePathPage(), ), ), diff --git a/lib/features/labels/view/widgets/label_item.dart b/lib/features/labels/view/widgets/label_item.dart index 4fc6cb0..69d7285 100644 --- a/lib/features/labels/view/widgets/label_item.dart +++ b/lib/features/labels/view/widgets/label_item.dart @@ -48,8 +48,9 @@ class LabelItem extends StatelessWidget { MaterialPageRoute( builder: (context) => BlocProvider( create: (context) => LinkedDocumentsCubit( - context.read(), filter, + context.read(), + context.read(), ), child: const LinkedDocumentsPage(), ), diff --git a/lib/features/labels/view/widgets/label_tab_view.dart b/lib/features/labels/view/widgets/label_tab_view.dart index 8aecad4..45c68c2 100644 --- a/lib/features/labels/view/widgets/label_tab_view.dart +++ b/lib/features/labels/view/widgets/label_tab_view.dart @@ -37,60 +37,65 @@ class LabelTabView extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, connectivityState) { - return BlocBuilder, LabelState>( - builder: (context, state) { - if (!state.isLoaded && !connectivityState.isConnected) { - return const OfflineWidget(); - } - final labels = state.labels.values.toList()..sort(); - if (labels.isEmpty) { - return SliverFillRemaining( - child: Center( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - emptyStateDescription, - textAlign: TextAlign.center, - ), - TextButton( - onPressed: onAddNew, - child: Text(emptyStateActionButtonLabel), - ), - ].padded(), + return BlocProvider( + create: (context) => LabelCubit( + context.read(), + ), + child: BlocBuilder( + builder: (context, connectivityState) { + return BlocBuilder, LabelState>( + builder: (context, state) { + if (!state.isLoaded && !connectivityState.isConnected) { + return const OfflineWidget(); + } + final labels = state.labels.values.toList()..sort(); + if (labels.isEmpty) { + return SliverFillRemaining( + child: Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + emptyStateDescription, + textAlign: TextAlign.center, + ), + TextButton( + onPressed: onAddNew, + child: Text(emptyStateActionButtonLabel), + ), + ].padded(), + ), ), + ); + } + return SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final l = labels.elementAt(index); + return LabelItem( + name: l.name, + content: contentBuilder?.call(l) ?? + Text( + translateMatchingAlgorithmName( + context, l.matchingAlgorithm) + + ((l.match?.isNotEmpty ?? false) + ? ": ${l.match}" + : ""), + maxLines: 2, + ), + onOpenEditPage: onEdit, + filterBuilder: filterBuilder, + leading: leadingBuilder?.call(l), + label: l, + ); + }, + childCount: labels.length, ), ); - } - return SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - final l = labels.elementAt(index); - return LabelItem( - name: l.name, - content: contentBuilder?.call(l) ?? - Text( - translateMatchingAlgorithmName( - context, l.matchingAlgorithm) + - ((l.match?.isNotEmpty ?? false) - ? ": ${l.match}" - : ""), - maxLines: 2, - ), - onOpenEditPage: onEdit, - filterBuilder: filterBuilder, - leading: leadingBuilder?.call(l), - label: l, - ); - }, - childCount: labels.length, - ), - ); - }, - ); - }, + }, + ); + }, + ), ); } } diff --git a/lib/features/labels/view/widgets/label_text.dart b/lib/features/labels/view/widgets/label_text.dart index 53b25ff..472fd58 100644 --- a/lib/features/labels/view/widgets/label_text.dart +++ b/lib/features/labels/view/widgets/label_text.dart @@ -2,13 +2,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; -import 'package:paperless_mobile/core/repository/state/repository_state.dart'; import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; import 'package:paperless_mobile/features/labels/bloc/label_state.dart'; -import 'package:paperless_mobile/features/labels/bloc/providers/document_type_bloc_provider.dart'; -class LabelText - extends StatelessWidget { +class LabelText extends StatelessWidget { final int? id; final String placeholder; final TextStyle? style; @@ -24,7 +21,7 @@ class LabelText Widget build(BuildContext context) { return BlocProvider( create: (context) => LabelCubit( - context.read>(), + context.read>(), ), child: BlocBuilder, LabelState>( builder: (context, state) { diff --git a/lib/features/linked_documents/bloc/linked_documents_cubit.dart b/lib/features/linked_documents/bloc/linked_documents_cubit.dart index bd66d05..c28b368 100644 --- a/lib/features/linked_documents/bloc/linked_documents_cubit.dart +++ b/lib/features/linked_documents/bloc/linked_documents_cubit.dart @@ -11,12 +11,27 @@ class LinkedDocumentsCubit extends Cubit @override final DocumentChangedNotifier notifier; - + LinkedDocumentsCubit( - this.api, DocumentFilter filter, + this.api, this.notifier, ) : super(const LinkedDocumentsState()) { updateFilter(filter: filter); + notifier.subscribe( + this, + onUpdated: replace, + onDeleted: remove, + ); + } + + @override + Future update(DocumentModel document) async { + final updated = await api.update(document); + if (!state.filter.matches(updated)) { + remove(document); + } else { + replace(document); + } } } diff --git a/lib/features/linked_documents/view/pages/linked_documents_page.dart b/lib/features/linked_documents/view/pages/linked_documents_page.dart index 7724a01..2a0ed87 100644 --- a/lib/features/linked_documents/view/pages/linked_documents_page.dart +++ b/lib/features/linked_documents/view/pages/linked_documents_page.dart @@ -2,11 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; -import 'package:paperless_mobile/features/documents/view/widgets/documents_list_loading_widget.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/view/widgets/adaptive_documents_view.dart'; -import 'package:paperless_mobile/features/documents/view/widgets/items/document_list_item.dart'; import 'package:paperless_mobile/features/linked_documents/bloc/linked_documents_cubit.dart'; import 'package:paperless_mobile/features/linked_documents/bloc/state/linked_documents_state.dart'; import 'package:paperless_mobile/generated/l10n.dart'; @@ -60,18 +56,15 @@ class _LinkedDocumentsPageState extends State { isLabelClickable: false, isLoading: state.isLoading, hasLoaded: state.hasLoaded, - onTap: (document) async { - final updatedDocument = await Navigator.pushNamed( + onTap: (document) { + Navigator.pushNamed( context, DocumentDetailsRoute.routeName, arguments: DocumentDetailsRouteArguments( document: document, isLabelClickable: false, ), - ) as DocumentModel?; - if (updatedDocument != document) { - context.read().reload(); - } + ); }, ); }, diff --git a/lib/features/notifications/services/local_notification_service.dart b/lib/features/notifications/services/local_notification_service.dart index a30893c..2d85814 100644 --- a/lib/features/notifications/services/local_notification_service.dart +++ b/lib/features/notifications/services/local_notification_service.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:developer'; +import 'package:flutter/foundation.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/features/notifications/services/models/notification_payloads/open_created_document_notification_payload.dart'; @@ -121,7 +122,7 @@ class LocalNotificationService { ) {} void onDidReceiveNotificationResponse(NotificationResponse response) { - log("Received Notification: ${response.payload}"); + debugPrint("Received Notification: ${response.payload}"); if (response.notificationResponseType == NotificationResponseType.selectedNotificationAction) { final action = diff --git a/lib/features/paged_document_view/paged_documents_mixin.dart b/lib/features/paged_document_view/paged_documents_mixin.dart index 22bebe1..2800502 100644 --- a/lib/features/paged_document_view/paged_documents_mixin.dart +++ b/lib/features/paged_document_view/paged_documents_mixin.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:collection/collection.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; @@ -73,14 +75,18 @@ mixin PagedDocumentsMixin try { final filter = state.filter.copyWith(page: 1); final result = await api.findAll(filter); - emit(state.copyWithPaged( - hasLoaded: true, - value: [result], - isLoading: false, - filter: filter, - )); + if (!isClosed) { + emit(state.copyWithPaged( + hasLoaded: true, + value: [result], + isLoading: false, + filter: filter, + )); + } } finally { - emit(state.copyWithPaged(isLoading: false)); + if (!isClosed) { + emit(state.copyWithPaged(isLoading: false)); + } } } @@ -88,16 +94,10 @@ mixin PagedDocumentsMixin /// Updates a document. If [shouldReload] is false, the updated document will /// replace the currently loaded one, otherwise all documents will be reloaded. /// - Future update( - DocumentModel document, { - bool shouldReload = true, - }) async { + Future update(DocumentModel document) async { final updatedDocument = await api.update(document); - if (shouldReload) { - await reload(); - } else { - replace(updatedDocument); - } + notifier.notifyUpdated(updatedDocument); + // replace(updatedDocument); } /// @@ -107,7 +107,8 @@ mixin PagedDocumentsMixin emit(state.copyWithPaged(isLoading: true)); try { await api.delete(document); - await remove(document); + notifier.notifyDeleted(document); + // remove(document); // Removing deleted now works with the change notifier. } finally { emit(state.copyWithPaged(isLoading: false)); } @@ -117,7 +118,7 @@ mixin PagedDocumentsMixin /// Removes [document] from the currently loaded state. /// Does not delete it from the server! /// - Future remove(DocumentModel document) async { + void remove(DocumentModel document) { final index = state.value.indexWhere( (page) => page.results.any((element) => element.id == document.id), ); @@ -144,23 +145,36 @@ mixin PagedDocumentsMixin /// /// Replaces the document with the same id as [document] from the currently - /// loaded state. + /// loaded state if the document's properties still match the given filter criteria, otherwise removes it. /// Future replace(DocumentModel document) async { - final index = state.value.indexWhere( + final matchesFilterCriteria = state.filter.matches(document); + if (!matchesFilterCriteria) { + return remove(document); + } + final pageIndex = state.value.indexWhere( (page) => page.results.any((element) => element.id == document.id), ); - if (index != -1) { - final foundPage = state.value[index]; + if (pageIndex != -1) { + final foundPage = state.value[pageIndex]; final replacementPage = foundPage.copyWith( - results: foundPage.results..replaceRange(index, index + 1, [document]), + results: foundPage.results + .map((doc) => doc.id == document.id ? document : doc) + .toList(), ); - emit(state.copyWithPaged( + final newState = state.copyWithPaged( value: state.value .mapIndexed((currIndex, element) => - currIndex == index ? replacementPage : element) + currIndex == pageIndex ? replacementPage : element) .toList(), - )); + ); + emit(newState); } } + + @override + Future close() { + notifier.unsubscribe(this); + return super.close(); + } } diff --git a/lib/features/saved_view/cubit/saved_view_cubit.dart b/lib/features/saved_view/cubit/saved_view_cubit.dart index 2372eb2..6ff9837 100644 --- a/lib/features/saved_view/cubit/saved_view_cubit.dart +++ b/lib/features/saved_view/cubit/saved_view_cubit.dart @@ -34,7 +34,9 @@ class SavedViewCubit extends Cubit { Future initialize() async { final views = await _repository.findAll(); final values = {for (var element in views) element.id!: element}; - emit(SavedViewState(value: values, hasLoaded: true)); + if (!isClosed) { + emit(SavedViewState(value: values, hasLoaded: true)); + } } Future reload() => initialize(); diff --git a/lib/features/saved_view/cubit/saved_view_details_cubit.dart b/lib/features/saved_view/cubit/saved_view_details_cubit.dart index 9b2dfd7..b9cec31 100644 --- a/lib/features/saved_view/cubit/saved_view_details_cubit.dart +++ b/lib/features/saved_view/cubit/saved_view_details_cubit.dart @@ -1,5 +1,6 @@ import 'package:bloc/bloc.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart'; import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart'; @@ -10,11 +11,20 @@ class SavedViewDetailsCubit extends Cubit @override final PaperlessDocumentsApi api; + @override + final DocumentChangedNotifier notifier; + final SavedView savedView; SavedViewDetailsCubit( - this.api, { + this.api, + this.notifier, { required this.savedView, }) : super(const SavedViewDetailsState()) { + notifier.subscribe( + this, + onDeleted: remove, + onUpdated: replace, + ); updateFilter(filter: savedView.toDocumentFilter()); } } diff --git a/lib/features/saved_view/view/saved_view_list.dart b/lib/features/saved_view/view/saved_view_list.dart index 2090c3d..e5af476 100644 --- a/lib/features/saved_view/view/saved_view_list.dart +++ b/lib/features/saved_view/view/saved_view_list.dart @@ -42,6 +42,7 @@ class SavedViewList extends StatelessWidget { providers: [ BlocProvider( create: (context) => SavedViewDetailsCubit( + context.read(), context.read(), savedView: view, ), diff --git a/lib/features/saved_view/view/saved_view_page.dart b/lib/features/saved_view/view/saved_view_page.dart index cd3aa0f..969f91e 100644 --- a/lib/features/saved_view/view/saved_view_page.dart +++ b/lib/features/saved_view/view/saved_view_page.dart @@ -117,18 +117,14 @@ class _SavedViewPageState extends State { ); } - void _onOpenDocumentDetails(DocumentModel document) async { - final updatedDocument = await Navigator.pushNamed( + void _onOpenDocumentDetails(DocumentModel document) { + Navigator.pushNamed( context, DocumentDetailsRoute.routeName, arguments: DocumentDetailsRouteArguments( document: document, isLabelClickable: false, ), - ) as DocumentModel?; - if (updatedDocument != document) { - // Reload in case document was edited and might not fulfill filter criteria of saved view anymore - context.read().reload(); - } + ); } } diff --git a/lib/features/scan/view/scanner_page.dart b/lib/features/scan/view/scanner_page.dart index 29a3952..3639316 100644 --- a/lib/features/scan/view/scanner_page.dart +++ b/lib/features/scan/view/scanner_page.dart @@ -12,9 +12,6 @@ import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/global/constants.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; -import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart'; -import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart'; -import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart'; import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/core/widgets/offline_banner.dart'; import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart'; @@ -46,6 +43,15 @@ class _ScannerPageState extends State with SingleTickerProviderStateMixin { @override Widget build(BuildContext context) { + final safeAreaPadding = MediaQuery.of(context).padding; + final availableHeight = MediaQuery.of(context).size.height - + 2 * kToolbarHeight - + kTextTabBarHeight - + kBottomNavigationBarHeight - + safeAreaPadding.top - + safeAreaPadding.bottom; + + print(availableHeight); return BlocBuilder( builder: (context, connectedState) { return Scaffold( @@ -61,7 +67,33 @@ class _ScannerPageState extends State // ), body: BlocBuilder>( builder: (context, state) { - return NestedScrollView( + return CustomScrollView( + physics: + state.isEmpty ? const NeverScrollableScrollPhysics() : null, + slivers: [ + SearchAppBar( + hintText: S.of(context).documentSearchSearchDocuments, + onOpenSearch: showDocumentSearchPage, + bottom: PreferredSize( + child: _buildActions(connectedState.isConnected), + preferredSize: const Size.fromHeight(kTextTabBarHeight), + ), + ), + if (state.isEmpty) + SliverToBoxAdapter( + child: SizedBox( + height: availableHeight, + child: Center( + child: _buildEmptyState(connectedState.isConnected), + ), + ), + ) + else + _buildImageGrid(state) + ], + ); + + NestedScrollView( floatHeaderSlivers: false, headerSliverBuilder: (context, innerBoxIsScrolled) => [ SearchAppBar( @@ -76,8 +108,9 @@ class _ScannerPageState extends State body: CustomScrollView( slivers: [ if (state.isEmpty) - SliverFillRemaining( - child: _buildEmptyState(connectedState.isConnected), + SliverFillViewport( + delegate: SliverChildListDelegate.fixed( + [_buildEmptyState(connectedState.isConnected)]), ) else _buildImageGrid(state) @@ -229,13 +262,11 @@ class _ScannerPageState extends State child: BlocProvider( create: (context) => DocumentUploadCubit( documentApi: context.read(), - correspondentRepository: context.read< - LabelRepository>(), - documentTypeRepository: context.read< - LabelRepository>(), - tagRepository: - context.read>(), + correspondentRepository: + context.read>(), + documentTypeRepository: + context.read>(), + tagRepository: context.read>(), ), child: DocumentUploadPreparationPage( fileBytes: file.bytes, @@ -346,14 +377,11 @@ class _ScannerPageState extends State child: BlocProvider( create: (context) => DocumentUploadCubit( documentApi: context.read(), - correspondentRepository: context.read< - LabelRepository>(), - documentTypeRepository: context.read< - LabelRepository>(), - tagRepository: - context.read>(), + correspondentRepository: + context.read>(), + documentTypeRepository: + context.read>(), + tagRepository: context.read>(), ), child: DocumentUploadPreparationPage( fileBytes: file.readAsBytesSync(), diff --git a/lib/features/settings/view/dialogs/account_settings_dialog.dart b/lib/features/settings/view/dialogs/account_settings_dialog.dart index d3ed33c..9d2aba3 100644 --- a/lib/features/settings/view/dialogs/account_settings_dialog.dart +++ b/lib/features/settings/view/dialogs/account_settings_dialog.dart @@ -6,10 +6,6 @@ import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.da import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; -import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart'; -import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart'; -import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart'; -import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart'; import 'package:paperless_mobile/core/widgets/hint_card.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart'; @@ -26,9 +22,10 @@ class AccountSettingsDialog extends StatelessWidget { scrollable: true, contentPadding: EdgeInsets.zero, title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const CloseButton(), Text(S.of(context).accountSettingsTitle), + const CloseButton(), ], ), content: BlocBuilder().logout(); await context.read().clear(); - await context.read>().clear(); - await context - .read>() - .clear(); - await context - .read>() - .clear(); - await context - .read>() - .clear(); + await context.read>().clear(); + await context.read>().clear(); + await context.read>().clear(); + await context.read>().clear(); await context.read().clear(); await HydratedBloc.storage.clear(); } on PaperlessServerException catch (error, stackTrace) { diff --git a/lib/features/similar_documents/cubit/similar_documents_cubit.dart b/lib/features/similar_documents/cubit/similar_documents_cubit.dart index dbaee29..1edb7fd 100644 --- a/lib/features/similar_documents/cubit/similar_documents_cubit.dart +++ b/lib/features/similar_documents/cubit/similar_documents_cubit.dart @@ -1,21 +1,32 @@ import 'package:bloc/bloc.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart'; import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart'; part 'similar_documents_state.dart'; class SimilarDocumentsCubit extends Cubit - with PagedDocumentsMixin { + with PagedDocumentsMixin { final int documentId; @override final PaperlessDocumentsApi api; + @override + final DocumentChangedNotifier notifier; + SimilarDocumentsCubit( - this.api, { + this.api, + this.notifier, { required this.documentId, - }) : super(const SimilarDocumentsState()); + }) : super(const SimilarDocumentsState()) { + notifier.subscribe( + this, + onDeleted: remove, + onUpdated: replace, + ); + } Future initialize() async { if (!state.hasLoaded) { diff --git a/lib/features/document_details/view/pages/similar_documents_view.dart b/lib/features/similar_documents/view/similar_documents_view.dart similarity index 77% rename from lib/features/document_details/view/pages/similar_documents_view.dart rename to lib/features/similar_documents/view/similar_documents_view.dart index 01c5fa2..0092e44 100644 --- a/lib/features/document_details/view/pages/similar_documents_view.dart +++ b/lib/features/similar_documents/view/similar_documents_view.dart @@ -2,14 +2,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; -import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart'; -import 'package:paperless_mobile/features/documents/view/widgets/documents_list_loading_widget.dart'; import 'package:paperless_mobile/core/widgets/hint_card.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart'; import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart'; import 'package:paperless_mobile/features/documents/view/widgets/items/document_list_item.dart'; import 'package:paperless_mobile/features/similar_documents/cubit/similar_documents_cubit.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; -import 'package:paperless_mobile/constants.dart'; +import 'package:paperless_mobile/routes/document_details_route.dart'; class SimilarDocumentsView extends StatefulWidget { const SimilarDocumentsView({super.key}); @@ -54,13 +53,9 @@ class _SimilarDocumentsViewState extends State { @override Widget build(BuildContext context) { - const earlyPreviewHintCard = HintCard( - hintIcon: Icons.construction, - hintText: "This view is still work in progress.", - ); return BlocBuilder( builder: (context, state) { - if (state.documents.isEmpty) { + if (state.hasLoaded && !state.isLoading && state.documents.isEmpty) { return DocumentsEmptyState( state: state, onReset: () => context.read().updateFilter( @@ -77,26 +72,23 @@ class _SimilarDocumentsViewState extends State { return CustomScrollView( controller: _scrollController, slivers: [ - const SliverToBoxAdapter(child: earlyPreviewHintCard), SliverAdaptiveDocumentsView( documents: state.documents, hasInternetConnection: connectivity.isConnected, isLabelClickable: false, isLoading: state.isLoading, hasLoaded: state.hasLoaded, - - ), - SliverList( - delegate: SliverChildBuilderDelegate( - childCount: state.documents.length, - (context, index) => DocumentListItem( - document: state.documents[index], - enableHeroAnimation: false, - isLabelClickable: false, - isSelected: false, - isSelectionActive: false, - ), - ), + enableHeroAnimation: false, + onTap: (document) { + Navigator.pushNamed( + context, + DocumentDetailsRoute.routeName, + arguments: DocumentDetailsRouteArguments( + document: document, + isLabelClickable: false, + ), + ); + }, ), ], ); diff --git a/lib/l10n/intl_cs.arb b/lib/l10n/intl_cs.arb index ddfe2d9..58842a3 100644 --- a/lib/l10n/intl_cs.arb +++ b/lib/l10n/intl_cs.arb @@ -6,6 +6,8 @@ "name": {} } }, + "accountSettingsAddAnotherAccount": "Add another account", + "@accountSettingsAddAnotherAccount": {}, "accountSettingsTitle": "Account", "@accountSettingsTitle": {}, "addCorrespondentPageTitle": "Nový korespondent", @@ -396,6 +398,10 @@ "@genericActionUploadLabel": {}, "genericMessageOfflineText": "Jste offline.", "@genericMessageOfflineText": {}, + "inboxActionAssignAsn": "Assign ASN", + "@inboxActionAssignAsn": {}, + "inboxActionDeleteDocument": "Delete document", + "@inboxActionDeleteDocument": {}, "inboxPageAssignAsnLabel": "Assign ASN", "@inboxPageAssignAsnLabel": {}, "inboxPageDocumentRemovedMessageText": "Dokument odstraněn z inboxu.", diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 6be28f4..1e29e7f 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -6,6 +6,8 @@ "name": {} } }, + "accountSettingsAddAnotherAccount": "Einen Account hinzufügen", + "@accountSettingsAddAnotherAccount": {}, "accountSettingsTitle": "Account", "@accountSettingsTitle": {}, "addCorrespondentPageTitle": "Neuer Korrespondent", @@ -396,6 +398,10 @@ "@genericActionUploadLabel": {}, "genericMessageOfflineText": "Du bist offline.", "@genericMessageOfflineText": {}, + "inboxActionAssignAsn": "ASN zuweisen", + "@inboxActionAssignAsn": {}, + "inboxActionDeleteDocument": "Dokument löschen", + "@inboxActionDeleteDocument": {}, "inboxPageAssignAsnLabel": "ASN zuweisen", "@inboxPageAssignAsnLabel": {}, "inboxPageDocumentRemovedMessageText": "Dokument aus Posteingang entfernt.", diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 8ab7807..9d5124c 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -6,6 +6,8 @@ "name": {} } }, + "accountSettingsAddAnotherAccount": "Add another account", + "@accountSettingsAddAnotherAccount": {}, "accountSettingsTitle": "Account", "@accountSettingsTitle": {}, "addCorrespondentPageTitle": "New Correspondent", @@ -396,6 +398,10 @@ "@genericActionUploadLabel": {}, "genericMessageOfflineText": "You're offline.", "@genericMessageOfflineText": {}, + "inboxActionAssignAsn": "Assign ASN", + "@inboxActionAssignAsn": {}, + "inboxActionDeleteDocument": "Delete document", + "@inboxActionDeleteDocument": {}, "inboxPageAssignAsnLabel": "Assign ASN", "@inboxPageAssignAsnLabel": {}, "inboxPageDocumentRemovedMessageText": "Document removed from inbox.", diff --git a/lib/l10n/intl_pl.arb b/lib/l10n/intl_pl.arb index d8e6a7f..6b6b654 100644 --- a/lib/l10n/intl_pl.arb +++ b/lib/l10n/intl_pl.arb @@ -6,6 +6,8 @@ "name": {} } }, + "accountSettingsAddAnotherAccount": "Add another account", + "@accountSettingsAddAnotherAccount": {}, "accountSettingsTitle": "Account", "@accountSettingsTitle": {}, "addCorrespondentPageTitle": "New Correspondent", @@ -396,6 +398,10 @@ "@genericActionUploadLabel": {}, "genericMessageOfflineText": "Jesteście w trybie offline.", "@genericMessageOfflineText": {}, + "inboxActionAssignAsn": "Assign ASN", + "@inboxActionAssignAsn": {}, + "inboxActionDeleteDocument": "Delete document", + "@inboxActionDeleteDocument": {}, "inboxPageAssignAsnLabel": "Assign ASN", "@inboxPageAssignAsnLabel": {}, "inboxPageDocumentRemovedMessageText": "Dokument usunięty ze skrzynki odbiorczej", diff --git a/lib/l10n/intl_tr.arb b/lib/l10n/intl_tr.arb index 3451880..25b7b62 100644 --- a/lib/l10n/intl_tr.arb +++ b/lib/l10n/intl_tr.arb @@ -6,6 +6,8 @@ "name": {} } }, + "accountSettingsAddAnotherAccount": "Add another account", + "@accountSettingsAddAnotherAccount": {}, "accountSettingsTitle": "Account", "@accountSettingsTitle": {}, "addCorrespondentPageTitle": "Yeni ek yazar", @@ -396,6 +398,10 @@ "@genericActionUploadLabel": {}, "genericMessageOfflineText": "Çevrimdışısınız.", "@genericMessageOfflineText": {}, + "inboxActionAssignAsn": "Assign ASN", + "@inboxActionAssignAsn": {}, + "inboxActionDeleteDocument": "Delete document", + "@inboxActionDeleteDocument": {}, "inboxPageAssignAsnLabel": "ASN ata", "@inboxPageAssignAsnLabel": {}, "inboxPageDocumentRemovedMessageText": "Döküman gelen kutusundan kaldırıldı.", diff --git a/lib/main.dart b/lib/main.dart index 4239ff2..817dc89 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -109,7 +109,7 @@ void main() async { final connectivityCubit = ConnectivityCubit(connectivityStatusService); // Remove temporarily downloaded files. - (await FileService.temporaryDirectory).deleteSync(recursive: true); + // (await FileService.temporaryDirectory).deleteSync(recursive: true); // Load application settings and stored authentication data await connectivityCubit.initialize(); @@ -173,20 +173,16 @@ void main() async { ], child: MultiRepositoryProvider( providers: [ - RepositoryProvider>.value( + RepositoryProvider>.value( value: tagRepository, ), - RepositoryProvider< - LabelRepository>.value( + RepositoryProvider>.value( value: correspondentRepository, ), - RepositoryProvider< - LabelRepository>.value( + RepositoryProvider>.value( value: documentTypeRepository, ), - RepositoryProvider< - LabelRepository>.value( + RepositoryProvider>.value( value: storagePathRepository, ), RepositoryProvider.value( diff --git a/lib/routes/document_details_route.dart b/lib/routes/document_details_route.dart index 36a0fad..8db47c3 100644 --- a/lib/routes/document_details_route.dart +++ b/lib/routes/document_details_route.dart @@ -17,8 +17,9 @@ class DocumentDetailsRoute extends StatelessWidget { return BlocProvider( create: (context) => DocumentDetailsCubit( - context.read(), - args.document, + context.read(), + context.read(), + initialDocument: args.document, ), child: LabelRepositoriesProvider( child: DocumentDetailsPage( diff --git a/packages/paperless_api/lib/src/models/document_filter.dart b/packages/paperless_api/lib/src/models/document_filter.dart index d872076..8ac6d99 100644 --- a/packages/paperless_api/lib/src/models/document_filter.dart +++ b/packages/paperless_api/lib/src/models/document_filter.dart @@ -146,7 +146,20 @@ class DocumentFilter extends Equatable { /// /// Checks whether the properties of [document] match the current filter criteria. /// - bool includes(DocumentModel document) {} + bool matches(DocumentModel document) { + return correspondent.matches(document.correspondent) && + documentType.matches(document.documentType) && + storagePath.matches(document.storagePath) && + tags.matches(document.tags) && + created.matches(document.created) && + added.matches(document.added) && + modified.matches(document.modified) && + query.matches( + title: document.title, + content: document.content, + asn: document.archiveSerialNumber, + ); + } int get appliedFiltersCount => [ documentType != initial.documentType, diff --git a/packages/paperless_api/lib/src/models/paged_search_result.dart b/packages/paperless_api/lib/src/models/paged_search_result.dart index 9beef0c..e426ed1 100644 --- a/packages/paperless_api/lib/src/models/paged_search_result.dart +++ b/packages/paperless_api/lib/src/models/paged_search_result.dart @@ -1,6 +1,5 @@ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; -import 'package:paperless_api/src/models/document_model.dart'; const pageRegex = r".*page=(\d+).*"; @@ -108,5 +107,10 @@ class PagedSearchResult extends Equatable { } @override - List get props => [count, next, previous, results]; + List get props => [ + count, + next, + previous, + results, + ]; } diff --git a/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/absolute_date_range_query.dart b/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/absolute_date_range_query.dart index 56d7ecc..73a3665 100644 --- a/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/absolute_date_range_query.dart +++ b/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/absolute_date_range_query.dart @@ -52,4 +52,17 @@ class AbsoluteDateRangeQuery extends DateRangeQuery { @override Map toJson() => _$AbsoluteDateRangeQueryToJson(this); + + @override + bool matches(DateTime dt) { + //TODO: Check if after and before are inclusive or exclusive definitions. + bool matches = true; + if (after != null) { + matches &= dt.isAfter(after!) || dt == after; + } + if (before != null) { + matches &= dt.isBefore(before!) || dt == before; + } + return matches; + } } diff --git a/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/date_range_query.dart b/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/date_range_query.dart index 5ce1fe8..7a7c6f7 100644 --- a/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/date_range_query.dart +++ b/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/date_range_query.dart @@ -7,4 +7,6 @@ abstract class DateRangeQuery extends Equatable { Map toQueryParameter(DateRangeQueryField field); Map toJson(); + + bool matches(DateTime dt); } diff --git a/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/relative_date_range_query.dart b/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/relative_date_range_query.dart index ae435b1..ef5ea56 100644 --- a/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/relative_date_range_query.dart +++ b/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/relative_date_range_query.dart @@ -1,3 +1,4 @@ +import 'package:jiffy/jiffy.dart'; import 'package:json_annotation/json_annotation.dart'; import 'date_range_query.dart'; @@ -35,9 +36,28 @@ class RelativeDateRangeQuery extends DateRangeQuery { ); } + /// Returns the datetime when subtracting the offset given the unit from now. + DateTime get dateTime { + switch (unit) { + case DateRangeUnit.day: + return Jiffy().subtract(days: offset).dateTime; + case DateRangeUnit.week: + return Jiffy().subtract(weeks: offset).dateTime; + case DateRangeUnit.month: + return Jiffy().subtract(months: offset).dateTime; + case DateRangeUnit.year: + return Jiffy().subtract(years: offset).dateTime; + } + } + @override Map toJson() => _$RelativeDateRangeQueryToJson(this); factory RelativeDateRangeQuery.fromJson(Map json) => _$RelativeDateRangeQueryFromJson(json); + + @override + bool matches(DateTime dt) { + return dt.isAfter(dateTime) || dt == dateTime; + } } diff --git a/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/unset_date_range_query.dart b/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/unset_date_range_query.dart index 055130f..1a0e0aa 100644 --- a/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/unset_date_range_query.dart +++ b/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/unset_date_range_query.dart @@ -14,4 +14,7 @@ class UnsetDateRangeQuery extends DateRangeQuery { Map toJson() { return {}; } + + @override + bool matches(DateTime dt) => true; } diff --git a/packages/paperless_api/lib/src/models/query_parameters/id_query_parameter.dart b/packages/paperless_api/lib/src/models/query_parameters/id_query_parameter.dart index 1890e2a..21e1884 100644 --- a/packages/paperless_api/lib/src/models/query_parameters/id_query_parameter.dart +++ b/packages/paperless_api/lib/src/models/query_parameters/id_query_parameter.dart @@ -1,5 +1,4 @@ import 'package:equatable/equatable.dart'; -import 'package:flutter/foundation.dart'; import 'package:json_annotation/json_annotation.dart'; part 'id_query_parameter.g.dart'; @@ -19,9 +18,7 @@ class IdQueryParameter extends Equatable { : assignmentStatus = 0, id = null; - const IdQueryParameter.fromId(int? id) - : assignmentStatus = null, - id = id; + const IdQueryParameter.fromId(this.id) : assignmentStatus = null; const IdQueryParameter.unset() : this.fromId(null); @@ -45,6 +42,13 @@ class IdQueryParameter extends Equatable { return params; } + bool matches(int? id) { + return onlyAssigned && id != null || + onlyNotAssigned && id == null || + isSet && id == this.id || + isUnset; + } + @override List get props => [assignmentStatus, id]; diff --git a/packages/paperless_api/lib/src/models/query_parameters/tags_query/any_assigned_tags_query.dart b/packages/paperless_api/lib/src/models/query_parameters/tags_query/any_assigned_tags_query.dart index adf5a25..36b1a03 100644 --- a/packages/paperless_api/lib/src/models/query_parameters/tags_query/any_assigned_tags_query.dart +++ b/packages/paperless_api/lib/src/models/query_parameters/tags_query/any_assigned_tags_query.dart @@ -27,4 +27,9 @@ class AnyAssignedTagsQuery extends TagsQuery { factory AnyAssignedTagsQuery.fromJson(Map json) => _$AnyAssignedTagsQueryFromJson(json); + + @override + bool matches(Iterable ids) { + return ids.isNotEmpty; + } } diff --git a/packages/paperless_api/lib/src/models/query_parameters/tags_query/ids_tags_query.dart b/packages/paperless_api/lib/src/models/query_parameters/tags_query/ids_tags_query.dart index 834fc14..f8802f0 100644 --- a/packages/paperless_api/lib/src/models/query_parameters/tags_query/ids_tags_query.dart +++ b/packages/paperless_api/lib/src/models/query_parameters/tags_query/ids_tags_query.dart @@ -1,5 +1,5 @@ import 'package:json_annotation/json_annotation.dart'; - +import 'package:collection/collection.dart'; import 'exclude_tag_id_query.dart'; import 'include_tag_id_query.dart'; import 'tag_id_query.dart'; @@ -85,4 +85,10 @@ class IdsTagsQuery extends TagsQuery { (json['queries'] as List).map((e) => TagIdQuery.fromJson(e)), ); } + + @override + bool matches(Iterable ids) { + return includedIds.toSet().difference(ids.toSet()).isEmpty && + excludedIds.toSet().intersection(ids.toSet()).isEmpty; + } } diff --git a/packages/paperless_api/lib/src/models/query_parameters/tags_query/only_not_assigned_tags_query.dart b/packages/paperless_api/lib/src/models/query_parameters/tags_query/only_not_assigned_tags_query.dart index 0c0d937..6d24678 100644 --- a/packages/paperless_api/lib/src/models/query_parameters/tags_query/only_not_assigned_tags_query.dart +++ b/packages/paperless_api/lib/src/models/query_parameters/tags_query/only_not_assigned_tags_query.dart @@ -14,4 +14,9 @@ class OnlyNotAssignedTagsQuery extends TagsQuery { Map toJson() { return {}; } + + @override + bool matches(Iterable ids) { + return ids.isEmpty; + } } diff --git a/packages/paperless_api/lib/src/models/query_parameters/tags_query/tags_query.dart b/packages/paperless_api/lib/src/models/query_parameters/tags_query/tags_query.dart index b9de435..984d846 100644 --- a/packages/paperless_api/lib/src/models/query_parameters/tags_query/tags_query.dart +++ b/packages/paperless_api/lib/src/models/query_parameters/tags_query/tags_query.dart @@ -4,4 +4,6 @@ abstract class TagsQuery extends Equatable { const TagsQuery(); Map toQueryParameter(); Map toJson(); + + bool matches(Iterable ids); } diff --git a/packages/paperless_api/lib/src/models/query_parameters/text_query.dart b/packages/paperless_api/lib/src/models/query_parameters/text_query.dart index 78b1123..fdebfb8 100644 --- a/packages/paperless_api/lib/src/models/query_parameters/text_query.dart +++ b/packages/paperless_api/lib/src/models/query_parameters/text_query.dart @@ -58,6 +58,26 @@ class TextQuery extends Equatable { return null; } + bool matches({ + required String title, + String? content, + int? asn, + }) { + if (queryText?.isEmpty ?? true) return true; + switch (queryType) { + case QueryType.title: + return title.contains(queryText!); + case QueryType.titleAndContent: + return title.contains(queryText!) || + (content?.contains(queryText!) ?? false); + case QueryType.extended: + //TODO: Implement. Might be too complex... + return true; + case QueryType.asn: + return int.tryParse(queryText!) == asn; + } + } + Map toJson() => _$TextQueryToJson(this); factory TextQuery.fromJson(Map json) => diff --git a/packages/paperless_api/pubspec.yaml b/packages/paperless_api/pubspec.yaml index 91162d0..396538a 100644 --- a/packages/paperless_api/pubspec.yaml +++ b/packages/paperless_api/pubspec.yaml @@ -19,6 +19,7 @@ dependencies: intl: ^0.17.0 dio: ^4.0.6 collection: ^1.17.0 + jiffy: ^5.0.0 dev_dependencies: flutter_test: diff --git a/pubspec.lock b/pubspec.lock index 7be9f14..452df54 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "8c7478991c7bbde2c1e18034ac697723176a5d3e7e0ca06c7f9aed69b6f388d7" + sha256: "0c80aeab9bc807ab10022cd3b2f4cf2ecdf231949dc1ddd9442406a003f19201" url: "https://pub.dev" source: hosted - version: "51.0.0" + version: "52.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "120fe7ce25377ba616bb210e7584983b163861f45d6ec446744d507e3943881b" + sha256: cd8ee83568a77f3ae6b913a36093a1c9b1264e7cb7f834d9ddd2311dade9c1f4 url: "https://pub.dev" source: hosted - version: "5.3.1" + version: "5.4.0" analyzer_plugin: dependency: transitive description: @@ -37,18 +37,18 @@ packages: dependency: transitive description: name: archive - sha256: "80e5141fafcb3361653ce308776cfd7d45e6e9fbb429e14eec571382c0c5fecb" + sha256: d6347d54a2d8028e0437e3c099f66fdb8ae02c4720c1e7534c9f24c10351f85d url: "https://pub.dev" source: hosted - version: "3.3.2" + version: "3.3.6" args: dependency: transitive description: name: args - sha256: b003c3098049a51720352d219b0bb5f219b60fbfb68e7a4748139a06a5676515 + sha256: "139d809800a412ebb26a3892da228b2d0ba36f0ef5d9a82166e5e52ec8d61611" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" asn1lib: dependency: transitive description: @@ -157,10 +157,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 + sha256: a3335cae313ea41f193e5637f98185e5cb37b3fde2c5c4654ac546b8164e59ac url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.0" build_runner_core: dependency: transitive description: @@ -181,10 +181,10 @@ packages: dependency: transitive description: name: built_value - sha256: "59e08b0079bb75f7e27392498e26339387c1089c6bd58525a14eb8508637277b" + sha256: "169565c8ad06adb760c3645bf71f00bff161b00002cace266cad42c5d22a7725" url: "https://pub.dev" source: hosted - version: "8.4.2" + version: "8.4.3" cached_network_image: dependency: "direct main" description: @@ -245,10 +245,10 @@ packages: dependency: "direct main" description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.1" connectivity_plus: dependency: "direct main" description: @@ -309,18 +309,18 @@ packages: dependency: transitive description: name: coverage - sha256: d2494157c32b303f47dedee955b1479f2979c4ff66934eb7c0def44fd9e0267a + sha256: "961c4aebd27917269b1896382c7cb1b1ba81629ba669ba09c27a7e5710ec9040" url: "https://pub.dev" source: hosted - version: "1.6.1" + version: "1.6.2" cross_file: dependency: transitive description: name: cross_file - sha256: f71079978789bc2fe78d79227f1f8cfe195b31bbd8db2399b0d15a4b96fb843b + sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9" url: "https://pub.dev" source: hosted - version: "0.3.3+2" + version: "0.3.3+4" crypto: dependency: transitive description: @@ -341,10 +341,10 @@ packages: dependency: "direct dev" description: name: dart_code_metrics - sha256: "95f22e95638c0dfb0cb4e3ba45e00bb06dd509c98f06d4c0fa45340b0a5392e0" + sha256: bb4ec5e729788dde5f7e8e9df4c05ec3b78532a5763e635337153ce40085514b url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "5.5.1" dart_code_metrics_presets: dependency: transitive description: @@ -453,16 +453,16 @@ packages: dependency: "direct main" description: name: dynamic_color - sha256: "37a15576f5a0bfd5555b613cf20ea3bd379607cf88d457374a16032f4e942174" + sha256: c4a508284b14ec4dda5adba2c28b2cdd34fbae1afead7e8c52cad87d51c5405b url: "https://pub.dev" source: hosted - version: "1.5.4" + version: "1.6.2" edge_detection: dependency: "direct main" description: path: "." ref: master - resolved-ref: "8c80e3a6e231985763ff501ad7ae12d76995a2e8" + resolved-ref: "24da81d7cb3bc6418d5901da355addb337793b46" url: "https://github.com/sawankumarbundelkhandi/edge_detection" source: git version: "1.1.1" @@ -526,18 +526,18 @@ packages: dependency: "direct main" description: name: file_picker - sha256: ecf52f978e72763ede54a93271318bbbca65a2be2d9ff658ec8ca4ea3a23d7ef + sha256: d090ae03df98b0247b82e5928f44d1b959867049d18d73635e2e0bc3f49542b9 url: "https://pub.dev" source: hosted - version: "5.2.4" + version: "5.2.5" fixnum: dependency: transitive description: name: fixnum - sha256: "04be3e934c52e082558cc9ee21f42f5c1cd7a1262f4c63cd0357c08d5bba81ec" + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" flutter: dependency: "direct main" description: flutter @@ -865,42 +865,50 @@ packages: dependency: "direct main" description: name: introduction_screen - sha256: "26d06cff940b9f3f1ec6591a6beea4da31183574b279c373e142ca76882ce9ea" + sha256: "73965475d6b271846f81c5fce5b459546a4ea36c285408691522437fd6bbeb69" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.4" io: dependency: transitive description: name: io - sha256: "0d4c73c3653ab85bf696d51a9657604c900a370549196a91f33e4c39af760852" + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" + jiffy: + dependency: transitive + description: + name: jiffy + sha256: "85172c4fc975a50224521c05bf43abc845288863b19d91bd3c221a96a8785dd3" + url: "https://pub.dev" + source: hosted + version: "5.0.0" js: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" json_annotation: dependency: "direct main" description: name: json_annotation - sha256: "3520fa844009431b5d4491a5a778603520cdc399ab3406332dcc50f93547258c" + sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "4.8.0" json_serializable: dependency: "direct dev" description: name: json_serializable - sha256: f3c2c18a7889580f71926f30c1937727c8c7d4f3a435f8f5e8b0ddd25253ef5d + sha256: dadc08bd61f72559f938dd08ec20dbfec6c709bba83515085ea943d2078d187a url: "https://pub.dev" source: hosted - version: "6.5.4" + version: "6.6.1" lints: dependency: transitive description: @@ -945,18 +953,18 @@ packages: dependency: transitive description: name: local_auth_windows - sha256: "53ef7487587e1cb06755861a9a74585b3b361ba1969ad374c728c75771a14fbb" + sha256: "888482e4f9ca3560e00bc227ce2badeb4857aad450c42a31c6cfc9dc21e0ccbc" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" logging: dependency: transitive description: name: logging - sha256: c0bbfe94d46aedf9b8b3e695cf3bd48c8e14b35e3b2c639e0aa7755d589ba946 + sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" matcher: dependency: transitive description: @@ -977,18 +985,18 @@ packages: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "12307e7f0605ce3da64cf0db90e5fcab0869f3ca03f76be6bb2991ce0a55e82b" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.0" mime: dependency: "direct main" description: name: mime - sha256: "52e38f7e1143ef39daf532117d6b8f8f617bf4bcd6044ed8c29040d20d269630" + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" mockito: dependency: "direct dev" description: @@ -1136,10 +1144,10 @@ packages: dependency: "direct main" description: name: path_provider - sha256: "050e8e85e4b7fecdf2bb3682c1c64c4887a183720c802d323de8a5fd76d372dd" + sha256: dcea5feb97d8abf90cab9e9030b497fb7c3cbf26b7a1fe9e3ef7dcb0a1ddec95 url: "https://pub.dev" source: hosted - version: "2.0.11" + version: "2.0.12" path_provider_android: dependency: transitive description: @@ -1148,14 +1156,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.22" - path_provider_ios: + path_provider_foundation: dependency: transitive description: - name: path_provider_ios - sha256: "03d639406f5343478352433f00d3c4394d52dac8df3d847869c5e2333e0bbce8" + name: path_provider_foundation + sha256: "62a68e7e1c6c459f9289859e2fae58290c981ce21d1697faf54910fe1faa4c74" url: "https://pub.dev" source: hosted - version: "2.0.11" + version: "2.1.1" path_provider_linux: dependency: transitive description: @@ -1164,14 +1172,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.7" - path_provider_macos: - dependency: transitive - description: - name: path_provider_macos - sha256: "2a97e7fbb7ae9dcd0dfc1220a78e9ec3e71da691912e617e8715ff2a13086ae8" - url: "https://pub.dev" - source: hosted - version: "2.0.6" path_provider_platform_interface: dependency: transitive description: @@ -1336,10 +1336,10 @@ packages: dependency: transitive description: name: pub_updater - sha256: "00e42b515aa046b171d05bbe2dd566c0feaab7808c33c5bacb5beff93cf16561" + sha256: "42890302ab2672adf567dc2b20e55b4ecc29d7e19c63b6b98143ab68dd717d3a" url: "https://pub.dev" source: hosted - version: "0.2.3" + version: "0.2.4" pubspec_parse: dependency: transitive description: @@ -1400,42 +1400,34 @@ packages: dependency: transitive description: name: shared_preferences - sha256: "76917b7d4b9526b2ba416808a7eb9fb2863c1a09cf63ec85f1453da240fa818a" + sha256: "5949029e70abe87f75cfe59d17bf5c397619c4b74a099b10116baeb34786fad9" url: "https://pub.dev" source: hosted - version: "2.0.15" + version: "2.0.17" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "8e251f3c986002b65fed6396bce81f379fb63c27317d49743cf289fd0fd1ab97" + sha256: "955e9736a12ba776bdd261cf030232b30eadfcd9c79b32a3250dd4a494e8c8f7" url: "https://pub.dev" source: hosted - version: "2.0.14" - shared_preferences_ios: + version: "2.0.15" + shared_preferences_foundation: dependency: transitive description: - name: shared_preferences_ios - sha256: "585a14cefec7da8c9c2fb8cd283a3bb726b4155c0952afe6a0caaa7b2272de34" + name: shared_preferences_foundation + sha256: "2b55c18636a4edc529fa5cd44c03d3f3100c00513f518c5127c951978efcccd0" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.3" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: fbc3cd6826896b66a5f576b025e4f344f780c84ea7f8203097a353370607a2c8 + sha256: f8ea038aa6da37090093974ebdcf4397010605fd2ff65c37a66f9d28394cb874 url: "https://pub.dev" source: hosted - version: "2.1.2" - shared_preferences_macos: - dependency: transitive - description: - name: shared_preferences_macos - sha256: fbb94bf296576f49be37a1496d5951796211a8db0aa22cc0d68c46440dad808c - url: "https://pub.dev" - source: hosted - version: "2.0.4" + version: "2.1.3" shared_preferences_platform_interface: dependency: transitive description: @@ -1456,10 +1448,10 @@ packages: dependency: transitive description: name: shared_preferences_windows - sha256: "07c274c2115d4d5e4280622abb09f0980e2c5b1fcdc98ae9f59a3bad5bfc1f26" + sha256: "5eaf05ae77658d3521d0e993ede1af962d4b326cd2153d312df716dc250f00c9" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" shelf: dependency: transitive description: @@ -1509,10 +1501,10 @@ packages: dependency: transitive description: name: source_gen - sha256: "2d79738b6bbf38a43920e2b8d189e9a3ce6cc201f4b8fc76be5e4fe377b1c38d" + sha256: c2bea18c95cfa0276a366270afaa2850b09b4a76db95d546f3d003dcc7011298 url: "https://pub.dev" source: hosted - version: "1.2.6" + version: "1.2.7" source_helper: dependency: transitive description: @@ -1549,18 +1541,18 @@ packages: dependency: transitive description: name: sqflite - sha256: "2b1697c7b78576fdc722c358f16f62171bd56e92dc13422d9e44be3fc446c276" + sha256: "78324387dc81df14f78df06019175a86a2ee0437624166c382e145d0a7fd9a4f" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.4+1" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "0c21a187d645aa65da5be6997c0c713eed61e049158870ae2de157e6897067ab" + sha256: bfd6973aaeeb93475bc0d875ac9aefddf7965ef22ce09790eb963992ffc5183f url: "https://pub.dev" source: hosted - version: "2.4.0+2" + version: "2.4.2+2" stack_trace: dependency: transitive description: @@ -1605,10 +1597,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "7b530acd9cb7c71b0019a1e7fa22c4105e675557a4400b6a401c71c5e0ade1ac" + sha256: "33b31b6beb98100bf9add464a36a8dd03eb10c7a8cf15aeec535e9b054aaf04b" url: "https://pub.dev" source: hosted - version: "3.0.0+3" + version: "3.0.1" term_glyph: dependency: transitive description: @@ -1621,26 +1613,26 @@ packages: dependency: transitive description: name: test - sha256: "98403d1090ac0aa9e33dfc8bf45cc2e0c1d5c58d7cb832cee1e50bf14f37961d" + sha256: b54d427664c00f2013ffb87797a698883c46aee9288e027a50b46eaee7486fa2 url: "https://pub.dev" source: hosted - version: "1.22.1" + version: "1.22.2" test_api: dependency: transitive description: name: test_api - sha256: c9282698e2982b6c3817037554e52f99d4daba493e8028f8112a83d68ccd0b12 + sha256: "6182294da5abf431177fccc1ee02401f6df30f766bc6130a0852c6b6d7ee6b2d" url: "https://pub.dev" source: hosted - version: "0.4.17" + version: "0.4.18" test_core: dependency: transitive description: name: test_core - sha256: c9e4661a5e6285b795d47ba27957ed8b6f980fc020e98b218e276e88aff02168 + sha256: "95ecc12692d0dd59080ab2d38d9cf32c7e9844caba23ff6cd285690398ee8ef4" url: "https://pub.dev" source: hosted - version: "0.4.21" + version: "0.4.22" timezone: dependency: transitive description: @@ -1653,10 +1645,10 @@ packages: dependency: transitive description: name: timing - sha256: c386d07d7f5efc613479a7c4d9d64b03710b03cfaa7e8ad5f2bfb295a1f0dfad + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" typed_data: dependency: transitive description: @@ -1685,42 +1677,42 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "3c92b0efb5e9dcb8f846aefabf9f0f739f91682ed486b991ceda51c288e60896" + sha256: "698fa0b4392effdc73e9e184403b627362eb5fbf904483ac9defbb1c2191d809" url: "https://pub.dev" source: hosted - version: "6.1.7" + version: "6.1.8" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "6f91d30ce9060c204b2dbe728adb300750fa4b228e8f7ed1b961aa1ceb728799" + sha256: "3e2f6dfd2c7d9cd123296cab8ef66cfc2c1a13f5845f42c7a0f365690a8a7dd1" url: "https://pub.dev" source: hosted - version: "6.0.22" + version: "6.0.23" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "6ba7dddee26c9fae27c9203c424631109d73c8fa26cfa7bc3e35e751cb87f62e" + sha256: bb328b24d3bccc20bdf1024a0990ac4f869d57663660de9c936fb8c043edefe3 url: "https://pub.dev" source: hosted - version: "6.0.17" + version: "6.0.18" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: "360fa359ab06bcb4f7c5cd3123a2a9a4d3364d4575d27c4b33468bd4497dd094" + sha256: "318c42cba924e18180c029be69caf0a1a710191b9ec49bb42b5998fdcccee3cc" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: a9b3ea9043eabfaadfa3fb89de67a11210d85569086d22b3854484beab8b3978 + sha256: "41988b55570df53b3dd2a7fc90c76756a963de6a8c5f8e113330cb35992e2094" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" url_launcher_platform_interface: dependency: transitive description: @@ -1733,18 +1725,18 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "5669882643b96bb6d5786637cac727c6e918a790053b09245fd4513b8a07df2a" + sha256: "44d79408ce9f07052095ef1f9a693c258d6373dc3944249374e30eff7219ccb0" url: "https://pub.dev" source: hosted - version: "2.0.13" + version: "2.0.14" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: e3c3b16d3104260c10eea3b0e34272aaa57921f83148b0619f74c2eced9b7ef1 + sha256: b6217370f8eb1fd85c8890c539f5a639a01ab209a36db82c921ebeacefc7a615 url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.3" uuid: dependency: "direct main" description: @@ -1765,10 +1757,10 @@ packages: dependency: transitive description: name: vm_service - sha256: e7fb6c2282f7631712b69c19d1bff82f3767eea33a2321c14fa59ad67ea391c7 + sha256: "2277c73618916ae3c2082b6df67b6ebb64b4c69d9bf23b23700707952ac30e60" url: "https://pub.dev" source: hosted - version: "9.4.0" + version: "10.1.2" watcher: dependency: transitive description: @@ -1781,18 +1773,18 @@ packages: dependency: "direct main" description: name: web_socket_channel - sha256: "3a969ddcc204a3e34e863d204b29c0752716f78b6f9cc8235083208d268a4ccd" + sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" webdriver: dependency: transitive description: name: webdriver - sha256: ef67178f0cc7e32c1494645b11639dd1335f1d18814aa8435113a92e9ef9d841 + sha256: "3c923e918918feeb90c4c9fdf1fe39220fa4c0e8e2c0fffaded174498ef86c49" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" webkit_inspection_protocol: dependency: transitive description: @@ -1813,10 +1805,10 @@ packages: dependency: transitive description: name: xdg_directories - sha256: "11541eedefbcaec9de35aa82650b695297ce668662bbd6e3911a7fabdbde589f" + sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 url: "https://pub.dev" source: hosted - version: "0.2.0+2" + version: "0.2.0+3" xml: dependency: transitive description: @@ -1834,5 +1826,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=3.0.0-35.0.dev <4.0.0" - flutter: ">=3.3.0" + dart: ">=3.0.0-134.0.dev <4.0.0" + flutter: ">=3.4.0-17.0.pre" diff --git a/pubspec.yaml b/pubspec.yaml index eea5a10..99aa8e8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -150,7 +150,6 @@ flutter: flutter_intl: enabled: true main_locale: en - localizely: project_id: 84b4144d-a628-4ba6-a8d0-4f9917444057 download_empty_as: main