From 9f6b95f506c7484dc2d34827b74d3840c4667bc1 Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Sun, 10 Dec 2023 12:48:32 +0100 Subject: [PATCH] fix: Add custom fields, translations, add app logs to login routes --- .../metadata/android/de-DE/changelogs/58.txt | 5 +- .../metadata/android/en-US/changelogs/58.txt | 5 +- fastlane/metadata | 2 +- flutter | 1 - lib/core/bloc/loading_status.dart | 6 + lib/core/bloc/my_bloc_observer.dart | 41 + lib/core/bloc/transient_error.dart | 16 + .../dio_http_error_interceptor.dart | 35 +- lib/core/repository/label_repository.dart | 154 ++- .../repository/label_repository_state.dart | 18 - .../repository/saved_view_repository.dart | 73 +- .../saved_view_repository_state.dart | 22 - lib/core/repository/user_repository.dart | 35 +- lib/core/service/file_service.dart | 37 +- .../error_code_localization_mapper.dart | 6 + lib/core/util/lambda_utils.dart | 7 + .../cubit/document_bulk_action_cubit.dart | 155 +-- .../cubit/document_bulk_action_state.dart | 23 +- .../fullscreen_bulk_edit_tags_widget.dart | 20 +- .../cubit/document_details_cubit.dart | 113 +- .../cubit/document_details_state.dart | 45 +- .../view/pages/document_details_page.dart | 114 +- .../widgets/archive_serial_number_field.dart | 16 +- .../widgets/document_meta_data_widget.dart | 2 + .../widgets/document_overview_widget.dart | 10 +- .../widgets/document_permissions_widget.dart | 19 +- .../view/document_edit_page.dart | 13 +- .../cubit/document_scanner_cubit.dart | 48 +- .../cubit/document_scanner_state.dart | 33 +- .../document_scan/view/scanner_page.dart | 13 +- .../cubit/document_upload_cubit.dart | 44 +- .../document_upload_preparation_page.dart | 10 +- .../documents/cubit/documents_cubit.dart | 16 - .../documents/cubit/documents_state.dart | 20 - .../documents/view/pages/documents_page.dart | 4 - .../date_and_document_type_widget.dart | 8 +- .../widgets/items/document_detailed_item.dart | 15 +- .../widgets/items/document_grid_item.dart | 14 +- .../widgets/items/document_list_item.dart | 15 +- .../widgets/search/document_filter_form.dart | 38 +- .../widgets/search/document_filter_panel.dart | 12 - .../sort_field_selection_bottom_sheet.dart | 15 +- .../view/widgets/sort_documents_button.dart | 4 - .../edit_label/cubit/edit_label_cubit.dart | 33 - .../edit_label/cubit/edit_label_state.dart | 11 - .../edit_label/view/add_label_page.dart | 4 +- .../edit_label/view/edit_label_page.dart | 4 +- .../view/impl/add_correspondent_page.dart | 6 +- .../view/impl/add_document_type_page.dart | 6 +- .../view/impl/add_storage_path_page.dart | 6 +- .../edit_label/view/impl/add_tag_page.dart | 7 +- .../view/impl/edit_correspondent_page.dart | 8 +- .../view/impl/edit_document_type_page.dart | 8 +- .../view/impl/edit_storage_path_page.dart | 8 +- .../edit_label/view/impl/edit_tag_page.dart | 8 +- lib/features/home/view/home_shell_widget.dart | 52 +- lib/features/inbox/cubit/inbox_cubit.dart | 12 +- lib/features/inbox/cubit/inbox_state.dart | 6 - .../inbox/view/widgets/inbox_item.dart | 11 +- lib/features/labels/cubit/label_cubit.dart | 127 +- .../labels/cubit/label_cubit_mixin.dart | 102 -- .../cubit/linked_documents_cubit.dart | 15 - .../cubit/linked_documents_state.dart | 17 - .../login/cubit/authentication_cubit.dart | 2 + lib/features/login/view/add_account_page.dart | 192 ++- .../client_certificate_form_field.dart | 32 +- .../form_fields/login_settings_page.dart | 22 + .../server_address_form_field.dart | 41 +- .../user_credentials_form_field.dart | 13 +- .../view/widgets/login_transition_page.dart | 21 +- .../saved_view/cubit/saved_view_cubit.dart | 21 +- .../cubit/saved_view_details_cubit.dart | 21 +- .../view/widgets/event_listener_shell.dart | 1 + .../cubit/similar_documents_cubit.dart | 29 +- .../cubit/similar_documents_state.dart | 7 +- .../view/similar_documents_view.dart | 12 + lib/l10n/intl_ca.arb | 13 +- lib/l10n/intl_cs.arb | 7 +- lib/l10n/intl_de.arb | 7 +- lib/l10n/intl_en.arb | 7 +- lib/l10n/intl_es.arb | 27 +- lib/l10n/intl_fr.arb | 7 +- lib/l10n/intl_nl.arb | 1028 +++++++++++++++++ lib/l10n/intl_pl.arb | 7 +- lib/l10n/intl_ru.arb | 7 +- lib/l10n/intl_tr.arb | 7 +- lib/main.dart | 12 + lib/routing/routes/documents_route.dart | 12 +- lib/routing/routes/labels_route.dart | 2 +- .../src/models/custom_field_data_type.dart | 9 + .../lib/src/models/custom_field_model.dart | 26 + .../lib/src/models/document_model.dart | 18 +- .../paperless_form_validation_exception.dart | 9 +- .../paperless_server_message_exception.dart | 3 +- .../src/models/paperless_api_exception.dart | 5 +- .../paperless_server_statistics_model.dart | 12 +- .../custom_fields/custom_fields_api.dart | 8 + .../custom_fields/custom_fields_api_impl.dart | 72 ++ .../paperless_documents_api_impl.dart | 5 +- .../labels_api/paperless_labels_api_impl.dart | 1 - .../paperless_server_stats_api_impl.dart | 7 +- pubspec.yaml | 2 +- 102 files changed, 2399 insertions(+), 1088 deletions(-) delete mode 160000 flutter create mode 100644 lib/core/bloc/loading_status.dart create mode 100644 lib/core/bloc/my_bloc_observer.dart create mode 100644 lib/core/bloc/transient_error.dart delete mode 100644 lib/core/repository/label_repository_state.dart delete mode 100644 lib/core/repository/saved_view_repository_state.dart create mode 100644 lib/core/util/lambda_utils.dart delete mode 100644 lib/features/edit_label/cubit/edit_label_cubit.dart delete mode 100644 lib/features/edit_label/cubit/edit_label_state.dart delete mode 100644 lib/features/labels/cubit/label_cubit_mixin.dart create mode 100644 lib/features/login/view/widgets/form_fields/login_settings_page.dart create mode 100644 lib/l10n/intl_nl.arb create mode 100644 packages/paperless_api/lib/src/models/custom_field_data_type.dart create mode 100644 packages/paperless_api/lib/src/models/custom_field_model.dart create mode 100644 packages/paperless_api/lib/src/modules/custom_fields/custom_fields_api.dart create mode 100644 packages/paperless_api/lib/src/modules/custom_fields/custom_fields_api_impl.dart diff --git a/android/fastlane/metadata/android/de-DE/changelogs/58.txt b/android/fastlane/metadata/android/de-DE/changelogs/58.txt index 0321b9f..1700a3e 100644 --- a/android/fastlane/metadata/android/de-DE/changelogs/58.txt +++ b/android/fastlane/metadata/android/de-DE/changelogs/58.txt @@ -1,2 +1,5 @@ * Neue Einstellung um Animationen zu deaktivieren -* Verbesserte Validierung von Server-Adressen \ No newline at end of file +* Verbesserte Validierung von Server-Adressen +* Beheben von Fehlern, durch welche es zu Problemen mit Paperless-ngx 2.x.x kam +* Weitere, kleinere Fehlerbehebungen +* Neue Übersetzungen \ No newline at end of file diff --git a/android/fastlane/metadata/android/en-US/changelogs/58.txt b/android/fastlane/metadata/android/en-US/changelogs/58.txt index 25a29d9..23ea4c9 100644 --- a/android/fastlane/metadata/android/en-US/changelogs/58.txt +++ b/android/fastlane/metadata/android/en-US/changelogs/58.txt @@ -1,2 +1,5 @@ * Add setting to disable animations -* Improved server-address validation \ No newline at end of file +* Improved server-address validation +* Fixed a bug which caused issues with newer versions of Paperless-ngx (2.x.x) +* Minor bugfixes +* Updated translations \ No newline at end of file diff --git a/fastlane/metadata b/fastlane/metadata index a4cbd11..54f33ba 100644 --- a/fastlane/metadata +++ b/fastlane/metadata @@ -1 +1 @@ -../android/fastlane/metadata \ No newline at end of file +../../android/fastlane/metadata \ No newline at end of file diff --git a/flutter b/flutter deleted file mode 160000 index d211f42..0000000 --- a/flutter +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d211f42860350d914a5ad8102f9ec32764dc6d06 diff --git a/lib/core/bloc/loading_status.dart b/lib/core/bloc/loading_status.dart new file mode 100644 index 0000000..847ce91 --- /dev/null +++ b/lib/core/bloc/loading_status.dart @@ -0,0 +1,6 @@ +enum LoadingStatus { + initial, + loading, + loaded, + error; +} diff --git a/lib/core/bloc/my_bloc_observer.dart b/lib/core/bloc/my_bloc_observer.dart new file mode 100644 index 0000000..7376813 --- /dev/null +++ b/lib/core/bloc/my_bloc_observer.dart @@ -0,0 +1,41 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_mobile/core/bloc/transient_error.dart'; +import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart'; +import 'package:paperless_mobile/features/logging/data/logger.dart'; +import 'package:paperless_mobile/helpers/message_helpers.dart'; +import 'package:paperless_mobile/routing/navigation_keys.dart'; + +class MyBlocObserver extends BlocObserver { + @override + void onError(BlocBase bloc, Object error, StackTrace stackTrace) { + if (error is TransientError) { + _handleTransientError(bloc, error, stackTrace); + } + super.onError(bloc, error, stackTrace); + } + + void _handleTransientError( + BlocBase bloc, + TransientError error, + StackTrace stackTrace, + ) { + assert(rootNavigatorKey.currentContext != null); + final message = switch (error) { + TransientPaperlessApiError(code: var code) => translateError( + rootNavigatorKey.currentContext!, + code, + ), + TransientMessageError(message: var message) => message, + }; + final details = switch (error) { + TransientPaperlessApiError(details: var details) => details, + _ => null, + }; + + showSnackBar( + rootNavigatorKey.currentContext!, + message, + details: details, + ); + } +} diff --git a/lib/core/bloc/transient_error.dart b/lib/core/bloc/transient_error.dart new file mode 100644 index 0000000..a40e106 --- /dev/null +++ b/lib/core/bloc/transient_error.dart @@ -0,0 +1,16 @@ +import 'package:paperless_api/paperless_api.dart'; + +sealed class TransientError extends Error {} + +class TransientPaperlessApiError extends TransientError { + final ErrorCode code; + final String? details; + + TransientPaperlessApiError({required this.code, this.details}); +} + +class TransientMessageError extends TransientError { + final String message; + + TransientMessageError({required this.message}); +} diff --git a/lib/core/interceptor/dio_http_error_interceptor.dart b/lib/core/interceptor/dio_http_error_interceptor.dart index fd5ea63..ec302a1 100644 --- a/lib/core/interceptor/dio_http_error_interceptor.dart +++ b/lib/core/interceptor/dio_http_error_interceptor.dart @@ -28,16 +28,31 @@ class DioHttpErrorInterceptor extends Interceptor { type: DioExceptionType.badResponse, ), ); - } else if (data is String && - data.contains("No required SSL certificate was sent")) { - handler.reject( - DioException( - requestOptions: err.requestOptions, - type: DioExceptionType.badResponse, - error: - const PaperlessApiException(ErrorCode.missingClientCertificate), - ), - ); + } else if (data is String) { + if (data.contains("No required SSL certificate was sent")) { + handler.reject( + DioException( + requestOptions: err.requestOptions, + type: DioExceptionType.badResponse, + error: const PaperlessApiException( + ErrorCode.missingClientCertificate), + ), + ); + } else { + handler.reject( + DioException( + requestOptions: err.requestOptions, + message: data, + error: PaperlessApiException( + ErrorCode.documentLoadFailed, + details: data, + ), + response: err.response, + stackTrace: err.stackTrace, + type: DioExceptionType.badResponse, + ), + ); + } } else { handler.reject(err); } diff --git a/lib/core/repository/label_repository.dart b/lib/core/repository/label_repository.dart index 7c2d3ef..8456454 100644 --- a/lib/core/repository/label_repository.dart +++ b/lib/core/repository/label_repository.dart @@ -1,192 +1,190 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/repository/label_repository_state.dart'; -import 'package:paperless_mobile/core/repository/persistent_repository.dart'; -class LabelRepository extends PersistentRepository { +class LabelRepository extends ChangeNotifier { final PaperlessLabelsApi _api; - LabelRepository(this._api) : super(const LabelRepositoryState()); + Map correspondents = {}; + Map documentTypes = {}; + Map storagePaths = {}; + Map tags = {}; - Future initialize() async { + LabelRepository(this._api); + + // Resets the repository to its initial state and loads all data from the API. + Future initialize({ + required bool loadCorrespondents, + required bool loadDocumentTypes, + required bool loadStoragePaths, + required bool loadTags, + }) async { + correspondents = {}; + documentTypes = {}; + storagePaths = {}; + tags = {}; await Future.wait([ - findAllCorrespondents(), - findAllDocumentTypes(), - findAllStoragePaths(), - findAllTags(), + if (loadCorrespondents) findAllCorrespondents(), + if (loadDocumentTypes) findAllDocumentTypes(), + if (loadStoragePaths) findAllStoragePaths(), + if (loadTags) findAllTags(), ]); } Future createTag(Tag object) async { final created = await _api.saveTag(object); - final updatedState = {...state.tags} - ..putIfAbsent(created.id!, () => created); - emit(state.copyWith(tags: updatedState)); + tags = {...tags, created.id!: created}; + notifyListeners(); return created; } Future deleteTag(Tag tag) async { await _api.deleteTag(tag); - final updatedState = {...state.tags}..removeWhere((k, v) => k == tag.id); - emit(state.copyWith(tags: updatedState)); + tags.remove(tag.id!); + notifyListeners(); return tag.id!; } Future findTag(int id) async { final tag = await _api.getTag(id); if (tag != null) { - final updatedState = {...state.tags}..[id] = tag; - emit(state.copyWith(tags: updatedState)); + tags = {...tags, id: tag}; + notifyListeners(); return tag; } return null; } Future> findAllTags([Iterable? ids]) async { - final tags = await _api.getTags(ids); - final updatedState = {...state.tags} - ..addEntries(tags.map((e) => MapEntry(e.id!, e))); - emit(state.copyWith(tags: updatedState)); - return tags; + final data = await _api.getTags(ids); + tags = {for (var tag in data) tag.id!: tag}; + notifyListeners(); + return data; } Future updateTag(Tag tag) async { final updated = await _api.updateTag(tag); - final updatedState = {...state.tags}..update(updated.id!, (_) => updated); - emit(state.copyWith(tags: updatedState)); + tags = {...tags, updated.id!: updated}; + notifyListeners(); return updated; } Future createCorrespondent(Correspondent correspondent) async { final created = await _api.saveCorrespondent(correspondent); - final updatedState = {...state.correspondents} - ..putIfAbsent(created.id!, () => created); - emit(state.copyWith(correspondents: updatedState)); + correspondents = {...correspondents, created.id!: created}; + notifyListeners(); return created; } Future deleteCorrespondent(Correspondent correspondent) async { await _api.deleteCorrespondent(correspondent); - final updatedState = {...state.correspondents} - ..removeWhere((k, v) => k == correspondent.id); - emit(state.copyWith(correspondents: updatedState)); - + correspondents.remove(correspondent.id!); + notifyListeners(); return correspondent.id!; } Future findCorrespondent(int id) async { final correspondent = await _api.getCorrespondent(id); if (correspondent != null) { - final updatedState = {...state.correspondents}..[id] = correspondent; - emit(state.copyWith(correspondents: updatedState)); + correspondents = {...correspondents, id: correspondent}; + notifyListeners(); return correspondent; } return null; } - Future> findAllCorrespondents( - [Iterable? ids]) async { - final correspondents = await _api.getCorrespondents(ids); - final updatedState = { - ...state.correspondents, - }..addAll({for (var element in correspondents) element.id!: element}); - emit(state.copyWith(correspondents: updatedState)); - return correspondents; + Future> findAllCorrespondents() async { + final data = await _api.getCorrespondents(); + correspondents = {for (var element in data) element.id!: element}; + notifyListeners(); + return data; } Future updateCorrespondent(Correspondent correspondent) async { final updated = await _api.updateCorrespondent(correspondent); - final updatedState = {...state.correspondents} - ..update(updated.id!, (_) => updated); - emit(state.copyWith(correspondents: updatedState)); - + correspondents = {...correspondents, updated.id!: updated}; + notifyListeners(); return updated; } Future createDocumentType(DocumentType documentType) async { final created = await _api.saveDocumentType(documentType); - final updatedState = {...state.documentTypes} - ..putIfAbsent(created.id!, () => created); - emit(state.copyWith(documentTypes: updatedState)); + documentTypes = {...documentTypes, created.id!: created}; + notifyListeners(); return created; } Future deleteDocumentType(DocumentType documentType) async { await _api.deleteDocumentType(documentType); - final updatedState = {...state.documentTypes} - ..removeWhere((k, v) => k == documentType.id); - emit(state.copyWith(documentTypes: updatedState)); + documentTypes.remove(documentType.id!); + notifyListeners(); return documentType.id!; } Future findDocumentType(int id) async { final documentType = await _api.getDocumentType(id); if (documentType != null) { - final updatedState = {...state.documentTypes}..[id] = documentType; - emit(state.copyWith(documentTypes: updatedState)); + documentTypes = {...documentTypes, id: documentType}; + notifyListeners(); return documentType; } return null; } - Future> findAllDocumentTypes( - [Iterable? ids]) async { - final documentTypes = await _api.getDocumentTypes(ids); - final updatedState = {...state.documentTypes} - ..addEntries(documentTypes.map((e) => MapEntry(e.id!, e))); - emit(state.copyWith(documentTypes: updatedState)); + Future> findAllDocumentTypes() async { + final documentTypes = await _api.getDocumentTypes(); + this.documentTypes = { + for (var dt in documentTypes) dt.id!: dt, + }; + notifyListeners(); return documentTypes; } Future updateDocumentType(DocumentType documentType) async { final updated = await _api.updateDocumentType(documentType); - final updatedState = {...state.documentTypes} - ..update(updated.id!, (_) => updated); - emit(state.copyWith(documentTypes: updatedState)); + documentTypes = {...documentTypes, updated.id!: updated}; + notifyListeners(); return updated; } Future createStoragePath(StoragePath storagePath) async { final created = await _api.saveStoragePath(storagePath); - final updatedState = {...state.storagePaths} - ..putIfAbsent(created.id!, () => created); - emit(state.copyWith(storagePaths: updatedState)); + storagePaths = {...storagePaths, created.id!: created}; + notifyListeners(); return created; } Future deleteStoragePath(StoragePath storagePath) async { await _api.deleteStoragePath(storagePath); - final updatedState = {...state.storagePaths} - ..removeWhere((k, v) => k == storagePath.id); - emit(state.copyWith(storagePaths: updatedState)); + storagePaths.remove(storagePath.id!); + notifyListeners(); return storagePath.id!; } Future findStoragePath(int id) async { final storagePath = await _api.getStoragePath(id); if (storagePath != null) { - final updatedState = {...state.storagePaths}..[id] = storagePath; - emit(state.copyWith(storagePaths: updatedState)); + storagePaths = {...storagePaths, id: storagePath}; + notifyListeners(); return storagePath; } return null; } - Future> findAllStoragePaths( - [Iterable? ids]) async { - final storagePaths = await _api.getStoragePaths(ids); - final updatedState = {...state.storagePaths} - ..addEntries(storagePaths.map((e) => MapEntry(e.id!, e))); - emit(state.copyWith(storagePaths: updatedState)); + Future> findAllStoragePaths() async { + final storagePaths = await _api.getStoragePaths(); + this.storagePaths = { + for (var sp in storagePaths) sp.id!: sp, + }; + notifyListeners(); return storagePaths; } Future updateStoragePath(StoragePath storagePath) async { final updated = await _api.updateStoragePath(storagePath); - final updatedState = {...state.storagePaths} - ..update(updated.id!, (_) => updated); - emit(state.copyWith(storagePaths: updatedState)); + storagePaths = {...storagePaths, updated.id!: updated}; + notifyListeners(); return updated; } diff --git a/lib/core/repository/label_repository_state.dart b/lib/core/repository/label_repository_state.dart deleted file mode 100644 index 26e7d87..0000000 --- a/lib/core/repository/label_repository_state.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:paperless_api/paperless_api.dart'; - -part 'label_repository_state.freezed.dart'; -part 'label_repository_state.g.dart'; - -@freezed -class LabelRepositoryState with _$LabelRepositoryState { - const factory LabelRepositoryState({ - @Default({}) Map correspondents, - @Default({}) Map documentTypes, - @Default({}) Map tags, - @Default({}) Map storagePaths, - }) = _LabelRepositoryState; - - factory LabelRepositoryState.fromJson(Map json) => - _$LabelRepositoryStateFromJson(json); -} diff --git a/lib/core/repository/saved_view_repository.dart b/lib/core/repository/saved_view_repository.dart index ebc64ed..6045512 100644 --- a/lib/core/repository/saved_view_repository.dart +++ b/lib/core/repository/saved_view_repository.dart @@ -1,95 +1,54 @@ import 'dart:async'; -import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:flutter/cupertino.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/repository/persistent_repository.dart'; -part 'saved_view_repository_state.dart'; -part 'saved_view_repository.g.dart'; -part 'saved_view_repository.freezed.dart'; - -class SavedViewRepository - extends PersistentRepository { +class SavedViewRepository extends ChangeNotifier { final PaperlessSavedViewsApi _api; - final Completer _initialized = Completer(); + Map savedViews = {}; - SavedViewRepository(this._api) - : super(const SavedViewRepositoryState.initial()); + SavedViewRepository(this._api); Future initialize() async { - try { - await findAll(); - _initialized.complete(); - } catch (e) { - _initialized.completeError(e); - emit(const SavedViewRepositoryState.error()); - } + await findAll(); } Future create(SavedView object) async { - await _initialized.future; final created = await _api.save(object); - final updatedState = {...state.savedViews} - ..putIfAbsent(created.id!, () => created); - emit(SavedViewRepositoryState.loaded(savedViews: updatedState)); + savedViews = {...savedViews, created.id!: created}; + notifyListeners(); return created; } Future update(SavedView object) async { - await _initialized.future; final updated = await _api.update(object); - final updatedState = {...state.savedViews}..update( - updated.id!, - (_) => updated, - ifAbsent: () => updated, - ); - emit(SavedViewRepositoryState.loaded(savedViews: updatedState)); + savedViews = {...savedViews, updated.id!: updated}; + notifyListeners(); return updated; } Future delete(SavedView view) async { - await _initialized.future; await _api.delete(view); - final updatedState = {...state.savedViews}..remove(view.id); - emit(SavedViewRepositoryState.loaded(savedViews: updatedState)); + savedViews.remove(view.id!); + notifyListeners(); return view.id!; } Future find(int id) async { - await _initialized.future; final found = await _api.find(id); if (found != null) { - final updatedState = {...state.savedViews} - ..update(id, (_) => found, ifAbsent: () => found); - emit(SavedViewRepositoryState.loaded(savedViews: updatedState)); + savedViews = {...savedViews, id: found}; + notifyListeners(); } return found; } Future> findAll([Iterable? ids]) async { final found = await _api.findAll(ids); - final updatedState = { - ...state.savedViews, - ...{for (final view in found) view.id!: view}, + savedViews = { + for (final view in found) view.id!: view, }; - emit(SavedViewRepositoryState.loaded(savedViews: updatedState)); + notifyListeners(); return found; } - - // @override - // Future clear() async { - // await _initialized.future; - // await super.clear(); - // emit(const SavedViewRepositoryState.initial()); - // } - - // @override - // SavedViewRepositoryState? fromJson(Map json) { - // return SavedViewRepositoryState.fromJson(json); - // } - - // @override - // Map? toJson(SavedViewRepositoryState state) { - // return state.toJson(); - // } } diff --git a/lib/core/repository/saved_view_repository_state.dart b/lib/core/repository/saved_view_repository_state.dart deleted file mode 100644 index 2498991..0000000 --- a/lib/core/repository/saved_view_repository_state.dart +++ /dev/null @@ -1,22 +0,0 @@ -part of 'saved_view_repository.dart'; - - - -@freezed -class SavedViewRepositoryState with _$SavedViewRepositoryState { - const factory SavedViewRepositoryState.initial({ - @Default({}) Map savedViews, - }) = _Initial; - const factory SavedViewRepositoryState.loading({ - @Default({}) Map savedViews, - }) = _Loading; - const factory SavedViewRepositoryState.loaded({ - @Default({}) Map savedViews, - }) = _Loaded; - const factory SavedViewRepositoryState.error({ - @Default({}) Map savedViews, - }) = _Error; - - factory SavedViewRepositoryState.fromJson(Map json) => - _$SavedViewRepositoryStateFromJson(json); -} diff --git a/lib/core/repository/user_repository.dart b/lib/core/repository/user_repository.dart index 990c85e..0d2cbce 100644 --- a/lib/core/repository/user_repository.dart +++ b/lib/core/repository/user_repository.dart @@ -1,30 +1,45 @@ import 'package:equatable/equatable.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/persistent_repository.dart'; +import 'package:paperless_mobile/features/logging/data/logger.dart'; part 'user_repository_state.dart'; -/// Repository for new users (API v3, server version 1.14.2+) class UserRepository extends PersistentRepository { - final PaperlessUserApiV3 _userApiV3; + final PaperlessUserApi _userApi; - UserRepository(this._userApiV3) : super(const UserRepositoryState()); + UserRepository(this._userApi) : super(const UserRepositoryState()); Future initialize() async { await findAll(); } Future> findAll() async { - final users = await _userApiV3.findAll(); - emit(state.copyWith(users: {for (var e in users) e.id: e})); - return users; + if (_userApi is PaperlessUserApiV3Impl) { + final users = await (_userApi as PaperlessUserApiV3Impl).findAll(); + emit(state.copyWith(users: {for (var e in users) e.id: e})); + return users; + } + logger.fw( + "Tried to access API v3 features while using an older API version.", + className: 'UserRepository', + methodName: 'findAll', + ); + return []; } Future find(int id) async { - final user = await _userApiV3.find(id); - emit(state.copyWith(users: state.users..[id] = user)); - return user; + if (_userApi is PaperlessUserApiV3Impl) { + final user = await (_userApi as PaperlessUserApiV3Impl).find(id); + emit(state.copyWith(users: state.users..[id] = user)); + return user; + } + logger.fw( + "Tried to access API v3 features while using an older API version.", + className: 'UserRepository', + methodName: 'findAll', + ); + return null; } // @override diff --git a/lib/core/service/file_service.dart b/lib/core/service/file_service.dart index 2dfa6a3..7a944bb 100644 --- a/lib/core/service/file_service.dart +++ b/lib/core/service/file_service.dart @@ -14,12 +14,12 @@ class FileService { static FileService? _singleton; - late final Directory _logDirectory; - late final Directory _temporaryDirectory; - late final Directory _documentsDirectory; - late final Directory _downloadsDirectory; - late final Directory _uploadDirectory; - late final Directory _temporaryScansDirectory; + late Directory _logDirectory; + late Directory _temporaryDirectory; + late Directory _documentsDirectory; + late Directory _downloadsDirectory; + late Directory _uploadDirectory; + late Directory _temporaryScansDirectory; Directory get logDirectory => _logDirectory; Directory get temporaryDirectory => _temporaryDirectory; @@ -186,14 +186,15 @@ class FileService { } Future _initTemporaryDirectory() async { - _temporaryDirectory = await getTemporaryDirectory(); + _temporaryDirectory = + await getTemporaryDirectory().then((value) => value.create()); } Future _initializeDocumentsDirectory() async { if (Platform.isAndroid) { final dirs = await getExternalStorageDirectories(type: StorageDirectory.documents); - _documentsDirectory = dirs!.first; + _documentsDirectory = await dirs!.first.create(recursive: true); return; } else if (Platform.isIOS) { final dir = await getApplicationDocumentsDirectory(); @@ -212,12 +213,12 @@ class FileService { .then((directory) async => directory?.firstOrNull ?? await getApplicationDocumentsDirectory()) - .then((directory) => - Directory('${directory.path}/logs').create(recursive: true)); + .then((directory) => Directory(p.join(directory.path, 'logs')) + .create(recursive: true)); return; } else if (Platform.isIOS) { - _logDirectory = await getApplicationDocumentsDirectory().then( - (value) => Directory('${value.path}/logs').create(recursive: true)); + _logDirectory = await getApplicationDocumentsDirectory().then((value) => + Directory(p.join(value.path, 'logs')).create(recursive: true)); return; } throw UnsupportedError("Platform not supported."); @@ -246,7 +247,7 @@ class FileService { Future _initUploadDirectory() async { final dir = await getApplicationDocumentsDirectory() - .then((dir) => Directory('${dir.path}/upload')); + .then((dir) => Directory(p.join(dir.path, 'upload'))); _uploadDirectory = await dir.create(recursive: true); } @@ -265,3 +266,13 @@ enum PaperlessDirectoryType { upload, logs; } + +extension ClearDirectoryExtension on Directory { + Future clear() async { + final streamedEntities = list(); + final entities = await streamedEntities.toList(); + await Future.wait([ + for (var entity in entities) entity.delete(recursive: true), + ]); + } +} diff --git a/lib/core/translation/error_code_localization_mapper.dart b/lib/core/translation/error_code_localization_mapper.dart index ac1204e..499c08e 100644 --- a/lib/core/translation/error_code_localization_mapper.dart +++ b/lib/core/translation/error_code_localization_mapper.dart @@ -76,5 +76,11 @@ String translateError(BuildContext context, ErrorCode code) { ErrorCode.userNotFound => S.of(context)!.userNotFound, ErrorCode.updateSavedViewError => S.of(context)!.couldNotUpdateSavedView, ErrorCode.userAlreadyExists => S.of(context)!.userAlreadyExists, + ErrorCode.customFieldCreateFailed => + 'Could not create custom field, please try again.', //TODO: INTL + ErrorCode.customFieldLoadFailed => + 'Could not load custom field.', //TODO: INTL + ErrorCode.customFieldDeleteFailed => + 'Could not delete custom field, please try again.', //TODO: INTL }; } diff --git a/lib/core/util/lambda_utils.dart b/lib/core/util/lambda_utils.dart new file mode 100644 index 0000000..f288525 --- /dev/null +++ b/lib/core/util/lambda_utils.dart @@ -0,0 +1,7 @@ +bool isNotNull(T element) { + return element != null; +} + +bool isNull(T element) { + return element == null; +} diff --git a/lib/features/document_bulk_action/cubit/document_bulk_action_cubit.dart b/lib/features/document_bulk_action/cubit/document_bulk_action_cubit.dart index 6dc5679..3091597 100644 --- a/lib/features/document_bulk_action/cubit/document_bulk_action_cubit.dart +++ b/lib/features/document_bulk_action/cubit/document_bulk_action_cubit.dart @@ -3,30 +3,23 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:collection/collection.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/bloc/transient_error.dart'; import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; part 'document_bulk_action_state.dart'; -part 'document_bulk_action_cubit.freezed.dart'; class DocumentBulkActionCubit extends Cubit { final PaperlessDocumentsApi _documentsApi; - final LabelRepository _labelRepository; final DocumentChangedNotifier _notifier; DocumentBulkActionCubit( this._documentsApi, - this._labelRepository, this._notifier, { required List selection, }) : super( DocumentBulkActionState( selection: selection, - correspondents: _labelRepository.state.correspondents, - documentTypes: _labelRepository.state.documentTypes, - storagePaths: _labelRepository.state.storagePaths, - tags: _labelRepository.state.tags, ), ) { _notifier.addListener( @@ -42,19 +35,6 @@ class DocumentBulkActionCubit extends Cubit { ); }, ); - _labelRepository.addListener( - this, - onChanged: (labels) { - emit( - state.copyWith( - correspondents: labels.correspondents, - documentTypes: labels.documentTypes, - storagePaths: labels.storagePaths, - tags: labels.tags, - ), - ); - }, - ); } Future bulkDelete() async { @@ -69,47 +49,74 @@ class DocumentBulkActionCubit extends Cubit { } Future bulkModifyCorrespondent(int? correspondentId) async { - final modifiedDocumentIds = await _documentsApi.bulkAction( - BulkModifyLabelAction.correspondent( - state.selectedIds, - labelId: correspondentId, - ), - ); - final updatedDocuments = state.selection - .where((element) => modifiedDocumentIds.contains(element.id)) - .map((doc) => doc.copyWith(correspondent: () => correspondentId)); - for (final doc in updatedDocuments) { - _notifier.notifyUpdated(doc); + try { + final modifiedDocumentIds = await _documentsApi.bulkAction( + BulkModifyLabelAction.correspondent( + state.selectedIds, + labelId: correspondentId, + ), + ); + final updatedDocuments = state.selection + .where((element) => modifiedDocumentIds.contains(element.id)) + .map((doc) => doc.copyWith(correspondent: () => correspondentId)); + for (final doc in updatedDocuments) { + _notifier.notifyUpdated(doc); + } + } on PaperlessApiException catch (e) { + addError( + TransientPaperlessApiError( + code: e.code, + details: e.details, + ), + ); } } Future bulkModifyDocumentType(int? documentTypeId) async { - final modifiedDocumentIds = await _documentsApi.bulkAction( - BulkModifyLabelAction.documentType( - state.selectedIds, - labelId: documentTypeId, - ), - ); - final updatedDocuments = state.selection - .where((element) => modifiedDocumentIds.contains(element.id)) - .map((doc) => doc.copyWith(documentType: () => documentTypeId)); - for (final doc in updatedDocuments) { - _notifier.notifyUpdated(doc); + try { + final modifiedDocumentIds = await _documentsApi.bulkAction( + BulkModifyLabelAction.documentType( + state.selectedIds, + labelId: documentTypeId, + ), + ); + final updatedDocuments = state.selection + .where((element) => modifiedDocumentIds.contains(element.id)) + .map((doc) => doc.copyWith(documentType: () => documentTypeId)); + for (final doc in updatedDocuments) { + _notifier.notifyUpdated(doc); + } + } on PaperlessApiException catch (e) { + addError( + TransientPaperlessApiError( + code: e.code, + details: e.details, + ), + ); } } Future bulkModifyStoragePath(int? storagePathId) async { - final modifiedDocumentIds = await _documentsApi.bulkAction( - BulkModifyLabelAction.storagePath( - state.selectedIds, - labelId: storagePathId, - ), - ); - final updatedDocuments = state.selection - .where((element) => modifiedDocumentIds.contains(element.id)) - .map((doc) => doc.copyWith(storagePath: () => storagePathId)); - for (final doc in updatedDocuments) { - _notifier.notifyUpdated(doc); + try { + final modifiedDocumentIds = await _documentsApi.bulkAction( + BulkModifyLabelAction.storagePath( + state.selectedIds, + labelId: storagePathId, + ), + ); + final updatedDocuments = state.selection + .where((element) => modifiedDocumentIds.contains(element.id)) + .map((doc) => doc.copyWith(storagePath: () => storagePathId)); + for (final doc in updatedDocuments) { + _notifier.notifyUpdated(doc); + } + } on PaperlessApiException catch (e) { + addError( + TransientPaperlessApiError( + code: e.code, + details: e.details, + ), + ); } } @@ -117,28 +124,36 @@ class DocumentBulkActionCubit extends Cubit { Iterable addTagIds = const [], Iterable removeTagIds = const [], }) async { - final modifiedDocumentIds = await _documentsApi.bulkAction( - BulkModifyTagsAction( - state.selectedIds, - addTags: addTagIds, - removeTags: removeTagIds, - ), - ); - final updatedDocuments = state.selection - .where((element) => modifiedDocumentIds.contains(element.id)) - .map((doc) => doc.copyWith(tags: [ - ...doc.tags.toSet().difference(removeTagIds.toSet()), - ...addTagIds - ])); - for (final doc in updatedDocuments) { - _notifier.notifyUpdated(doc); + try { + final modifiedDocumentIds = await _documentsApi.bulkAction( + BulkModifyTagsAction( + state.selectedIds, + addTags: addTagIds, + removeTags: removeTagIds, + ), + ); + final updatedDocuments = state.selection + .where((element) => modifiedDocumentIds.contains(element.id)) + .map((doc) => doc.copyWith(tags: [ + ...doc.tags.toSet().difference(removeTagIds.toSet()), + ...addTagIds + ])); + for (final doc in updatedDocuments) { + _notifier.notifyUpdated(doc); + } + } on PaperlessApiException catch (e) { + addError( + TransientPaperlessApiError( + code: e.code, + details: e.details, + ), + ); } } @override Future close() { _notifier.removeListener(this); - _labelRepository.removeListener(this); return super.close(); } } diff --git a/lib/features/document_bulk_action/cubit/document_bulk_action_state.dart b/lib/features/document_bulk_action/cubit/document_bulk_action_state.dart index 11b77c6..4898a9e 100644 --- a/lib/features/document_bulk_action/cubit/document_bulk_action_state.dart +++ b/lib/features/document_bulk_action/cubit/document_bulk_action_state.dart @@ -1,15 +1,18 @@ part of 'document_bulk_action_cubit.dart'; -@freezed -class DocumentBulkActionState with _$DocumentBulkActionState { - const DocumentBulkActionState._(); - const factory DocumentBulkActionState({ - required List selection, - required Map correspondents, - required Map documentTypes, - required Map tags, - required Map storagePaths, - }) = _DocumentBulkActionState; +class DocumentBulkActionState { + final List selection; + + DocumentBulkActionState({ + required this.selection, + }); Iterable get selectedIds => selection.map((d) => d.id); + DocumentBulkActionState copyWith({ + List? selection, + }) { + return DocumentBulkActionState( + selection: selection ?? this.selection, + ); + } } diff --git a/lib/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_tags_widget.dart b/lib/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_tags_widget.dart index 52dc103..80d28a9 100644 --- a/lib/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_tags_widget.dart +++ b/lib/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_tags_widget.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/widgets/form_fields/fullscreen_selection_form.dart'; import 'package:paperless_mobile/core/extensions/dart_extensions.dart'; import 'package:paperless_mobile/features/document_bulk_action/cubit/document_bulk_action_cubit.dart'; @@ -35,11 +36,12 @@ class _FullscreenBulkEditTagsWidgetState void initState() { super.initState(); final state = context.read().state; + final labels = context.read(); _sharedTags = state.selection .map((e) => e.tags) .map((e) => e.toSet()) .fold( - state.tags.values.map((e) => e.id!).toSet(), + labels.tags.values.map((e) => e.id!).toSet(), (previousValue, element) => previousValue.intersection(element), ) .toList(); @@ -49,14 +51,10 @@ class _FullscreenBulkEditTagsWidgetState .toSet() .difference(_sharedTags.toSet()) .toList(); - _filteredTags = state.tags.keys.toList(); + _filteredTags = labels.tags.keys.toList(); _controller.addListener(() { setState(() { - _filteredTags = context - .read() - .state - .tags - .values + _filteredTags = labels.tags.values .where((e) => e.name.normalized().contains(_controller.text.normalized())) .map((e) => e.id!) @@ -69,6 +67,7 @@ class _FullscreenBulkEditTagsWidgetState @override Widget build(BuildContext context) { + final labelRepository = context.watch(); return BlocBuilder( builder: (context, state) { return FullscreenSelectionForm( @@ -86,7 +85,7 @@ class _FullscreenBulkEditTagsWidgetState selectionBuilder: (context, index) { return _buildTagOption( _filteredTags[index], - state.tags, + labelRepository.tags, ); }, selectionCount: _filteredTags.length, @@ -155,11 +154,12 @@ class _FullscreenBulkEditTagsWidgetState void _submit() async { if (_addTags.isNotEmpty || _removeTags.isNotEmpty) { final bloc = context.read(); + final labelRepository = context.read(); final addNames = _addTags - .map((value) => "\"${bloc.state.tags[value]!.name}\"") + .map((value) => "\"${labelRepository.tags[value]!.name}\"") .toList(); final removeNames = _removeTags - .map((value) => "\"${bloc.state.tags[value]!.name}\"") + .map((value) => "\"${labelRepository.tags[value]!.name}\"") .toList(); final shouldPerformAction = await showDialog( context: context, diff --git a/lib/features/document_details/cubit/document_details_cubit.dart b/lib/features/document_details/cubit/document_details_cubit.dart index 466d911..0238bd6 100644 --- a/lib/features/document_details/cubit/document_details_cubit.dart +++ b/lib/features/document_details/cubit/document_details_cubit.dart @@ -4,8 +4,11 @@ import 'dart:io'; import 'package:bloc/bloc.dart'; import 'package:cross_file/cross_file.dart'; import 'package:flutter/material.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:open_filex/open_filex.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/bloc/loading_status.dart'; +import 'package:paperless_mobile/core/bloc/transient_error.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/service/file_service.dart'; @@ -15,6 +18,7 @@ import 'package:path/path.dart' as p; import 'package:printing/printing.dart'; import 'package:share_plus/share_plus.dart'; +part 'document_details_cubit.freezed.dart'; part 'document_details_state.dart'; class DocumentDetailsCubit extends Cubit { @@ -22,15 +26,13 @@ class DocumentDetailsCubit extends Cubit { final PaperlessDocumentsApi _api; final DocumentChangedNotifier _notifier; final LocalNotificationService _notificationService; - final LabelRepository _labelRepository; DocumentDetailsCubit( this._api, - this._labelRepository, this._notifier, this._notificationService, { required this.id, - }) : super(const DocumentDetailsInitial()) { + }) : super(const DocumentDetailsState()) { _notifier.addListener( this, onUpdated: (document) { @@ -42,7 +44,7 @@ class DocumentDetailsCubit extends Cubit { Future initialize() async { debugPrint("Initialize called"); - emit(const DocumentDetailsLoading()); + emit(const DocumentDetailsState(status: LoadingStatus.loading)); try { final (document, metaData) = await Future.wait([ _api.find(id), @@ -54,11 +56,12 @@ class DocumentDetailsCubit extends Cubit { // final document = await _api.find(id); // final metaData = await _api.getMetaData(id); debugPrint("Document data loaded for $id"); - emit(DocumentDetailsLoaded( + emit(DocumentDetailsState( + status: LoadingStatus.loaded, document: document, metaData: metaData, )); - } catch (error, stackTrace) { + } on PaperlessApiException catch (error, stackTrace) { logger.fe( "An error occurred while loading data for document $id.", className: runtimeType.toString(), @@ -66,13 +69,22 @@ class DocumentDetailsCubit extends Cubit { error: error, stackTrace: stackTrace, ); - emit(const DocumentDetailsError()); + emit(const DocumentDetailsState(status: LoadingStatus.error)); + addError( + TransientPaperlessApiError(code: error.code, details: error.details), + ); } } Future delete(DocumentModel document) async { - await _api.delete(document); - _notifier.notifyDeleted(document); + try { + await _api.delete(document); + _notifier.notifyDeleted(document); + } on PaperlessApiException catch (e) { + addError( + TransientPaperlessApiError(code: e.code, details: e.details), + ); + } } Future assignAsn( @@ -80,29 +92,34 @@ class DocumentDetailsCubit extends Cubit { int? asn, bool autoAssign = false, }) async { - if (!autoAssign) { - final updatedDocument = await _api.update( - document.copyWith(archiveSerialNumber: () => asn), + try { + if (!autoAssign) { + final updatedDocument = await _api.update( + document.copyWith(archiveSerialNumber: () => asn), + ); + _notifier.notifyUpdated(updatedDocument); + } else { + final int autoAsn = await _api.findNextAsn(); + final updatedDocument = await _api + .update(document.copyWith(archiveSerialNumber: () => autoAsn)); + _notifier.notifyUpdated(updatedDocument); + } + } on PaperlessApiException catch (e) { + addError( + TransientPaperlessApiError(code: e.code, details: e.details), ); - _notifier.notifyUpdated(updatedDocument); - } else { - final int autoAsn = await _api.findNextAsn(); - final updatedDocument = await _api - .update(document.copyWith(archiveSerialNumber: () => autoAsn)); - _notifier.notifyUpdated(updatedDocument); } } Future openDocumentInSystemViewer() async { - final s = state; - if (s is! DocumentDetailsLoaded) { + if (state.status != LoadingStatus.loaded) { throw Exception( "Document cannot be opened in system viewer " "if document information has not yet been loaded.", ); } final cacheDir = FileService.instance.temporaryDirectory; - final filePath = s.metaData.mediaFilename.replaceAll("/", " "); + final filePath = state.metaData!.mediaFilename.replaceAll("/", " "); final fileName = "${p.basenameWithoutExtension(filePath)}.pdf"; final file = File("${cacheDir.path}/$fileName"); @@ -110,7 +127,7 @@ class DocumentDetailsCubit extends Cubit { if (!file.existsSync()) { file.createSync(); await _api.downloadToFile( - s.document, + state.document!, file.path, ); } @@ -121,14 +138,7 @@ class DocumentDetailsCubit extends Cubit { } void replace(DocumentModel document) { - final s = state; - if (s is! DocumentDetailsLoaded) { - return; - } - emit(DocumentDetailsLoaded( - document: document, - metaData: s.metaData, - )); + emit(state.copyWith(document: document)); } Future downloadDocument({ @@ -136,12 +146,11 @@ class DocumentDetailsCubit extends Cubit { required String locale, required String userId, }) async { - final s = state; - if (s is! DocumentDetailsLoaded) { + if (state.status != LoadingStatus.loaded) { return; } String targetPath = _buildDownloadFilePath( - s.metaData, + state.metaData!, downloadOriginal, FileService.instance.downloadsDirectory, ); @@ -150,7 +159,7 @@ class DocumentDetailsCubit extends Cubit { await File(targetPath).create(); } else { await _notificationService.notifyDocumentDownload( - document: s.document, + document: state.document!, filename: p.basename(targetPath), filePath: targetPath, finished: true, @@ -169,12 +178,12 @@ class DocumentDetailsCubit extends Cubit { // ); await _api.downloadToFile( - s.document, + state.document!, targetPath, original: downloadOriginal, onProgressChanged: (progress) { _notificationService.notifyDocumentDownload( - document: s.document, + document: state.document!, filename: p.basename(targetPath), filePath: targetPath, finished: true, @@ -185,28 +194,27 @@ class DocumentDetailsCubit extends Cubit { }, ); await _notificationService.notifyDocumentDownload( - document: s.document, + document: state.document!, filename: p.basename(targetPath), filePath: targetPath, finished: true, locale: locale, userId: userId, ); - logger.fi("Document '${s.document.title}' saved to $targetPath."); + logger.fi("Document '${state.document!.title}' saved to $targetPath."); } Future shareDocument({bool shareOriginal = false}) async { - final s = state; - if (s is! DocumentDetailsLoaded) { + if (state.status != LoadingStatus.loaded) { return; } String filePath = _buildDownloadFilePath( - s.metaData, + state.metaData!, shareOriginal, FileService.instance.temporaryDirectory, ); await _api.downloadToFile( - s.document, + state.document!, filePath, original: shareOriginal, ); @@ -214,27 +222,26 @@ class DocumentDetailsCubit extends Cubit { [ XFile( filePath, - name: s.document.originalFileName, + name: state.document!.originalFileName, mimeType: "application/pdf", - lastModified: s.document.modified, + lastModified: state.document!.modified, ), ], - subject: s.document.title, + subject: state.document!.title, ); } Future printDocument() async { - final s = state; - if (s is! DocumentDetailsLoaded) { + if (state.status != LoadingStatus.loaded) { return; } final filePath = _buildDownloadFilePath( - s.metaData, + state.metaData!, false, FileService.instance.temporaryDirectory, ); await _api.downloadToFile( - s.document, + state.document!, filePath, original: false, ); @@ -243,13 +250,16 @@ class DocumentDetailsCubit extends Cubit { throw Exception("An error occurred while downloading the document."); } Printing.layoutPdf( - name: s.document.title, + name: state.document!.title, onLayout: (format) => file.readAsBytesSync(), ); } String _buildDownloadFilePath( - DocumentMetaData meta, bool original, Directory dir) { + DocumentMetaData meta, + bool original, + Directory dir, + ) { final normalizedPath = meta.mediaFilename.replaceAll("/", " "); final extension = original ? p.extension(normalizedPath) : '.pdf'; return "${dir.path}/${p.basenameWithoutExtension(normalizedPath)}$extension"; @@ -257,7 +267,6 @@ class DocumentDetailsCubit extends Cubit { @override Future close() async { - _labelRepository.removeListener(this); _notifier.removeListener(this); await super.close(); } diff --git a/lib/features/document_details/cubit/document_details_state.dart b/lib/features/document_details/cubit/document_details_state.dart index 0d7bbcd..26aed23 100644 --- a/lib/features/document_details/cubit/document_details_state.dart +++ b/lib/features/document_details/cubit/document_details_state.dart @@ -1,41 +1,10 @@ part of 'document_details_cubit.dart'; -sealed class DocumentDetailsState { - const DocumentDetailsState(); +@freezed +class DocumentDetailsState with _$DocumentDetailsState { + const factory DocumentDetailsState({ + @Default(LoadingStatus.initial) LoadingStatus status, + DocumentModel? document, + DocumentMetaData? metaData, + }) = _DocumentDetailsState; } - -class DocumentDetailsInitial extends DocumentDetailsState { - const DocumentDetailsInitial(); -} - -class DocumentDetailsLoading extends DocumentDetailsState { - const DocumentDetailsLoading(); -} - -class DocumentDetailsLoaded extends DocumentDetailsState { - final DocumentModel document; - final DocumentMetaData metaData; - - const DocumentDetailsLoaded({ - required this.document, - required this.metaData, - }); -} - -class DocumentDetailsError extends DocumentDetailsState { - const DocumentDetailsError(); -} - - -// @freezed -// class DocumentDetailsState with _$DocumentDetailsState { -// const factory DocumentDetailsState({ -// required DocumentModel document, -// DocumentMetaData? metaData, -// @Default(false) bool isFullContentLoaded, -// @Default({}) Map correspondents, -// @Default({}) Map documentTypes, -// @Default({}) Map tags, -// @Default({}) Map storagePaths, -// }) = _DocumentDetailsState; -// } 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 0d0d92b..30ea68c 100644 --- a/lib/features/document_details/view/pages/document_details_page.dart +++ b/lib/features/document_details/view/pages/document_details_page.dart @@ -6,6 +6,7 @@ import 'package:open_filex/open_filex.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/accessibility/accessibility_utils.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; +import 'package:paperless_mobile/core/bloc/loading_status.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart'; @@ -15,6 +16,7 @@ import 'package:paperless_mobile/features/document_details/view/widgets/document import 'package:paperless_mobile/features/document_details/view/widgets/document_download_button.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/document_meta_data_widget.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/document_overview_widget.dart'; +import 'package:paperless_mobile/features/document_details/view/widgets/document_permissions_widget.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/document_share_button.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'; @@ -65,7 +67,7 @@ class _DocumentDetailsPageState extends State { debugPrint(disableAnimations.toString()); final hasMultiUserSupport = context.watch().hasMultiUserSupport; - final tabLength = 4 + (hasMultiUserSupport && false ? 1 : 0); + final tabLength = 4 + (hasMultiUserSupport ? 1 : 0); return AnnotatedRegion( value: buildOverlayStyle( Theme.of(context), @@ -79,9 +81,8 @@ class _DocumentDetailsPageState extends State { extendBodyBehindAppBar: false, floatingActionButtonLocation: FloatingActionButtonLocation.endDocked, - floatingActionButton: switch (state) { - DocumentDetailsLoaded(document: var document) => - _buildEditButton(document), + floatingActionButton: switch (state.status) { + LoadingStatus.loaded => _buildEditButton(state.document!), _ => null }, bottomNavigationBar: _buildBottomAppBar(), @@ -93,9 +94,8 @@ class _DocumentDetailsPageState extends State { sliver: BlocBuilder( builder: (context, state) { - final title = switch (state) { - DocumentDetailsLoaded(document: var document) => - document.title, + final title = switch (state.status) { + LoadingStatus.loaded => state.document!.title, _ => widget.title ?? '', }; return SliverAppBar( @@ -201,17 +201,17 @@ class _DocumentDetailsPageState extends State { ), ), ), - // if (hasMultiUserSupport && false) - // Tab( - // child: Text( - // "Permissions", - // style: TextStyle( - // color: Theme.of(context) - // .colorScheme - // .onPrimaryContainer, - // ), - // ), - // ), + if (hasMultiUserSupport) + Tab( + child: Text( + "Permissions", + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), + ), + ), ], ), ), @@ -227,7 +227,6 @@ class _DocumentDetailsPageState extends State { context.read(), context.read(), context.read(), - context.read(), documentId: widget.id, ), child: Padding( @@ -243,17 +242,15 @@ class _DocumentDetailsPageState extends State { handle: NestedScrollView .sliverOverlapAbsorberHandleFor(context), ), - switch (state) { - DocumentDetailsLoaded( - document: var document - ) => + switch (state.status) { + LoadingStatus.loaded => DocumentOverviewWidget( - document: document, + document: state.document!, itemSpacing: _itemSpacing, queryString: widget.titleAndContentQueryString, ), - DocumentDetailsError() => _buildErrorState(), + LoadingStatus.error => _buildErrorState(), _ => _buildLoadingState(), }, ], @@ -264,16 +261,13 @@ class _DocumentDetailsPageState extends State { handle: NestedScrollView .sliverOverlapAbsorberHandleFor(context), ), - switch (state) { - DocumentDetailsLoaded( - document: var document - ) => - DocumentContentWidget( - document: document, + switch (state.status) { + LoadingStatus.loaded => DocumentContentWidget( + document: state.document!, queryString: widget.titleAndContentQueryString, ), - DocumentDetailsError() => _buildErrorState(), + LoadingStatus.error => _buildErrorState(), _ => _buildLoadingState(), } ], @@ -284,17 +278,14 @@ class _DocumentDetailsPageState extends State { handle: NestedScrollView .sliverOverlapAbsorberHandleFor(context), ), - switch (state) { - DocumentDetailsLoaded( - document: var document, - metaData: var metaData, - ) => + switch (state.status) { + LoadingStatus.loaded => DocumentMetaDataWidget( - document: document, + document: state.document!, itemSpacing: _itemSpacing, - metaData: metaData, + metaData: state.metaData!, ), - DocumentDetailsError() => _buildErrorState(), + LoadingStatus.error => _buildErrorState(), _ => _buildLoadingState(), }, ], @@ -312,20 +303,25 @@ class _DocumentDetailsPageState extends State { ), ], ), - // if (hasMultiUserSupport && false) - // CustomScrollView( - // controller: _pagingScrollController, - // slivers: [ - // SliverOverlapInjector( - // handle: NestedScrollView - // .sliverOverlapAbsorberHandleFor( - // context), - // ), - // DocumentPermissionsWidget( - // document: state.document, - // ), - // ], - // ), + if (hasMultiUserSupport) + CustomScrollView( + controller: _pagingScrollController, + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView + .sliverOverlapAbsorberHandleFor( + context), + ), + switch (state.status) { + LoadingStatus.loaded => + DocumentPermissionsWidget( + document: state.document!, + ), + LoadingStatus.error => _buildErrorState(), + _ => _buildLoadingState(), + } + ], + ), ], ), ), @@ -383,8 +379,8 @@ class _DocumentDetailsPageState extends State { return BottomAppBar( child: Builder( builder: (context) { - return switch (state) { - DocumentDetailsLoaded(document: var document) => Row( + return switch (state.status) { + LoadingStatus.loaded => Row( mainAxisAlignment: MainAxisAlignment.start, children: [ ConnectivityAwareActionWrapper( @@ -398,7 +394,7 @@ class _DocumentDetailsPageState extends State { child: IconButton( tooltip: S.of(context)!.deleteDocumentTooltip, icon: const Icon(Icons.delete), - onPressed: () => _onDelete(document), + onPressed: () => _onDelete(state.document!), ).paddedSymmetrically(horizontal: 4), ), ConnectivityAwareActionWrapper( @@ -408,7 +404,7 @@ class _DocumentDetailsPageState extends State { enabled: false, ), child: DocumentDownloadButton( - document: document, + document: state.document, ), ), ConnectivityAwareActionWrapper( @@ -422,7 +418,7 @@ class _DocumentDetailsPageState extends State { onPressed: _onOpenFileInSystemViewer, ).paddedOnly(right: 4.0), ), - DocumentShareButton(document: document), + DocumentShareButton(document: state.document), IconButton( tooltip: S.of(context)!.print, onPressed: () => context diff --git a/lib/features/document_details/view/widgets/archive_serial_number_field.dart b/lib/features/document_details/view/widgets/archive_serial_number_field.dart index 1f420f7..2845862 100644 --- a/lib/features/document_details/view/widgets/archive_serial_number_field.dart +++ b/lib/features/document_details/view/widgets/archive_serial_number_field.dart @@ -3,6 +3,7 @@ import 'package:flutter/services.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/core/bloc/loading_status.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart'; @@ -50,16 +51,13 @@ class _ArchiveSerialNumberFieldState extends State { context.watch().paperlessUser.canEditDocuments; return BlocListener( listenWhen: (previous, current) => - previous is DocumentDetailsLoaded && - current is DocumentDetailsLoaded && - previous.document.archiveSerialNumber != - current.document.archiveSerialNumber, + previous.status == LoadingStatus.loaded && + current.status == LoadingStatus.loaded && + previous.document!.archiveSerialNumber != + current.document!.archiveSerialNumber, listener: (context, state) { - _asnEditingController.text = (state as DocumentDetailsLoaded) - .document - .archiveSerialNumber - ?.toString() ?? - ''; + _asnEditingController.text = + state.document!.archiveSerialNumber?.toString() ?? ''; setState(() { _canUpdate = false; }); diff --git a/lib/features/document_details/view/widgets/document_meta_data_widget.dart b/lib/features/document_details/view/widgets/document_meta_data_widget.dart index fb55bd7..02d99fc 100644 --- a/lib/features/document_details/view/widgets/document_meta_data_widget.dart +++ b/lib/features/document_details/view/widgets/document_meta_data_widget.dart @@ -4,6 +4,7 @@ import 'package:intl/intl.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/core/repository/user_repository.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/archive_serial_number_field.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/details_item.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; @@ -69,6 +70,7 @@ class DocumentMetaDataWidget extends StatelessWidget { context: context, label: S.of(context)!.originalMIMEType, ).paddedOnly(bottom: itemSpacing), + ], ), ); diff --git a/lib/features/document_details/view/widgets/document_overview_widget.dart b/lib/features/document_details/view/widgets/document_overview_widget.dart index c6d255d..d846a69 100644 --- a/lib/features/document_details/view/widgets/document_overview_widget.dart +++ b/lib/features/document_details/view/widgets/document_overview_widget.dart @@ -27,7 +27,7 @@ class DocumentOverviewWidget extends StatelessWidget { @override Widget build(BuildContext context) { final user = context.watch().paperlessUser; - final availableLabels = context.watch().state; + final labelRepository = context.watch(); return SliverList.list( children: [ @@ -51,7 +51,7 @@ class DocumentOverviewWidget extends StatelessWidget { label: S.of(context)!.documentType, content: LabelText( style: Theme.of(context).textTheme.bodyLarge, - label: availableLabels.documentTypes[document.documentType], + label: labelRepository.documentTypes[document.documentType], ), ).paddedOnly(bottom: itemSpacing), if (document.correspondent != null && user.canViewCorrespondents) @@ -59,14 +59,14 @@ class DocumentOverviewWidget extends StatelessWidget { label: S.of(context)!.correspondent, content: LabelText( style: Theme.of(context).textTheme.bodyLarge, - label: availableLabels.correspondents[document.correspondent], + label: labelRepository.correspondents[document.correspondent], ), ).paddedOnly(bottom: itemSpacing), if (document.storagePath != null && user.canViewStoragePaths) DetailsItem( label: S.of(context)!.storagePath, content: LabelText( - label: availableLabels.storagePaths[document.storagePath], + label: labelRepository.storagePaths[document.storagePath], ), ).paddedOnly(bottom: itemSpacing), if (document.tags.isNotEmpty && user.canViewTags) @@ -77,7 +77,7 @@ class DocumentOverviewWidget extends StatelessWidget { child: TagsWidget( isClickable: false, tags: - document.tags.map((e) => availableLabels.tags[e]!).toList(), + document.tags.map((e) => labelRepository.tags[e]!).toList(), ), ), ).paddedOnly(bottom: itemSpacing), diff --git a/lib/features/document_details/view/widgets/document_permissions_widget.dart b/lib/features/document_details/view/widgets/document_permissions_widget.dart index d66e913..a87214c 100644 --- a/lib/features/document_details/view/widgets/document_permissions_widget.dart +++ b/lib/features/document_details/view/widgets/document_permissions_widget.dart @@ -1,5 +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/user_repository.dart'; +import 'package:paperless_mobile/features/document_details/view/widgets/details_item.dart'; class DocumentPermissionsWidget extends StatefulWidget { final DocumentModel document; @@ -13,8 +16,20 @@ class DocumentPermissionsWidget extends StatefulWidget { class _DocumentPermissionsWidgetState extends State { @override Widget build(BuildContext context) { - return const SliverToBoxAdapter( - child: Placeholder(), + return BlocBuilder( + builder: (context, state) { + final owner = state.users[widget.document.owner]; + return SliverList.list( + children: [ + if (owner != null) + DetailsItem.text( + owner.username, + label: 'Owner', + context: context, + ), + ], + ); + }, ); } } diff --git a/lib/features/document_edit/view/document_edit_page.dart b/lib/features/document_edit/view/document_edit_page.dart index a3d0d10..8930727 100644 --- a/lib/features/document_edit/view/document_edit_page.dart +++ b/lib/features/document_edit/view/document_edit_page.dart @@ -185,6 +185,8 @@ class _DocumentEditPageState extends State Padding _buildEditForm(BuildContext context, DocumentEditState state, FieldSuggestions? filteredSuggestions, UserModel currentUser) { + final labelRepository = context.watch(); + return Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: TabBarView( @@ -211,8 +213,7 @@ class _DocumentEditPageState extends State ).push(context), addLabelText: S.of(context)!.addCorrespondent, labelText: S.of(context)!.correspondent, - options: - context.watch().state.correspondents, + options: labelRepository.correspondents, initialValue: state.document.correspondent != null ? SetIdQueryParameter( id: state.document.correspondent!) @@ -243,8 +244,7 @@ class _DocumentEditPageState extends State ? SetIdQueryParameter( id: state.document.documentType!) : const UnsetIdQueryParameter(), - options: - context.watch().state.documentTypes, + options: labelRepository.documentTypes, name: _DocumentEditPageState.fkDocumentType, prefixIcon: const Icon(Icons.description_outlined), allowSelectUnassigned: true, @@ -266,8 +266,7 @@ class _DocumentEditPageState extends State canCreateNewLabel: currentUser.canCreateStoragePaths, addLabelText: S.of(context)!.addStoragePath, labelText: S.of(context)!.storagePath, - options: - context.watch().state.storagePaths, + options: labelRepository.storagePaths, initialValue: state.document.storagePath != null ? SetIdQueryParameter(id: state.document.storagePath!) : const UnsetIdQueryParameter(), @@ -280,7 +279,7 @@ class _DocumentEditPageState extends State // Tag form field if (currentUser.canViewTags) TagsFormField( - options: context.watch().state.tags, + options: labelRepository.tags, name: fkTags, allowOnlySelection: true, allowCreation: true, diff --git a/lib/features/document_scan/cubit/document_scanner_cubit.dart b/lib/features/document_scan/cubit/document_scanner_cubit.dart index 85b63d7..7cf0c52 100644 --- a/lib/features/document_scan/cubit/document_scanner_cubit.dart +++ b/lib/features/document_scan/cubit/document_scanner_cubit.dart @@ -3,20 +3,24 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/bloc/loading_status.dart'; +import 'package:paperless_mobile/core/bloc/transient_error.dart'; import 'package:paperless_mobile/features/logging/data/logger.dart'; import 'package:paperless_mobile/core/model/info_message_exception.dart'; import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart'; import 'package:rxdart/rxdart.dart'; +part 'document_scanner_cubit.freezed.dart'; part 'document_scanner_state.dart'; class DocumentScannerCubit extends Cubit { final LocalNotificationService _notificationService; DocumentScannerCubit(this._notificationService) - : super(const InitialDocumentScannerState()); + : super(const DocumentScannerState()); Future initialize() async { logger.fd( @@ -24,7 +28,7 @@ class DocumentScannerCubit extends Cubit { className: runtimeType.toString(), methodName: "initialize", ); - emit(const RestoringDocumentScannerState()); + emit(const DocumentScannerState(status: LoadingStatus.loading)); final tempDir = FileService.instance.temporaryScansDirectory; final allFiles = tempDir.list().whereType(); final scans = @@ -36,13 +40,14 @@ class DocumentScannerCubit extends Cubit { ); emit( scans.isEmpty - ? const InitialDocumentScannerState() - : LoadedDocumentScannerState(scans: scans), + ? const DocumentScannerState() + : DocumentScannerState(scans: scans, status: LoadingStatus.loaded), ); } void addScan(File file) async { - emit(LoadedDocumentScannerState( + emit(DocumentScannerState( + status: LoadingStatus.loaded, scans: [...state.scans, file], )); } @@ -60,21 +65,22 @@ class DocumentScannerCubit extends Cubit { final scans = state.scans..remove(file); emit( scans.isEmpty - ? const InitialDocumentScannerState() - : LoadedDocumentScannerState(scans: scans), + ? const DocumentScannerState() + : DocumentScannerState( + status: LoadingStatus.loaded, + scans: scans, + ), ); } Future reset() async { try { - Future.wait([ - for (final file in state.scans) file.delete(), - ]); + Future.wait([for (final file in state.scans) file.delete()]); imageCache.clear(); } catch (_) { - throw const PaperlessApiException(ErrorCode.scanRemoveFailed); + addError(TransientPaperlessApiError(code: ErrorCode.scanRemoveFailed)); } finally { - emit(const InitialDocumentScannerState()); + emit(const DocumentScannerState()); } } @@ -83,12 +89,16 @@ class DocumentScannerCubit extends Cubit { String fileName, String locale, ) async { - var file = await FileService.instance.saveToFile(bytes, fileName); - _notificationService.notifyFileSaved( - filename: fileName, - filePath: file.path, - finished: true, - locale: locale, - ); + try { + var file = await FileService.instance.saveToFile(bytes, fileName); + _notificationService.notifyFileSaved( + filename: fileName, + filePath: file.path, + finished: true, + locale: locale, + ); + } on Exception catch (e) { + addError(TransientMessageError(message: e.toString())); + } } } diff --git a/lib/features/document_scan/cubit/document_scanner_state.dart b/lib/features/document_scan/cubit/document_scanner_state.dart index 70f7b33..f1084ed 100644 --- a/lib/features/document_scan/cubit/document_scanner_state.dart +++ b/lib/features/document_scan/cubit/document_scanner_state.dart @@ -1,30 +1,9 @@ part of 'document_scanner_cubit.dart'; -sealed class DocumentScannerState { - final List scans; - - const DocumentScannerState({ - this.scans = const [], - }); -} - -class InitialDocumentScannerState extends DocumentScannerState { - const InitialDocumentScannerState(); -} - -class RestoringDocumentScannerState extends DocumentScannerState { - const RestoringDocumentScannerState({super.scans}); -} - -class LoadedDocumentScannerState extends DocumentScannerState { - const LoadedDocumentScannerState({super.scans}); -} - -class ErrorDocumentScannerState extends DocumentScannerState { - final String message; - - const ErrorDocumentScannerState({ - required this.message, - super.scans, - }); +@freezed +class DocumentScannerState with _$DocumentScannerState { + const factory DocumentScannerState({ + @Default(LoadingStatus.initial) LoadingStatus status, + @Default([]) List scans, + }) = _DocumentScannerState; } diff --git a/lib/features/document_scan/view/scanner_page.dart b/lib/features/document_scan/view/scanner_page.dart index 8cfb32b..2bf183f 100644 --- a/lib/features/document_scan/view/scanner_page.dart +++ b/lib/features/document_scan/view/scanner_page.dart @@ -10,6 +10,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hive/hive.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/constants.dart'; +import 'package:paperless_mobile/core/bloc/loading_status.dart'; import 'package:paperless_mobile/core/database/hive/hive_config.dart'; import 'package:paperless_mobile/core/database/tables/global_settings.dart'; import 'package:paperless_mobile/core/global/constants.dart'; @@ -78,13 +79,11 @@ class _ScannerPageState extends State ], body: BlocBuilder( builder: (context, state) { - return switch (state) { - InitialDocumentScannerState() => _buildEmptyState(), - RestoringDocumentScannerState() => Center( - child: Text("Restoring..."), - ), - LoadedDocumentScannerState() => _buildImageGrid(state.scans), - ErrorDocumentScannerState() => Placeholder(), + return switch (state.status) { + LoadingStatus.initial => _buildEmptyState(), + LoadingStatus.loading => Center(child: Text("Restoring...")), + LoadingStatus.loaded => _buildImageGrid(state.scans), + LoadingStatus.error => Placeholder(), }; }, ), diff --git a/lib/features/document_upload/cubit/document_upload_cubit.dart b/lib/features/document_upload/cubit/document_upload_cubit.dart index 5bc09e3..a60988d 100644 --- a/lib/features/document_upload/cubit/document_upload_cubit.dart +++ b/lib/features/document_upload/cubit/document_upload_cubit.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:flutter/foundation.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/bloc/transient_error.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/service/connectivity_status_service.dart'; import 'package:paperless_mobile/features/tasks/model/pending_tasks_notifier.dart'; @@ -33,24 +34,31 @@ class DocumentUploadCubit extends Cubit { DateTime? createdAt, int? asn, }) async { - final taskId = await _documentApi.create( - bytes, - filename: filename, - title: title, - correspondent: correspondent, - documentType: documentType, - tags: tags, - createdAt: createdAt, - asn: asn, - onProgressChanged: (progress) { - if (!isClosed) { - emit(state.copyWith(uploadProgress: progress)); - } - }, - ); - if (taskId != null) { - _tasksNotifier.listenToTaskChanges(taskId); + try { + final taskId = await _documentApi.create( + bytes, + filename: filename, + title: title, + correspondent: correspondent, + documentType: documentType, + tags: tags, + createdAt: createdAt, + asn: asn, + onProgressChanged: (progress) { + if (!isClosed) { + emit(state.copyWith(uploadProgress: progress)); + } + }, + ); + if (taskId != null) { + _tasksNotifier.listenToTaskChanges(taskId); + } + return taskId; + } on PaperlessApiException catch (error) { + addError(TransientPaperlessApiError( + code: error.code, + details: error.details, + )); } - return taskId; } } 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 8791c07..4ab0df6 100644 --- a/lib/features/document_upload/view/document_upload_preparation_page.dart +++ b/lib/features/document_upload/view/document_upload_preparation_page.dart @@ -70,7 +70,7 @@ class _DocumentUploadPreparationPageState @override Widget build(BuildContext context) { - final labels = context.watch().state; + final labelRepository = context.watch(); return BlocBuilder( builder: (context, state) { return Scaffold( @@ -242,7 +242,7 @@ class _DocumentUploadPreparationPageState addLabelText: S.of(context)!.addCorrespondent, labelText: S.of(context)!.correspondent + " *", name: DocumentModel.correspondentKey, - options: labels.correspondents, + options: labelRepository.correspondents, prefixIcon: const Icon(Icons.person_outline), allowSelectUnassigned: true, canCreateNewLabel: context @@ -265,7 +265,7 @@ class _DocumentUploadPreparationPageState addLabelText: S.of(context)!.addDocumentType, labelText: S.of(context)!.documentType + " *", name: DocumentModel.documentTypeKey, - options: labels.documentTypes, + options: labelRepository.documentTypes, prefixIcon: const Icon(Icons.description_outlined), allowSelectUnassigned: true, @@ -283,7 +283,7 @@ class _DocumentUploadPreparationPageState allowCreation: true, allowExclude: false, allowOnlySelection: true, - options: labels.tags, + options: labelRepository.tags, ), Text( "* " + S.of(context)!.uploadInferValuesHint, @@ -353,8 +353,6 @@ class _DocumentUploadPreparationPageState S.of(context)!.documentSuccessfullyUploadedProcessing, ); context.pop(DocumentUploadResult(true, taskId)); - } on PaperlessApiException catch (error, stackTrace) { - showErrorMessage(context, error, stackTrace); } on PaperlessFormValidationException catch (exception) { setState(() => _errors = exception.validationMessages); } catch (error, stackTrace) { diff --git a/lib/features/documents/cubit/documents_cubit.dart b/lib/features/documents/cubit/documents_cubit.dart index 3103b57..0dbacb7 100644 --- a/lib/features/documents/cubit/documents_cubit.dart +++ b/lib/features/documents/cubit/documents_cubit.dart @@ -1,8 +1,6 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; -import 'package:json_annotation/json_annotation.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart'; import 'package:paperless_mobile/core/extensions/document_extensions.dart'; @@ -20,7 +18,6 @@ class DocumentsCubit extends Cubit @override final PaperlessDocumentsApi api; - final LabelRepository _labelRepository; @override final ConnectivityStatusService connectivityStatusService; @@ -32,7 +29,6 @@ class DocumentsCubit extends Cubit DocumentsCubit( this.api, this.notifier, - this._labelRepository, this._userState, this.connectivityStatusService, ) : super(DocumentsState( @@ -58,17 +54,6 @@ class DocumentsCubit extends Cubit ); }, ); - _labelRepository.addListener( - this, - onChanged: (labels) => emit( - state.copyWith( - correspondents: labels.correspondents, - documentTypes: labels.documentTypes, - storagePaths: labels.storagePaths, - tags: labels.tags, - ), - ), - ); } Future bulkDelete(List documents) async { @@ -111,7 +96,6 @@ class DocumentsCubit extends Cubit @override Future close() { notifier.removeListener(this); - _labelRepository.removeListener(this); return super.close(); } diff --git a/lib/features/documents/cubit/documents_state.dart b/lib/features/documents/cubit/documents_state.dart index 87a050a..7e41d7d 100644 --- a/lib/features/documents/cubit/documents_state.dart +++ b/lib/features/documents/cubit/documents_state.dart @@ -2,10 +2,6 @@ part of 'documents_cubit.dart'; class DocumentsState extends DocumentPagingState { final List selection; - final Map correspondents; - final Map documentTypes; - final Map tags; - final Map storagePaths; final ViewType viewType; @@ -16,10 +12,6 @@ class DocumentsState extends DocumentPagingState { super.filter = const DocumentFilter(), super.hasLoaded = false, super.isLoading = false, - this.correspondents = const {}, - this.documentTypes = const {}, - this.tags = const {}, - this.storagePaths = const {}, }); List get selectedIds => selection.map((e) => e.id).toList(); @@ -31,10 +23,6 @@ class DocumentsState extends DocumentPagingState { DocumentFilter? filter, List? selection, ViewType? viewType, - Map? correspondents, - Map? documentTypes, - Map? tags, - Map? storagePaths, }) { return DocumentsState( hasLoaded: hasLoaded ?? this.hasLoaded, @@ -43,10 +31,6 @@ class DocumentsState extends DocumentPagingState { filter: filter ?? this.filter, selection: selection ?? this.selection, viewType: viewType ?? this.viewType, - correspondents: correspondents ?? this.correspondents, - documentTypes: documentTypes ?? this.documentTypes, - tags: tags ?? this.tags, - storagePaths: storagePaths ?? this.storagePaths, ); } @@ -54,10 +38,6 @@ class DocumentsState extends DocumentPagingState { List get props => [ selection, viewType, - correspondents, - documentTypes, - tags, - storagePaths, super.filter, super.hasLoaded, super.isLoading, diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index ea519a6..c1cc561 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -489,10 +489,6 @@ class _DocumentsPageState extends State { initialFilter: context.read().state.filter, scrollController: controller, draggableSheetController: draggableSheetController, - correspondents: state.correspondents, - documentTypes: state.documentTypes, - storagePaths: state.storagePaths, - tags: state.tags, ); }, ), diff --git a/lib/features/documents/view/widgets/date_and_document_type_widget.dart b/lib/features/documents/view/widgets/date_and_document_type_widget.dart index f9da206..9deac1d 100644 --- a/lib/features/documents/view/widgets/date_and_document_type_widget.dart +++ b/lib/features/documents/view/widgets/date_and_document_type_widget.dart @@ -18,6 +18,7 @@ class DateAndDocumentTypeLabelWidget extends StatelessWidget { Widget build(BuildContext context) { final subtitleStyle = Theme.of(context).textTheme.labelMedium?.apply(color: Colors.grey); + final labelRepository = context.watch(); return RichText( maxLines: 1, overflow: TextOverflow.ellipsis, @@ -37,11 +38,8 @@ class DateAndDocumentTypeLabelWidget extends StatelessWidget { ? () => onDocumentTypeSelected!(document.documentType) : null, child: Text( - context - .watch() - .state - .documentTypes[document.documentType]! - .name, + labelRepository + .documentTypes[document.documentType]!.name, style: subtitleStyle, ), ), diff --git a/lib/features/documents/view/widgets/items/document_detailed_item.dart b/lib/features/documents/view/widgets/items/document_detailed_item.dart index 0410e61..8274de4 100644 --- a/lib/features/documents/view/widgets/items/document_detailed_item.dart +++ b/lib/features/documents/view/widgets/items/document_detailed_item.dart @@ -1,21 +1,18 @@ import 'dart:math'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; import 'package:hive_flutter/adapters.dart'; -import 'package:intl/intl.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/database/hive/hive_config.dart'; import 'package:paperless_mobile/core/database/tables/global_settings.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; -import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/features/documents/view/widgets/date_and_document_type_widget.dart'; import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart'; import 'package:paperless_mobile/features/documents/view/widgets/items/document_item.dart'; import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart'; -import 'package:paperless_mobile/features/labels/document_type/view/widgets/document_type_widget.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart'; import 'package:provider/provider.dart'; @@ -58,7 +55,7 @@ class DocumentDetailedItem extends DocumentItem { final maxHeight = highlights != null ? min(600.0, availableHeight) : min(500.0, availableHeight); - final labels = context.watch().state; + final labelRepository = context.watch(); return Card( color: isSelected ? Theme.of(context).colorScheme.inversePrimary : null, child: InkWell( @@ -93,8 +90,9 @@ class DocumentDetailedItem extends DocumentItem { Align( alignment: Alignment.bottomLeft, child: TagsWidget( - tags: - document.tags.map((e) => labels.tags[e]!).toList(), + tags: document.tags + .map((e) => labelRepository.tags[e]!) + .toList(), onTagSelected: onTagSelected, ).padded(), ), @@ -107,7 +105,8 @@ class DocumentDetailedItem extends DocumentItem { textStyle: Theme.of(context).textTheme.titleSmall?.apply( color: Theme.of(context).colorScheme.onSurfaceVariant, ), - correspondent: labels.correspondents[document.correspondent], + correspondent: + labelRepository.correspondents[document.correspondent], ).paddedLTRB(8, 8, 8, 0), Text( document.title.isEmpty ? '(-)' : document.title, diff --git a/lib/features/documents/view/widgets/items/document_grid_item.dart b/lib/features/documents/view/widgets/items/document_grid_item.dart index b56557e..9d9438e 100644 --- a/lib/features/documents/view/widgets/items/document_grid_item.dart +++ b/lib/features/documents/view/widgets/items/document_grid_item.dart @@ -30,6 +30,7 @@ class DocumentGridItem extends DocumentItem { @override Widget build(BuildContext context) { var currentUser = context.watch().paperlessUser; + final labelRepository = context.watch(); return Stack( children: [ Card( @@ -75,10 +76,7 @@ class DocumentGridItem extends DocumentItem { if (currentUser.canViewTags) TagsWidget.sliver( tags: document.tags - .map((e) => context - .watch() - .state - .tags[e]!) + .map((e) => labelRepository.tags[e]!) .toList(), onTagSelected: onTagSelected, ), @@ -102,17 +100,13 @@ class DocumentGridItem extends DocumentItem { children: [ if (currentUser.canViewCorrespondents) CorrespondentWidget( - correspondent: context - .watch() - .state + correspondent: labelRepository .correspondents[document.correspondent], onSelected: onCorrespondentSelected, ), if (currentUser.canViewDocumentTypes) DocumentTypeWidget( - documentType: context - .watch() - .state + documentType: labelRepository .documentTypes[document.documentType], onSelected: onDocumentTypeSelected, ), 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 8e3a38a..6f64525 100644 --- a/lib/features/documents/view/widgets/items/document_list_item.dart +++ b/lib/features/documents/view/widgets/items/document_list_item.dart @@ -1,8 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'package:paperless_api/src/models/document_model.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; -import 'package:paperless_mobile/core/repository/label_repository_state.dart'; import 'package:paperless_mobile/features/documents/view/widgets/date_and_document_type_widget.dart'; import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart'; import 'package:paperless_mobile/features/documents/view/widgets/items/document_item.dart'; @@ -32,7 +29,7 @@ class DocumentListItem extends DocumentItem { @override Widget build(BuildContext context) { - final labels = context.watch().state; + final labelRepository = context.watch(); return ListTile( tileColor: backgroundColor, @@ -51,10 +48,8 @@ class DocumentListItem extends DocumentItem { absorbing: isSelectionActive, child: CorrespondentWidget( isClickable: isLabelClickable, - correspondent: context - .watch() - .state - .correspondents[document.correspondent], + correspondent: + labelRepository.correspondents[document.correspondent], onSelected: onCorrespondentSelected, ), ), @@ -70,8 +65,8 @@ class DocumentListItem extends DocumentItem { child: TagsWidget( isClickable: isLabelClickable, tags: document.tags - .where((e) => labels.tags.containsKey(e)) - .map((e) => labels.tags[e]!) + .where((e) => labelRepository.tags.containsKey(e)) + .map((e) => labelRepository.tags[e]!) .toList(), onTagSelected: (id) => onTagSelected?.call(id), ), diff --git a/lib/features/documents/view/widgets/search/document_filter_form.dart b/lib/features/documents/view/widgets/search/document_filter_form.dart index 30813a9..0618660 100644 --- a/lib/features/documents/view/widgets/search/document_filter_form.dart +++ b/lib/features/documents/view/widgets/search/document_filter_form.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/widgets/form_builder_fields/extended_date_range_form_field/form_builder_extended_date_range_picker.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart'; import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart'; @@ -47,10 +48,6 @@ class DocumentFilterForm extends StatefulWidget { final DocumentFilter initialFilter; final ScrollController? scrollController; final EdgeInsets padding; - final Map correspondents; - final Map documentTypes; - final Map tags; - final Map storagePaths; const DocumentFilterForm({ super.key, @@ -59,10 +56,6 @@ class DocumentFilterForm extends StatefulWidget { required this.initialFilter, this.scrollController, this.padding = const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - required this.correspondents, - required this.documentTypes, - required this.tags, - required this.storagePaths, }); @override @@ -80,13 +73,14 @@ class _DocumentFilterFormState extends State { @override Widget build(BuildContext context) { + final labelRepository = context.watch(); return FormBuilder( key: widget.formKey, child: CustomScrollView( controller: widget.scrollController, slivers: [ if (widget.header != null) widget.header!, - ..._buildFormFieldList(), + ..._buildFormFieldList(labelRepository), const SliverToBoxAdapter( child: SizedBox( height: 32, @@ -97,7 +91,7 @@ class _DocumentFilterFormState extends State { ); } - List _buildFormFieldList() { + List _buildFormFieldList(LabelRepository labelRepository) { return [ _buildQueryFormField(), Align( @@ -123,10 +117,10 @@ class _DocumentFilterFormState extends State { _checkQueryConstraints(); }, ), - _buildCorrespondentFormField(), - _buildDocumentTypeFormField(), - _buildStoragePathFormField(), - _buildTagsFormField(), + _buildCorrespondentFormField(labelRepository.correspondents), + _buildDocumentTypeFormField(labelRepository.documentTypes), + _buildStoragePathFormField(labelRepository.storagePaths), + _buildTagsFormField(labelRepository.tags), ] .map((w) => SliverPadding( padding: widget.padding, @@ -151,10 +145,10 @@ class _DocumentFilterFormState extends State { } } - Widget _buildDocumentTypeFormField() { + Widget _buildDocumentTypeFormField(Map documentTypes) { return LabelFormField( name: DocumentFilterForm.fkDocumentType, - options: widget.documentTypes, + options: documentTypes, labelText: S.of(context)!.documentType, initialValue: widget.initialFilter.documentType, prefixIcon: const Icon(Icons.description_outlined), @@ -166,10 +160,10 @@ class _DocumentFilterFormState extends State { ); } - Widget _buildCorrespondentFormField() { + Widget _buildCorrespondentFormField(Map correspondents) { return LabelFormField( name: DocumentFilterForm.fkCorrespondent, - options: widget.correspondents, + options: correspondents, labelText: S.of(context)!.correspondent, initialValue: widget.initialFilter.correspondent, prefixIcon: const Icon(Icons.person_outline), @@ -181,10 +175,10 @@ class _DocumentFilterFormState extends State { ); } - Widget _buildStoragePathFormField() { + Widget _buildStoragePathFormField(Map storagePaths) { return LabelFormField( name: DocumentFilterForm.fkStoragePath, - options: widget.storagePaths, + options: storagePaths, labelText: S.of(context)!.storagePath, initialValue: widget.initialFilter.storagePath, prefixIcon: const Icon(Icons.folder_outlined), @@ -202,11 +196,11 @@ class _DocumentFilterFormState extends State { ); } - Widget _buildTagsFormField() { + Widget _buildTagsFormField(Map tags) { return TagsFormField( name: DocumentModel.tagsKey, initialValue: widget.initialFilter.tags, - options: widget.tags, + options: tags, allowExclude: false, allowOnlySelection: false, allowCreation: false, diff --git a/lib/features/documents/view/widgets/search/document_filter_panel.dart b/lib/features/documents/view/widgets/search/document_filter_panel.dart index 8cdb5af..c71c069 100644 --- a/lib/features/documents/view/widgets/search/document_filter_panel.dart +++ b/lib/features/documents/view/widgets/search/document_filter_panel.dart @@ -13,20 +13,12 @@ class DocumentFilterPanel extends StatefulWidget { final DocumentFilter initialFilter; final ScrollController scrollController; final DraggableScrollableController draggableSheetController; - final Map correspondents; - final Map documentTypes; - final Map tags; - final Map storagePaths; const DocumentFilterPanel({ Key? key, required this.initialFilter, required this.scrollController, required this.draggableSheetController, - required this.correspondents, - required this.documentTypes, - required this.tags, - required this.storagePaths, }) : super(key: key); @override @@ -104,10 +96,6 @@ class _DocumentFilterPanelState extends State { scrollController: widget.scrollController, initialFilter: widget.initialFilter, header: _buildPanelHeader(), - correspondents: widget.correspondents, - documentTypes: widget.documentTypes, - storagePaths: widget.storagePaths, - tags: widget.tags, ), ), ); diff --git a/lib/features/documents/view/widgets/search/sort_field_selection_bottom_sheet.dart b/lib/features/documents/view/widgets/search/sort_field_selection_bottom_sheet.dart index 0b8d89c..37ab488 100644 --- a/lib/features/documents/view/widgets/search/sort_field_selection_bottom_sheet.dart +++ b/lib/features/documents/view/widgets/search/sort_field_selection_bottom_sheet.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/translation/sort_field_localization_mapper.dart'; import 'package:paperless_mobile/core/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; @@ -8,10 +10,6 @@ import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; class SortFieldSelectionBottomSheet extends StatefulWidget { final SortOrder initialSortOrder; final SortField? initialSortField; - final Map correspondents; - final Map documentTypes; - final Map tags; - final Map storagePaths; final Future Function(SortField? field, SortOrder order) onSubmit; @@ -20,10 +18,6 @@ class SortFieldSelectionBottomSheet extends StatefulWidget { required this.initialSortOrder, required this.initialSortField, required this.onSubmit, - required this.correspondents, - required this.documentTypes, - required this.tags, - required this.storagePaths, }); @override @@ -45,6 +39,7 @@ class _SortFieldSelectionBottomSheetState @override Widget build(BuildContext context) { + final labelRepository = context.watch(); return ClipRRect( child: SingleChildScrollView( child: Column( @@ -75,7 +70,7 @@ class _SortFieldSelectionBottomSheetState _buildSortOption(SortField.archiveSerialNumber), _buildSortOption( SortField.correspondentName, - enabled: widget.correspondents.values.fold( + enabled: labelRepository.correspondents.values.fold( false, (previousValue, element) => previousValue || (element.documentCount ?? 0) > 0), @@ -83,7 +78,7 @@ class _SortFieldSelectionBottomSheetState _buildSortOption(SortField.title), _buildSortOption( SortField.documentType, - enabled: widget.documentTypes.values.fold( + enabled: labelRepository.documentTypes.values.fold( false, (previousValue, element) => previousValue || (element.documentCount ?? 0) > 0), diff --git a/lib/features/documents/view/widgets/sort_documents_button.dart b/lib/features/documents/view/widgets/sort_documents_button.dart index ec94f63..c6cac6d 100644 --- a/lib/features/documents/view/widgets/sort_documents_button.dart +++ b/lib/features/documents/view/widgets/sort_documents_button.dart @@ -69,10 +69,6 @@ class SortDocumentsButton extends StatelessWidget { ), ); }, - correspondents: state.correspondents, - documentTypes: state.documentTypes, - storagePaths: state.storagePaths, - tags: state.tags, ), ), ), diff --git a/lib/features/edit_label/cubit/edit_label_cubit.dart b/lib/features/edit_label/cubit/edit_label_cubit.dart deleted file mode 100644 index e885eac..0000000 --- a/lib/features/edit_label/cubit/edit_label_cubit.dart +++ /dev/null @@ -1,33 +0,0 @@ -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/features/labels/cubit/label_cubit_mixin.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -part 'edit_label_state.dart'; -part 'edit_label_cubit.freezed.dart'; - -class EditLabelCubit extends Cubit - with LabelCubitMixin { - @override - final LabelRepository labelRepository; - - EditLabelCubit(this.labelRepository) : super(const EditLabelState()) { - labelRepository.addListener( - this, - onChanged: (labels) => state.copyWith( - correspondents: labels.correspondents, - documentTypes: labels.documentTypes, - tags: labels.tags, - storagePaths: labels.storagePaths, - ), - ); - } - - @override - Future close() { - labelRepository.removeListener(this); - return super.close(); - } -} diff --git a/lib/features/edit_label/cubit/edit_label_state.dart b/lib/features/edit_label/cubit/edit_label_state.dart deleted file mode 100644 index 3d40045..0000000 --- a/lib/features/edit_label/cubit/edit_label_state.dart +++ /dev/null @@ -1,11 +0,0 @@ -part of 'edit_label_cubit.dart'; - -@freezed -class EditLabelState with _$EditLabelState { - const factory EditLabelState({ - @Default({}) Map correspondents, - @Default({}) Map documentTypes, - @Default({}) Map tags, - @Default({}) Map storagePaths, - }) = _EditLabelState; -} diff --git a/lib/features/edit_label/view/add_label_page.dart b/lib/features/edit_label/view/add_label_page.dart index 1a3f3b3..3dcbd79 100644 --- a/lib/features/edit_label/view/add_label_page.dart +++ b/lib/features/edit_label/view/add_label_page.dart @@ -2,8 +2,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/label_repository.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/features/labels/cubit/label_cubit.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; class AddLabelPage extends StatelessWidget { @@ -25,7 +25,7 @@ class AddLabelPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => EditLabelCubit( + create: (context) => LabelCubit( context.read(), ), child: AddLabelFormWidget( diff --git a/lib/features/edit_label/view/edit_label_page.dart b/lib/features/edit_label/view/edit_label_page.dart index c73cef2..e089b6d 100644 --- a/lib/features/edit_label/view/edit_label_page.dart +++ b/lib/features/edit_label/view/edit_label_page.dart @@ -9,8 +9,8 @@ import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart'; import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart'; import 'package:paperless_mobile/core/widgets/dialog_utils/pop_with_unsaved_changes.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/features/labels/cubit/label_cubit.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; @@ -35,7 +35,7 @@ class EditLabelPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => EditLabelCubit( + create: (context) => LabelCubit( context.read(), ), child: EditLabelForm( 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 8d615c8..edb2c36 100644 --- a/lib/features/edit_label/view/impl/add_correspondent_page.dart +++ b/lib/features/edit_label/view/impl/add_correspondent_page.dart @@ -1,8 +1,8 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart'; import 'package:paperless_mobile/features/edit_label/view/add_label_page.dart'; +import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; class AddCorrespondentPage extends StatelessWidget { @@ -12,7 +12,7 @@ class AddCorrespondentPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => EditLabelCubit( + create: (context) => LabelCubit( context.read(), ), child: AddLabelPage( @@ -20,7 +20,7 @@ class AddCorrespondentPage extends StatelessWidget { fromJsonT: Correspondent.fromJson, initialName: initialName, onSubmit: (context, label) => - context.read().addCorrespondent(label), + context.read().addCorrespondent(label), ), ); } 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 35a8b40..2ea2e54 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 @@ -1,8 +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/features/edit_label/cubit/edit_label_cubit.dart'; import 'package:paperless_mobile/features/edit_label/view/add_label_page.dart'; +import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; class AddDocumentTypePage extends StatelessWidget { @@ -15,7 +15,7 @@ class AddDocumentTypePage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => EditLabelCubit( + create: (context) => LabelCubit( context.read(), ), child: AddLabelPage( @@ -23,7 +23,7 @@ class AddDocumentTypePage extends StatelessWidget { fromJsonT: DocumentType.fromJson, initialName: initialName, onSubmit: (context, label) => - context.read().addDocumentType(label), + context.read().addDocumentType(label), ), ); } 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 b033a72..0ec160c 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 @@ -1,8 +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/features/edit_label/cubit/edit_label_cubit.dart'; import 'package:paperless_mobile/features/edit_label/view/add_label_page.dart'; +import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart'; import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_autofill_form_builder_field.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; @@ -13,7 +13,7 @@ class AddStoragePathPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => EditLabelCubit( + create: (context) => LabelCubit( context.read(), ), child: AddLabelPage( @@ -21,7 +21,7 @@ class AddStoragePathPage extends StatelessWidget { fromJsonT: StoragePath.fromJson, initialName: initialName, onSubmit: (context, label) => - context.read().addStoragePath(label), + context.read().addStoragePath(label), additionalFields: const [ StoragePathAutofillFormBuilderField(name: StoragePath.pathKey), SizedBox(height: 120.0), 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 88310b4..98c3027 100644 --- a/lib/features/edit_label/view/impl/add_tag_page.dart +++ b/lib/features/edit_label/view/impl/add_tag_page.dart @@ -5,8 +5,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/widgets/form_builder_fields/form_builder_color_picker.dart'; -import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart'; import 'package:paperless_mobile/features/edit_label/view/add_label_page.dart'; +import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; class AddTagPage extends StatelessWidget { @@ -16,15 +16,14 @@ class AddTagPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => EditLabelCubit( + create: (context) => LabelCubit( context.read(), ), child: AddLabelPage( pageTitle: Text(S.of(context)!.addTag), fromJsonT: Tag.fromJson, initialName: initialName, - onSubmit: (context, label) => - context.read().addTag(label), + onSubmit: (context, label) => context.read().addTag(label), additionalFields: [ FormBuilderColorPickerField( name: Tag.colorKey, 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 c358cd8..dae3e9e 100644 --- a/lib/features/edit_label/view/impl/edit_correspondent_page.dart +++ b/lib/features/edit_label/view/impl/edit_correspondent_page.dart @@ -2,8 +2,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/database/tables/local_user_account.dart'; -import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart'; import 'package:paperless_mobile/features/edit_label/view/edit_label_page.dart'; +import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart'; class EditCorrespondentPage extends StatelessWidget { final Correspondent correspondent; @@ -13,7 +13,7 @@ class EditCorrespondentPage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( lazy: false, - create: (context) => EditLabelCubit( + create: (context) => LabelCubit( context.read(), ), child: Builder(builder: (context) { @@ -21,9 +21,9 @@ class EditCorrespondentPage extends StatelessWidget { label: correspondent, fromJsonT: Correspondent.fromJson, onSubmit: (context, label) => - context.read().replaceCorrespondent(label), + context.read().replaceCorrespondent(label), onDelete: (context, label) => - context.read().removeCorrespondent(label), + context.read().removeCorrespondent(label), canDelete: context .watch() .paperlessUser 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 824e0e7..286638d 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 @@ -2,8 +2,8 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; -import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart'; import 'package:paperless_mobile/features/edit_label/view/edit_label_page.dart'; +import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart'; class EditDocumentTypePage extends StatelessWidget { final DocumentType documentType; @@ -12,16 +12,16 @@ class EditDocumentTypePage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => EditLabelCubit( + create: (context) => LabelCubit( context.read(), ), child: EditLabelPage( label: documentType, fromJsonT: DocumentType.fromJson, onSubmit: (context, label) => - context.read().replaceDocumentType(label), + context.read().replaceDocumentType(label), onDelete: (context, label) => - context.read().removeDocumentType(label), + context.read().removeDocumentType(label), canDelete: context .watch() .paperlessUser 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 91d512c..775676b 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 @@ -2,8 +2,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/database/tables/local_user_account.dart'; -import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart'; import 'package:paperless_mobile/features/edit_label/view/edit_label_page.dart'; +import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart'; import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_autofill_form_builder_field.dart'; class EditStoragePathPage extends StatelessWidget { @@ -13,16 +13,16 @@ class EditStoragePathPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => EditLabelCubit( + create: (context) => LabelCubit( context.read(), ), child: EditLabelPage( label: storagePath, fromJsonT: StoragePath.fromJson, onSubmit: (context, label) => - context.read().replaceStoragePath(label), + context.read().replaceStoragePath(label), onDelete: (context, label) => - context.read().removeStoragePath(label), + context.read().removeStoragePath(label), canDelete: context .watch() .paperlessUser 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 c9af9bd..2d85496 100644 --- a/lib/features/edit_label/view/impl/edit_tag_page.dart +++ b/lib/features/edit_label/view/impl/edit_tag_page.dart @@ -4,8 +4,8 @@ import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/widgets/form_builder_fields/form_builder_color_picker.dart'; -import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart'; import 'package:paperless_mobile/features/edit_label/view/edit_label_page.dart'; +import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; class EditTagPage extends StatelessWidget { @@ -16,16 +16,16 @@ class EditTagPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => EditLabelCubit( + create: (context) => LabelCubit( context.read(), ), child: EditLabelPage( label: tag, fromJsonT: Tag.fromJson, onSubmit: (context, label) => - context.read().replaceTag(label), + context.read().replaceTag(label), onDelete: (context, label) => - context.read().removeTag(label), + context.read().removeTag(label), canDelete: context.watch().paperlessUser.canDeleteTags, additionalFields: [ diff --git a/lib/features/home/view/home_shell_widget.dart b/lib/features/home/view/home_shell_widget.dart index 0c2ef47..ed7fb7e 100644 --- a/lib/features/home/view/home_shell_widget.dart +++ b/lib/features/home/view/home_shell_widget.dart @@ -53,8 +53,8 @@ class HomeShellWidget extends StatelessWidget { builder: (context, box, _) { if (currentUserId == null) { //This only happens during logout... - //TODO: Find way so this does not occur anymore - return SizedBox.shrink(); + //FIXME: Find way so this does not occur anymore + return const SizedBox.shrink(); } final currentLocalUser = box.get(currentUserId)!; return MultiProvider( @@ -107,36 +107,31 @@ class HomeShellWidget extends StatelessWidget { ), if (currentLocalUser.hasMultiUserSupport) Provider( - create: (context) => PaperlessUserApiV3Impl( + create: (context) => paperlessProviderFactory.createUserApi( context.read().client, + apiVersion: paperlessApiVersion, ), ), ], builder: (context, _) { return MultiProvider( providers: [ - Provider( + ChangeNotifierProvider( create: (context) { - final repo = LabelRepository(context.read()); - if (currentLocalUser - .paperlessUser.canViewCorrespondents) { - repo.findAllCorrespondents(); - } - if (currentLocalUser - .paperlessUser.canViewDocumentTypes) { - repo.findAllDocumentTypes(); - } - if (currentLocalUser.paperlessUser.canViewTags) { - repo.findAllTags(); - } - if (currentLocalUser - .paperlessUser.canViewStoragePaths) { - repo.findAllStoragePaths(); - } - return repo; + return LabelRepository(context.read()) + ..initialize( + loadCorrespondents: currentLocalUser + .paperlessUser.canViewCorrespondents, + loadDocumentTypes: currentLocalUser + .paperlessUser.canViewDocumentTypes, + loadStoragePaths: currentLocalUser + .paperlessUser.canViewStoragePaths, + loadTags: + currentLocalUser.paperlessUser.canViewTags, + ); }, ), - Provider( + ChangeNotifierProvider( create: (context) { final repo = SavedViewRepository(context.read()); if (currentLocalUser.paperlessUser.canViewSavedViews) { @@ -145,6 +140,12 @@ class HomeShellWidget extends StatelessWidget { return repo; }, ), + if (currentLocalUser.hasMultiUserSupport) + Provider( + create: (context) => UserRepository( + context.read(), + )..initialize(), + ), ], builder: (context, _) { return MultiProvider( @@ -152,7 +153,6 @@ class HomeShellWidget extends StatelessWidget { Provider( lazy: false, create: (context) => DocumentsCubit( - context.read(), context.read(), context.read(), Hive.box( @@ -196,12 +196,6 @@ class HomeShellWidget extends StatelessWidget { context.read(), ), ), - if (currentLocalUser.hasMultiUserSupport) - Provider( - create: (context) => UserRepository( - context.read(), - )..initialize(), - ), ], child: child, ); diff --git a/lib/features/inbox/cubit/inbox_cubit.dart b/lib/features/inbox/cubit/inbox_cubit.dart index 68de175..a405361 100644 --- a/lib/features/inbox/cubit/inbox_cubit.dart +++ b/lib/features/inbox/cubit/inbox_cubit.dart @@ -6,7 +6,6 @@ import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/features/logging/data/logger.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/label_repository_state.dart'; import 'package:paperless_mobile/core/service/connectivity_status_service.dart'; import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart'; import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart'; @@ -37,7 +36,7 @@ class InboxCubit extends HydratedCubit this._labelRepository, this.notifier, this.connectivityStatusService, - ) : super(InboxState(labels: _labelRepository.state)) { + ) : super(const InboxState()) { notifier.addListener( this, onDeleted: remove, @@ -62,12 +61,6 @@ class InboxCubit extends HydratedCubit } }, ); - _labelRepository.addListener( - this, - onChanged: (labels) { - emit(state.copyWith(labels: labels)); - }, - ); } @override @@ -112,7 +105,7 @@ class InboxCubit extends HydratedCubit if (inboxTags.isEmpty) { // no inbox tags = no inbox items. - return emit( + return emit( state.copyWith( hasLoaded: true, value: [], @@ -256,7 +249,6 @@ class InboxCubit extends HydratedCubit @override Future close() { - _labelRepository.removeListener(this); return super.close(); } diff --git a/lib/features/inbox/cubit/inbox_state.dart b/lib/features/inbox/cubit/inbox_state.dart index 5dc8f7e..7d31696 100644 --- a/lib/features/inbox/cubit/inbox_state.dart +++ b/lib/features/inbox/cubit/inbox_state.dart @@ -4,8 +4,6 @@ part of 'inbox_cubit.dart'; class InboxState extends DocumentPagingState { final Iterable inboxTags; - final LabelRepositoryState labels; - final int itemsInInboxCount; @JsonKey() @@ -19,7 +17,6 @@ class InboxState extends DocumentPagingState { this.inboxTags = const [], this.isHintAcknowledged = false, this.itemsInInboxCount = 0, - this.labels = const LabelRepositoryState(), }); @override @@ -32,7 +29,6 @@ class InboxState extends DocumentPagingState { documents, isHintAcknowledged, itemsInInboxCount, - labels, ]; InboxState copyWith({ @@ -42,7 +38,6 @@ class InboxState extends DocumentPagingState { List>? value, DocumentFilter? filter, bool? isHintAcknowledged, - LabelRepositoryState? labels, Map? suggestions, int? itemsInInboxCount, }) { @@ -52,7 +47,6 @@ class InboxState extends DocumentPagingState { value: value ?? super.value, inboxTags: inboxTags ?? this.inboxTags, isHintAcknowledged: isHintAcknowledged ?? this.isHintAcknowledged, - labels: labels ?? this.labels, filter: filter ?? super.filter, itemsInInboxCount: itemsInInboxCount ?? this.itemsInInboxCount, ); diff --git a/lib/features/inbox/view/widgets/inbox_item.dart b/lib/features/inbox/view/widgets/inbox_item.dart index 1b74cc9..79c8590 100644 --- a/lib/features/inbox/view/widgets/inbox_item.dart +++ b/lib/features/inbox/view/widgets/inbox_item.dart @@ -4,6 +4,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/extensions/document_extensions.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:paperless_mobile/core/util/lambda_utils.dart'; import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart'; import 'package:paperless_mobile/core/workarounds/colored_chip.dart'; import 'package:paperless_mobile/core/extensions/flutter_extensions.dart'; @@ -148,6 +150,7 @@ class _InboxItemState extends State { @override Widget build(BuildContext context) { + final labelRepository = context.read(); return BlocBuilder( builder: (context, state) { return GestureDetector( @@ -193,7 +196,7 @@ class _InboxItemState extends State { ?.fontSize, ), LabelText( - label: state.labels.correspondents[ + label: labelRepository.correspondents[ widget.document.correspondent], style: Theme.of(context).textTheme.bodyMedium, placeholder: "-", @@ -208,7 +211,7 @@ class _InboxItemState extends State { ?.fontSize, ), LabelText( - label: state.labels.documentTypes[ + label: labelRepository.documentTypes[ widget.document.documentType], style: Theme.of(context).textTheme.bodyMedium, placeholder: "-", @@ -217,8 +220,8 @@ class _InboxItemState extends State { const Spacer(), TagsWidget( tags: widget.document.tags - .map((e) => state.labels.tags[e]) - .whereNot((e) => e == null) + .map((e) => labelRepository.tags[e]) + .where(isNotNull) .toList() .cast(), isClickable: false, diff --git a/lib/features/labels/cubit/label_cubit.dart b/lib/features/labels/cubit/label_cubit.dart index c8d6d5f..d9efbe4 100644 --- a/lib/features/labels/cubit/label_cubit.dart +++ b/lib/features/labels/cubit/label_cubit.dart @@ -2,41 +2,134 @@ import 'package:bloc/bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; -import 'package:paperless_mobile/features/labels/cubit/label_cubit_mixin.dart'; part 'label_cubit.freezed.dart'; part 'label_state.dart'; -class LabelCubit extends Cubit with LabelCubitMixin { - @override +class LabelCubit extends Cubit { final LabelRepository labelRepository; LabelCubit(this.labelRepository) : super(const LabelState()) { labelRepository.addListener( - this, - onChanged: (labels) { + () { emit(state.copyWith( - correspondents: labels.correspondents, - documentTypes: labels.documentTypes, - storagePaths: labels.storagePaths, - tags: labels.tags, + correspondents: labelRepository.correspondents, + documentTypes: labelRepository.documentTypes, + storagePaths: labelRepository.storagePaths, + tags: labelRepository.tags, )); }, ); } - Future reload() { - return Future.wait([ - labelRepository.findAllCorrespondents(), - labelRepository.findAllDocumentTypes(), - labelRepository.findAllTags(), - labelRepository.findAllStoragePaths(), - ]); + Future reload({ + required bool loadCorrespondents, + required bool loadDocumentTypes, + required bool loadStoragePaths, + required bool loadTags, + }) { + return labelRepository.initialize( + loadCorrespondents: loadCorrespondents, + loadDocumentTypes: loadDocumentTypes, + loadStoragePaths: loadStoragePaths, + loadTags: loadTags, + ); + } + + Future addCorrespondent(Correspondent item) async { + assert(item.id == null); + final addedItem = await labelRepository.createCorrespondent(item); + return addedItem; + } + + Future reloadCorrespondents() { + return labelRepository.findAllCorrespondents(); + } + + Future replaceCorrespondent(Correspondent item) async { + assert(item.id != null); + final updatedItem = await labelRepository.updateCorrespondent(item); + return updatedItem; + } + + Future removeCorrespondent(Correspondent item) async { + assert(item.id != null); + if (labelRepository.correspondents.containsKey(item.id)) { + await labelRepository.deleteCorrespondent(item); + } + } + + Future addDocumentType(DocumentType item) async { + assert(item.id == null); + final addedItem = await labelRepository.createDocumentType(item); + return addedItem; + } + + Future reloadDocumentTypes() { + return labelRepository.findAllDocumentTypes(); + } + + Future replaceDocumentType(DocumentType item) async { + assert(item.id != null); + final updatedItem = await labelRepository.updateDocumentType(item); + return updatedItem; + } + + Future removeDocumentType(DocumentType item) async { + assert(item.id != null); + if (labelRepository.documentTypes.containsKey(item.id)) { + await labelRepository.deleteDocumentType(item); + } + } + + Future addStoragePath(StoragePath item) async { + assert(item.id == null); + final addedItem = await labelRepository.createStoragePath(item); + return addedItem; + } + + Future reloadStoragePaths() { + return labelRepository.findAllStoragePaths(); + } + + Future replaceStoragePath(StoragePath item) async { + assert(item.id != null); + final updatedItem = await labelRepository.updateStoragePath(item); + return updatedItem; + } + + Future removeStoragePath(StoragePath item) async { + assert(item.id != null); + if (labelRepository.storagePaths.containsKey(item.id)) { + await labelRepository.deleteStoragePath(item); + } + } + + Future addTag(Tag item) async { + assert(item.id == null); + final addedItem = await labelRepository.createTag(item); + return addedItem; + } + + Future reloadTags() { + return labelRepository.findAllTags(); + } + + Future replaceTag(Tag item) async { + assert(item.id != null); + final updatedItem = await labelRepository.updateTag(item); + return updatedItem; + } + + Future removeTag(Tag item) async { + assert(item.id != null); + if (labelRepository.tags.containsKey(item.id)) { + await labelRepository.deleteTag(item); + } } @override Future close() { - labelRepository.removeListener(this); return super.close(); } } diff --git a/lib/features/labels/cubit/label_cubit_mixin.dart b/lib/features/labels/cubit/label_cubit_mixin.dart deleted file mode 100644 index 020df25..0000000 --- a/lib/features/labels/cubit/label_cubit_mixin.dart +++ /dev/null @@ -1,102 +0,0 @@ -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/repository/label_repository.dart'; - -/// -/// Mixin which adds functionality to manage labels to [Bloc]s. -/// -mixin LabelCubitMixin on BlocBase { - LabelRepository get labelRepository; - - Future addCorrespondent(Correspondent item) async { - assert(item.id == null); - final addedItem = await labelRepository.createCorrespondent(item); - return addedItem; - } - - Future reloadCorrespondents() { - return labelRepository.findAllCorrespondents(); - } - - Future replaceCorrespondent(Correspondent item) async { - assert(item.id != null); - final updatedItem = await labelRepository.updateCorrespondent(item); - return updatedItem; - } - - Future removeCorrespondent(Correspondent item) async { - assert(item.id != null); - if (labelRepository.state.correspondents.containsKey(item.id)) { - await labelRepository.deleteCorrespondent(item); - } - } - - Future addDocumentType(DocumentType item) async { - assert(item.id == null); - final addedItem = await labelRepository.createDocumentType(item); - return addedItem; - } - - Future reloadDocumentTypes() { - return labelRepository.findAllDocumentTypes(); - } - - Future replaceDocumentType(DocumentType item) async { - assert(item.id != null); - final updatedItem = await labelRepository.updateDocumentType(item); - return updatedItem; - } - - Future removeDocumentType(DocumentType item) async { - assert(item.id != null); - if (labelRepository.state.documentTypes.containsKey(item.id)) { - await labelRepository.deleteDocumentType(item); - } - } - - Future addStoragePath(StoragePath item) async { - assert(item.id == null); - final addedItem = await labelRepository.createStoragePath(item); - return addedItem; - } - - Future reloadStoragePaths() { - return labelRepository.findAllStoragePaths(); - } - - Future replaceStoragePath(StoragePath item) async { - assert(item.id != null); - final updatedItem = await labelRepository.updateStoragePath(item); - return updatedItem; - } - - Future removeStoragePath(StoragePath item) async { - assert(item.id != null); - if (labelRepository.state.storagePaths.containsKey(item.id)) { - await labelRepository.deleteStoragePath(item); - } - } - - Future addTag(Tag item) async { - assert(item.id == null); - final addedItem = await labelRepository.createTag(item); - return addedItem; - } - - Future reloadTags() { - return labelRepository.findAllTags(); - } - - Future replaceTag(Tag item) async { - assert(item.id != null); - final updatedItem = await labelRepository.updateTag(item); - return updatedItem; - } - - Future removeTag(Tag item) async { - assert(item.id != null); - if (labelRepository.state.tags.containsKey(item.id)) { - await labelRepository.deleteTag(item); - } - } -} diff --git a/lib/features/linked_documents/cubit/linked_documents_cubit.dart b/lib/features/linked_documents/cubit/linked_documents_cubit.dart index c7fbeba..0a84624 100644 --- a/lib/features/linked_documents/cubit/linked_documents_cubit.dart +++ b/lib/features/linked_documents/cubit/linked_documents_cubit.dart @@ -19,28 +19,13 @@ class LinkedDocumentsCubit extends HydratedCubit final ConnectivityStatusService connectivityStatusService; @override final DocumentChangedNotifier notifier; - - final LabelRepository _labelRepository; - LinkedDocumentsCubit( DocumentFilter filter, this.api, this.notifier, - this._labelRepository, this.connectivityStatusService, ) : super(LinkedDocumentsState(filter: filter)) { updateFilter(filter: filter); - _labelRepository.addListener( - this, - onChanged: (labels) { - emit(state.copyWith( - correspondents: labels.correspondents, - documentTypes: labels.documentTypes, - tags: labels.tags, - storagePaths: labels.storagePaths, - )); - }, - ); notifier.addListener( this, onUpdated: replace, diff --git a/lib/features/linked_documents/cubit/linked_documents_state.dart b/lib/features/linked_documents/cubit/linked_documents_state.dart index aa75e66..0bca910 100644 --- a/lib/features/linked_documents/cubit/linked_documents_state.dart +++ b/lib/features/linked_documents/cubit/linked_documents_state.dart @@ -5,21 +5,12 @@ class LinkedDocumentsState extends DocumentPagingState { @JsonKey() final ViewType viewType; - final Map correspondents; - final Map documentTypes; - final Map storagePaths; - final Map tags; - const LinkedDocumentsState({ this.viewType = ViewType.list, super.filter = const DocumentFilter(), super.isLoading, super.hasLoaded, super.value, - this.correspondents = const {}, - this.documentTypes = const {}, - this.storagePaths = const {}, - this.tags = const {}, }); LinkedDocumentsState copyWith({ @@ -39,10 +30,6 @@ class LinkedDocumentsState extends DocumentPagingState { hasLoaded: hasLoaded ?? this.hasLoaded, value: value ?? this.value, viewType: viewType ?? this.viewType, - correspondents: correspondents ?? this.correspondents, - documentTypes: documentTypes ?? this.documentTypes, - storagePaths: storagePaths ?? this.storagePaths, - tags: tags ?? this.tags, ); } @@ -64,10 +51,6 @@ class LinkedDocumentsState extends DocumentPagingState { @override List get props => [ viewType, - correspondents, - documentTypes, - tags, - storagePaths, ...super.props, ]; diff --git a/lib/features/login/cubit/authentication_cubit.dart b/lib/features/login/cubit/authentication_cubit.dart index 6f3993d..121e779 100644 --- a/lib/features/login/cubit/authentication_cubit.dart +++ b/lib/features/login/cubit/authentication_cubit.dart @@ -112,6 +112,8 @@ class AuthenticationCubit extends Cubit { /// Switches to another account if it exists. Future switchAccount(String localUserId) async { emit(const SwitchingAccountsState()); + await FileService.instance.initialize(); + final redactedId = redactUserId(localUserId); logger.fd( 'Trying to switch to user $redactedId...', diff --git a/lib/features/login/view/add_account_page.dart b/lib/features/login/view/add_account_page.dart index 07eab01..fcc9aab 100644 --- a/lib/features/login/view/add_account_page.dart +++ b/lib/features/login/view/add_account_page.dart @@ -1,9 +1,13 @@ import 'dart:async'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/constants.dart'; import 'package:paperless_mobile/core/exception/server_message_exception.dart'; import 'package:paperless_mobile/core/model/info_message_exception.dart'; import 'package:paperless_mobile/core/service/connectivity_status_service.dart'; @@ -13,10 +17,13 @@ import 'package:paperless_mobile/features/login/model/client_certificate_form_mo import 'package:paperless_mobile/features/login/model/login_form_credentials.dart'; import 'package:paperless_mobile/features/login/model/reachability_status.dart'; import 'package:paperless_mobile/features/login/view/widgets/form_fields/client_certificate_form_field.dart'; +import 'package:paperless_mobile/features/login/view/widgets/form_fields/login_settings_page.dart'; import 'package:paperless_mobile/features/login/view/widgets/form_fields/server_address_form_field.dart'; import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_credentials_form_field.dart'; +import 'package:paperless_mobile/generated/assets.gen.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; +import 'package:paperless_mobile/routing/routes/app_logs_route.dart'; class AddAccountPage extends StatefulWidget { final FutureOr Function( @@ -58,10 +65,172 @@ class _AddAccountPageState extends State { final _formKey = GlobalKey(); bool _isCheckingConnection = false; ReachabilityStatus _reachabilityStatus = ReachabilityStatus.unknown; - bool _isFormSubmitted = false; + + final _pageController = PageController(); @override Widget build(BuildContext context) { + return Scaffold( + resizeToAvoidBottomInset: false, + appBar: AppBar( + title: Text(widget.titleText), + ), + body: FormBuilder( + key: _formKey, + child: AutofillGroup( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Assets.logos.paperlessLogoGreenPng.image( + width: 150, + height: 150, + ), + Text( + 'Paperless Mobile', + style: Theme.of(context).textTheme.displaySmall, + ).padded(), + SizedBox(height: 24), + Expanded( + child: PageView( + physics: NeverScrollableScrollPhysics(), + controller: _pageController, + allowImplicitScrolling: false, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + ServerAddressFormField( + onChanged: (value) { + setState(() { + _reachabilityStatus = ReachabilityStatus.unknown; + }); + }, + ).paddedSymmetrically( + horizontal: 12, + vertical: 12, + ), + ClientCertificateFormField( + initialBytes: widget.initialClientCertificate?.bytes, + initialPassphrase: + widget.initialClientCertificate?.passphrase, + ).padded(), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + //TODO: Move additional headers and client cert to separate page + // IconButton.filledTonal( + // onPressed: () { + // Navigator.of(context).push( + // MaterialPageRoute(builder: (context) { + // return LoginSettingsPage(); + // }), + // ); + // }, + // icon: Icon(Icons.settings), + // ), + SizedBox(width: 8), + FilledButton.icon( + onPressed: () async { + final status = await _updateReachability(); + if (status == ReachabilityStatus.reachable) { + Future.delayed(1.seconds, () { + _pageController.nextPage( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + }); + } + }, + icon: _isCheckingConnection + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Theme.of(context) + .colorScheme + .onSecondary, + ), + ) + : _reachabilityStatus == + ReachabilityStatus.reachable + ? Icon(Icons.done) + : Icon(Icons.arrow_forward), + label: Text(S.of(context)!.continueLabel), + ), + ], + ).paddedSymmetrically( + horizontal: 16, + vertical: 8, + ), + _buildStatusIndicator().padded(), + ], + ), + Column( + children: [ + UserCredentialsFormField( + formKey: _formKey, + initialUsername: widget.initialUsername, + initialPassword: widget.initialPassword, + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton.icon( + onPressed: () { + _pageController.previousPage( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + }, + icon: Icon(Icons.arrow_back), + label: Text(S.of(context)!.edit), + ), + FilledButton( + onPressed: () { + _onSubmit(); + }, + child: Text(S.of(context)!.signIn), + ), + ], + ).padded(), + Text( + S.of(context)!.loginRequiredPermissionsHint, + style: Theme.of(context).textTheme.bodySmall?.apply( + color: Theme.of(context) + .colorScheme + .onBackground + .withOpacity(0.6), + ), + ).padded(16), + ], + ), + ], + ), + ), + Text.rich( + TextSpan( + style: Theme.of(context).textTheme.labelLarge, + children: [ + TextSpan(text: S.of(context)!.version(packageInfo.version)), + WidgetSpan(child: SizedBox(width: 24)), + TextSpan( + style: TextStyle( + color: Theme.of(context).colorScheme.primary), + text: S.of(context)!.appLogs(''), + recognizer: TapGestureRecognizer() + ..onTap = () { + AppLogsRoute().push(context); + }, + ), + ], + ), + ).padded(), + ], + ), + ), + ), + ); return Scaffold( appBar: AppBar( title: Text(widget.titleText), @@ -91,7 +260,7 @@ class _AddAccountPageState extends State { children: [ ServerAddressFormField( initialValue: widget.initialServerUrl, - onSubmit: (address) { + onChanged: (address) { _updateReachability(address); }, ).padded(), @@ -117,7 +286,7 @@ class _AddAccountPageState extends State { .withOpacity(0.6), ), ).padded(16), - ] + ], ], ), ), @@ -125,7 +294,7 @@ class _AddAccountPageState extends State { ); } - Future _updateReachability([String? address]) async { + Future _updateReachability([String? address]) async { setState(() { _isCheckingConnection = true; }); @@ -150,13 +319,10 @@ class _AddAccountPageState extends State { _isCheckingConnection = false; _reachabilityStatus = status; }); + return status; } Widget _buildStatusIndicator() { - if (_isCheckingConnection) { - return const ListTile(); - } - Widget _buildIconText( IconData icon, String text, [ @@ -176,14 +342,6 @@ class _AddAccountPageState extends State { Color errorColor = Theme.of(context).colorScheme.error; switch (_reachabilityStatus) { - case ReachabilityStatus.unknown: - return Container(); - case ReachabilityStatus.reachable: - return _buildIconText( - Icons.done, - S.of(context)!.connectionSuccessfulylEstablished, - Colors.green, - ); case ReachabilityStatus.notReachable: return _buildIconText( Icons.close, @@ -214,6 +372,8 @@ class _AddAccountPageState extends State { S.of(context)!.connectionTimedOut, errorColor, ); + default: + return const ListTile(); } } diff --git a/lib/features/login/view/widgets/form_fields/client_certificate_form_field.dart b/lib/features/login/view/widgets/form_fields/client_certificate_form_field.dart index b29a2f7..1087489 100644 --- a/lib/features/login/view/widgets/form_fields/client_certificate_form_field.dart +++ b/lib/features/login/view/widgets/form_fields/client_certificate_form_field.dart @@ -7,7 +7,8 @@ import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:paperless_mobile/core/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/login/model/client_certificate_form_model.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; - +import 'package:paperless_mobile/helpers/message_helpers.dart'; +import 'package:path/path.dart' as p; import 'obscured_input_text_form_field.dart'; class ClientCertificateFormField extends StatefulWidget { @@ -16,10 +17,10 @@ class ClientCertificateFormField extends StatefulWidget { final String? initialPassphrase; final Uint8List? initialBytes; - final void Function(ClientCertificateFormModel? cert) onChanged; + final ValueChanged? onChanged; const ClientCertificateFormField({ super.key, - required this.onChanged, + this.onChanged, this.initialPassphrase, this.initialBytes, }); @@ -29,13 +30,15 @@ class ClientCertificateFormField extends StatefulWidget { _ClientCertificateFormFieldState(); } -class _ClientCertificateFormFieldState - extends State { +class _ClientCertificateFormFieldState extends State + with AutomaticKeepAliveClientMixin { File? _selectedFile; @override Widget build(BuildContext context) { + super.build(context); return FormBuilderField( key: const ValueKey('login-client-cert'), + name: ClientCertificateFormField.fkClientCertificate, onChanged: widget.onChanged, initialValue: widget.initialBytes != null ? ClientCertificateFormModel( @@ -43,16 +46,6 @@ class _ClientCertificateFormFieldState passphrase: widget.initialPassphrase, ) : null, - validator: (value) { - if (value == null) { - return null; - } - assert(_selectedFile != null); - if (_selectedFile?.path.split(".").last != 'pfx') { - return S.of(context)!.invalidCertificateFormat; - } - return null; - }, builder: (field) { final theme = Theme.of(context).copyWith(dividerColor: Colors.transparent); //new @@ -127,7 +120,6 @@ class _ClientCertificateFormFieldState ), ); }, - name: ClientCertificateFormField.fkClientCertificate, ); } @@ -140,6 +132,11 @@ class _ClientCertificateFormFieldState if (result == null || result.files.single.path == null) { return; } + final path = result.files.single.path!; + if (p.extension(path) != '.pfx') { + showSnackBar(context, S.of(context)!.invalidCertificateFormat); + return; + } File file = File(result.files.single.path!); setState(() { _selectedFile = file; @@ -171,4 +168,7 @@ class _ClientCertificateFormFieldState ); } } + + @override + bool get wantKeepAlive => true; } diff --git a/lib/features/login/view/widgets/form_fields/login_settings_page.dart b/lib/features/login/view/widgets/form_fields/login_settings_page.dart new file mode 100644 index 0000000..cfa7103 --- /dev/null +++ b/lib/features/login/view/widgets/form_fields/login_settings_page.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:paperless_mobile/features/login/view/widgets/form_fields/client_certificate_form_field.dart'; +import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; + +class LoginSettingsPage extends StatelessWidget { + const LoginSettingsPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(S.of(context)!.settings), + ), + body: ListView( + children: [ + ClientCertificateFormField(onChanged: (certificate) {}), + ], + ), + ); + } +} diff --git a/lib/features/login/view/widgets/form_fields/server_address_form_field.dart b/lib/features/login/view/widgets/form_fields/server_address_form_field.dart index b03ed15..171ab27 100644 --- a/lib/features/login/view/widgets/form_fields/server_address_form_field.dart +++ b/lib/features/login/view/widgets/form_fields/server_address_form_field.dart @@ -9,10 +9,11 @@ import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; class ServerAddressFormField extends StatefulWidget { static const String fkServerAddress = "serverAddress"; final String? initialValue; - final void Function(String? address) onSubmit; + final ValueChanged? onChanged; + const ServerAddressFormField({ Key? key, - required this.onSubmit, + this.onChanged, this.initialValue, }) : super(key: key); @@ -20,8 +21,10 @@ class ServerAddressFormField extends StatefulWidget { State createState() => _ServerAddressFormFieldState(); } -class _ServerAddressFormFieldState extends State { +class _ServerAddressFormFieldState extends State + with AutomaticKeepAliveClientMixin { bool _canClear = false; + final _textFieldKey = GlobalKey(); @override void initState() { @@ -38,10 +41,12 @@ class _ServerAddressFormFieldState extends State { @override Widget build(BuildContext context) { + super.build(context); return FormBuilderField( initialValue: widget.initialValue, name: ServerAddressFormField.fkServerAddress, autovalidateMode: AutovalidateMode.onUserInteraction, + onChanged: widget.onChanged, builder: (field) { return RawAutocomplete( focusNode: _focusNode, @@ -51,6 +56,7 @@ class _ServerAddressFormFieldState extends State { onSelected: onSelected, options: options, maxOptionsHeight: 200.0, + maxWidth: MediaQuery.sizeOf(context).width - 40, ); }, key: const ValueKey('login-server-address'), @@ -60,12 +66,12 @@ class _ServerAddressFormFieldState extends State { .where((element) => element.contains(textEditingValue.text)); }, onSelected: (option) { - _formatInput(); - field.didChange(_textEditingController.text); + _formatInput(field); }, fieldViewBuilder: (context, textEditingController, focusNode, onFieldSubmitted) { return TextFormField( + key: _textFieldKey, controller: textEditingController, focusNode: focusNode, decoration: InputDecoration( @@ -78,15 +84,22 @@ class _ServerAddressFormFieldState extends State { onPressed: () { textEditingController.clear(); field.didChange(textEditingController.text); - widget.onSubmit(textEditingController.text); }, ) : null, ), autofocus: false, onFieldSubmitted: (_) { + _formatInput(field); onFieldSubmitted(); - _formatInput(); + }, + onTapOutside: (event) { + if (!FocusScope.of(context).hasFocus) { + return; + } + _formatInput(field); + onFieldSubmitted(); + FocusScope.of(context).unfocus(); }, autovalidateMode: AutovalidateMode.onUserInteraction, validator: (value) { @@ -113,7 +126,7 @@ class _ServerAddressFormFieldState extends State { ); } - void _formatInput() { + void _formatInput(FormFieldState field) { String address = _textEditingController.text.trim(); address = address.replaceAll(RegExp(r'^\/+|\/+$'), ''); _textEditingController.text = address; @@ -121,8 +134,11 @@ class _ServerAddressFormFieldState extends State { baseOffset: address.length, extentOffset: address.length, ); - widget.onSubmit(address); + field.didChange(_textEditingController.text); } + + @override + bool get wantKeepAlive => true; } /// Taken from [Autocomplete] @@ -131,12 +147,14 @@ class _AutocompleteOptions extends StatelessWidget { required this.onSelected, required this.options, required this.maxOptionsHeight, + required this.maxWidth, }); final AutocompleteOnSelected onSelected; final Iterable options; final double maxOptionsHeight; + final double maxWidth; @override Widget build(BuildContext context) { @@ -145,7 +163,10 @@ class _AutocompleteOptions extends StatelessWidget { child: Material( elevation: 4.0, child: ConstrainedBox( - constraints: BoxConstraints(maxHeight: maxOptionsHeight), + constraints: BoxConstraints( + maxHeight: maxOptionsHeight, + maxWidth: maxWidth, + ), child: ListView.builder( padding: EdgeInsets.zero, shrinkWrap: true, diff --git a/lib/features/login/view/widgets/form_fields/user_credentials_form_field.dart b/lib/features/login/view/widgets/form_fields/user_credentials_form_field.dart index 2f55bdc..a1bf105 100644 --- a/lib/features/login/view/widgets/form_fields/user_credentials_form_field.dart +++ b/lib/features/login/view/widgets/form_fields/user_credentials_form_field.dart @@ -12,13 +12,13 @@ import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; class UserCredentialsFormField extends StatefulWidget { static const fkCredentials = 'credentials'; - final void Function() onFieldsSubmitted; + final VoidCallback? onFieldsSubmitted; final String? initialUsername; final String? initialPassword; final GlobalKey formKey; const UserCredentialsFormField({ Key? key, - required this.onFieldsSubmitted, + this.onFieldsSubmitted, this.initialUsername, this.initialPassword, required this.formKey, @@ -29,12 +29,14 @@ class UserCredentialsFormField extends StatefulWidget { _UserCredentialsFormFieldState(); } -class _UserCredentialsFormFieldState extends State { +class _UserCredentialsFormFieldState extends State + with AutomaticKeepAliveClientMixin { final _usernameFocusNode = FocusNode(); final _passwordFocusNode = FocusNode(); @override Widget build(BuildContext context) { + super.build(context); return FormBuilderField( initialValue: LoginFormCredentials( password: widget.initialPassword, @@ -87,7 +89,7 @@ class _UserCredentialsFormFieldState extends State { LoginFormCredentials(password: password), ), onFieldSubmitted: (_) { - widget.onFieldsSubmitted(); + widget.onFieldsSubmitted?.call(); }, validator: (value) { if (value?.trim().isEmpty ?? true) { @@ -100,6 +102,9 @@ class _UserCredentialsFormFieldState extends State { ), ); } + + @override + bool get wantKeepAlive => true; } /** diff --git a/lib/features/login/view/widgets/login_transition_page.dart b/lib/features/login/view/widgets/login_transition_page.dart index 5bbcb19..ccd93f5 100644 --- a/lib/features/login/view/widgets/login_transition_page.dart +++ b/lib/features/login/view/widgets/login_transition_page.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:paperless_mobile/core/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; +import 'package:paperless_mobile/routing/routes/app_logs_route.dart'; import 'package:paperless_mobile/theme.dart'; class LoginTransitionPage extends StatelessWidget { @@ -20,10 +22,25 @@ class LoginTransitionPage extends StatelessWidget { body: Stack( alignment: Alignment.center, children: [ - const CircularProgressIndicator(), + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 16), + Align( + alignment: Alignment.bottomCenter, + child: Text(text).paddedOnly(bottom: 24), + ), + ], + ), Align( alignment: Alignment.bottomCenter, - child: Text(text).paddedOnly(bottom: 24), + child: TextButton( + child: Text(S.of(context)!.appLogs('')), + onPressed: () { + AppLogsRoute().push(context); + }, + ), ), ], ).padded(16), diff --git a/lib/features/saved_view/cubit/saved_view_cubit.dart b/lib/features/saved_view/cubit/saved_view_cubit.dart index a2c255b..2642fd6 100644 --- a/lib/features/saved_view/cubit/saved_view_cubit.dart +++ b/lib/features/saved_view/cubit/saved_view_cubit.dart @@ -13,17 +13,14 @@ class SavedViewCubit extends Cubit { SavedViewCubit(this._savedViewRepository) : super(const SavedViewState.initial()) { - _savedViewRepository.addListener( - this, - onChanged: (views) { - views.when( - initial: (savedViews) => emit(const SavedViewState.initial()), - loading: (savedViews) => emit(const SavedViewState.loading()), - loaded: (savedViews) => - emit(SavedViewState.loaded(savedViews: savedViews)), - error: (savedViews) => emit(const SavedViewState.error()), - ); - }, + _savedViewRepository.addListener(_onSavedViewsChanged); + } + + void _onSavedViewsChanged() { + emit( + SavedViewState.loaded( + savedViews: _savedViewRepository.savedViews, + ), ); } @@ -53,7 +50,7 @@ class SavedViewCubit extends Cubit { @override Future close() { - _savedViewRepository.removeListener(this); + _savedViewRepository.removeListener(_onSavedViewsChanged); return super.close(); } } diff --git a/lib/features/saved_view_details/cubit/saved_view_details_cubit.dart b/lib/features/saved_view_details/cubit/saved_view_details_cubit.dart index 33af9fa..0305a2d 100644 --- a/lib/features/saved_view_details/cubit/saved_view_details_cubit.dart +++ b/lib/features/saved_view_details/cubit/saved_view_details_cubit.dart @@ -34,32 +34,13 @@ class SavedViewDetailsCubit extends Cubit required this.savedView, int initialCount = 25, }) : super( - SavedViewDetailsState( - correspondents: _labelRepository.state.correspondents, - documentTypes: _labelRepository.state.documentTypes, - tags: _labelRepository.state.tags, - storagePaths: _labelRepository.state.storagePaths, - viewType: _userState.savedViewsViewType, - ), + SavedViewDetailsState(viewType: _userState.savedViewsViewType), ) { notifier.addListener( this, onDeleted: remove, onUpdated: replace, ); - _labelRepository.addListener( - this, - onChanged: (labels) { - if (!isClosed) { - emit(state.copyWith( - correspondents: labels.correspondents, - documentTypes: labels.documentTypes, - tags: labels.tags, - storagePaths: labels.storagePaths, - )); - } - }, - ); updateFilter( filter: savedView.toDocumentFilter().copyWith( page: 1, diff --git a/lib/features/sharing/view/widgets/event_listener_shell.dart b/lib/features/sharing/view/widgets/event_listener_shell.dart index 19f1aed..66f6786 100644 --- a/lib/features/sharing/view/widgets/event_listener_shell.dart +++ b/lib/features/sharing/view/widgets/event_listener_shell.dart @@ -14,6 +14,7 @@ import 'package:paperless_mobile/core/database/hive/hive_extensions.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/service/connectivity_status_service.dart'; +import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart'; import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart'; import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart'; diff --git a/lib/features/similar_documents/cubit/similar_documents_cubit.dart b/lib/features/similar_documents/cubit/similar_documents_cubit.dart index 563aeba..12d628c 100644 --- a/lib/features/similar_documents/cubit/similar_documents_cubit.dart +++ b/lib/features/similar_documents/cubit/similar_documents_cubit.dart @@ -3,6 +3,7 @@ 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/service/connectivity_status_service.dart'; +import 'package:paperless_mobile/features/logging/data/logger.dart'; import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart'; import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart'; @@ -17,15 +18,12 @@ class SimilarDocumentsCubit extends Cubit @override final PaperlessDocumentsApi api; - final LabelRepository _labelRepository; - @override final DocumentChangedNotifier notifier; SimilarDocumentsCubit( this.api, this.notifier, - this._labelRepository, this.connectivityStatusService, { required this.documentId, }) : super(const SimilarDocumentsState(filter: DocumentFilter())) { @@ -39,19 +37,30 @@ class SimilarDocumentsCubit extends Cubit @override Future initialize() async { if (!state.hasLoaded) { - await updateFilter( - filter: state.filter.copyWith( - moreLike: () => documentId, - sortField: SortField.score, - ), - ); + try { + await updateFilter( + filter: state.filter.copyWith( + moreLike: () => documentId, + sortField: SortField.score, + ), + ); + emit(state.copyWith(error: null)); + } on PaperlessApiException catch (e, stackTrace) { + logger.fe( + "An error occurred while loading similar documents for document $documentId", + className: "SimilarDocumentsCubit", + methodName: "initialize", + error: e.details, + stackTrace: stackTrace, + ); + emit(state.copyWith(error: e.code)); + } } } @override Future close() { notifier.removeListener(this); - _labelRepository.removeListener(this); return super.close(); } diff --git a/lib/features/similar_documents/cubit/similar_documents_state.dart b/lib/features/similar_documents/cubit/similar_documents_state.dart index e006fa6..86a4af5 100644 --- a/lib/features/similar_documents/cubit/similar_documents_state.dart +++ b/lib/features/similar_documents/cubit/similar_documents_state.dart @@ -1,19 +1,22 @@ part of 'similar_documents_cubit.dart'; class SimilarDocumentsState extends DocumentPagingState { + final ErrorCode? error; const SimilarDocumentsState({ required super.filter, super.hasLoaded, super.isLoading, super.value, + this.error, }); @override - List get props => [ + List get props => [ filter, hasLoaded, isLoading, value, + error, ]; @override @@ -36,12 +39,14 @@ class SimilarDocumentsState extends DocumentPagingState { bool? isLoading, List>? value, DocumentFilter? filter, + ErrorCode? error, }) { return SimilarDocumentsState( hasLoaded: hasLoaded ?? this.hasLoaded, isLoading: isLoading ?? this.isLoading, value: value ?? this.value, filter: filter ?? this.filter, + error: error, ); } } diff --git a/lib/features/similar_documents/view/similar_documents_view.dart b/lib/features/similar_documents/view/similar_documents_view.dart index 21ccd2c..c227236 100644 --- a/lib/features/similar_documents/view/similar_documents_view.dart +++ b/lib/features/similar_documents/view/similar_documents_view.dart @@ -3,6 +3,8 @@ 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/core/extensions/document_extensions.dart'; +import 'package:paperless_mobile/core/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart'; import 'package:paperless_mobile/core/widgets/offline_widget.dart'; import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart'; import 'package:paperless_mobile/features/paged_document_view/view/document_paging_view_mixin.dart'; @@ -49,6 +51,16 @@ class _SimilarDocumentsViewState extends State child: OfflineWidget(), ); } + if (state.error != null) { + return SliverFillRemaining( + child: Center( + child: Text( + translateError(context, state.error!), + textAlign: TextAlign.center, + ), + ).padded(), + ); + } if (state.hasLoaded && !state.isLoading && state.documents.isEmpty) { diff --git a/lib/l10n/intl_ca.arb b/lib/l10n/intl_ca.arb index f639029..1915311 100644 --- a/lib/l10n/intl_ca.arb +++ b/lib/l10n/intl_ca.arb @@ -1010,14 +1010,19 @@ "couldNotLoadLogfileFrom": "No es pot carregar log desde {date}.", "loadingLogsFrom": "Carregant registres des de {date}...", "clearLogs": "Netejar registres des de {date}", - "showPdf": "Show PDF", + "showPdf": "Mostra PDF", "@showPdf": { "description": "Tooltip shown on the \"show pdf\" button on the document edit page" }, - "hidePdf": "Hide PDF", + "hidePdf": "Oculta PDF", "@hidePdf": { "description": "Tooltip shown on the \"show pdf\" icon button on the document edit page" }, - "misc": "Miscellaneous", - "loggingOut": "Logging out..." + "misc": "Miscel·lanni", + "loggingOut": "Sortint...", + "testingConnection": "Provant connexió...", + "@testingConnection": { + "description": "Text shown while the app tries to establish a connection to the specified host." + }, + "version": "Versió {versionCode}" } \ No newline at end of file diff --git a/lib/l10n/intl_cs.arb b/lib/l10n/intl_cs.arb index 19be896..32c55a9 100644 --- a/lib/l10n/intl_cs.arb +++ b/lib/l10n/intl_cs.arb @@ -1019,5 +1019,10 @@ "description": "Tooltip shown on the \"show pdf\" icon button on the document edit page" }, "misc": "Miscellaneous", - "loggingOut": "Logging out..." + "loggingOut": "Logging out...", + "testingConnection": "Testing connection...", + "@testingConnection": { + "description": "Text shown while the app tries to establish a connection to the specified host." + }, + "version": "Version {versionCode}" } \ No newline at end of file diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 7b7345b..6118dd3 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -1019,5 +1019,10 @@ "description": "Tooltip shown on the \"show pdf\" icon button on the document edit page" }, "misc": "Sonstige", - "loggingOut": "Abmelden..." + "loggingOut": "Abmelden...", + "testingConnection": "Teste Verbindung...", + "@testingConnection": { + "description": "Text shown while the app tries to establish a connection to the specified host." + }, + "version": "Version {versionCode}" } \ No newline at end of file diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 3dcfffe..84674b1 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -1019,5 +1019,10 @@ "description": "Tooltip shown on the \"show pdf\" icon button on the document edit page" }, "misc": "Miscellaneous", - "loggingOut": "Logging out..." + "loggingOut": "Logging out...", + "testingConnection": "Testing connection...", + "@testingConnection": { + "description": "Text shown while the app tries to establish a connection to the specified host." + }, + "version": "Version {versionCode}" } \ No newline at end of file diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 7e1ce9e..f05b474 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -873,7 +873,7 @@ "@donate": { "description": "Label of the in-app donate button" }, - "donationDialogContent": "¡Gracias por querer apoyar esta aplicación!\nDebido a las políticas de pago, tanto de Google como de Apple, no se puede mostrar ningún enlace que lo dirija a las donaciones. En este contexto, ni siquiera es posible enlazar la página del repositorio del proyecto. Por lo tanto, puedes visitar la sección \"Donations\" en el archivo README de este proyecto. Tu apoyo es valorado gratamente y ayuda a mantener con vida el desarrollo de esta aplicación.\n¡Muchas gracias!", + "donationDialogContent": "¡Gracias por querer apoyar esta aplicación!\nDebido a las políticas de pago, tanto de Google como de Apple, no se puede mostrar ningún enlace que lo dirija a las donaciones. En este contexto, ni siquiera es posible enlazar la página del repositorio del proyecto. Por lo tanto, puedes visitar la sección \"Donaciones\" en el archivo README de este proyecto. Tu apoyo es valorado gratamente y ayuda a mantener con vida el desarrollo de esta aplicación.\n¡Muchas gracias!", "@donationDialogContent": { "description": "Text displayed in the donation dialog" }, @@ -881,11 +881,11 @@ "@noDocumentsFound": { "description": "Message shown when no documents were found." }, - "couldNotDeleteCorrespondent": "No se pudo remover el interlocutor, intente nuevamente.", + "couldNotDeleteCorrespondent": "No se pudo borrar el interlocutor, intente nuevamente.", "@couldNotDeleteCorrespondent": { "description": "Message shown in snackbar when a correspondent could not be deleted." }, - "couldNotDeleteDocumentType": "No se pudo remover el tipo de documento, intente nuevamente.", + "couldNotDeleteDocumentType": "No se pudo borrar el tipo de documento, intente nuevamente.", "@couldNotDeleteDocumentType": { "description": "Message shown when a document type could not be deleted" }, @@ -893,7 +893,7 @@ "@couldNotDeleteTag": { "description": "Message shown when a tag could not be deleted" }, - "couldNotDeleteStoragePath": "No se pudo remover la ruta de almacenamiento, intente nuevamente.", + "couldNotDeleteStoragePath": "No se pudo borrar la ruta de almacenamiento, intente nuevamente.", "@couldNotDeleteStoragePath": { "description": "Message shown when a storage path could not be deleted" }, @@ -934,7 +934,7 @@ "description": "Message shown when a saved view could not be updated" }, "couldNotUpdateStoragePath": "No se pudo actualizar la ruta de almacenamiento, intente nuevamente.", - "savedViewSuccessfullyUpdated": "La vista guardada se actualizó correctamente.", + "savedViewSuccessfullyUpdated": "Vista guardada actualizada correctamente.", "@savedViewSuccessfullyUpdated": { "description": "Message shown when a saved view was successfully updated." }, @@ -984,7 +984,7 @@ "@authenticatingDots": { "description": "Message shown when the app is authenticating the user" }, - "persistingUserInformation": "Preservando información del usuario...", + "persistingUserInformation": "Guardando información del usuario...", "fetchingUserInformation": "Obteniendo información del usuario...", "@fetchingUserInformation": { "description": "Message shown when the app loads user data from the server" @@ -1001,7 +1001,7 @@ "@discardChangesWarning": { "description": "Warning message shown when the user tries to close a route without saving the changes." }, - "changelog": "Changelog", + "changelog": "Registro de cambios", "noLogsFoundOn": "No se encontraron registros en {date}.", "logfileBottomReached": "Has alcanzado el final del archivo de registro.", "appLogs": "Registros de la aplicación {date}", @@ -1010,14 +1010,19 @@ "couldNotLoadLogfileFrom": "No se pudo cargar el archivo de registro desde {date}.", "loadingLogsFrom": "Cargando registros desde {date}...", "clearLogs": "Limpiar registros desde {date}", - "showPdf": "Show PDF", + "showPdf": "Mostrar PDF", "@showPdf": { "description": "Tooltip shown on the \"show pdf\" button on the document edit page" }, - "hidePdf": "Hide PDF", + "hidePdf": "Ocultar PDF", "@hidePdf": { "description": "Tooltip shown on the \"show pdf\" icon button on the document edit page" }, - "misc": "Miscellaneous", - "loggingOut": "Logging out..." + "misc": "Otros", + "loggingOut": "Cerrando sesión...", + "testingConnection": "Testing connection...", + "@testingConnection": { + "description": "Text shown while the app tries to establish a connection to the specified host." + }, + "version": "Version {versionCode}" } \ No newline at end of file diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index c33ce6b..471430d 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -1019,5 +1019,10 @@ "description": "Tooltip shown on the \"show pdf\" icon button on the document edit page" }, "misc": "Sonstige", - "loggingOut": "Logging out..." + "loggingOut": "Logging out...", + "testingConnection": "Testing connection...", + "@testingConnection": { + "description": "Text shown while the app tries to establish a connection to the specified host." + }, + "version": "Version {versionCode}" } \ No newline at end of file diff --git a/lib/l10n/intl_nl.arb b/lib/l10n/intl_nl.arb new file mode 100644 index 0000000..84674b1 --- /dev/null +++ b/lib/l10n/intl_nl.arb @@ -0,0 +1,1028 @@ +{ + "developedBy": "Developed by {name}.", + "@developedBy": { + "placeholders": { + "name": {} + } + }, + "addAnotherAccount": "Add another account", + "@addAnotherAccount": {}, + "account": "Account", + "@account": {}, + "addCorrespondent": "New Correspondent", + "@addCorrespondent": { + "description": "Title when adding a new correspondent" + }, + "addDocumentType": "New Document Type", + "@addDocumentType": { + "description": "Title when adding a new document type" + }, + "addStoragePath": "New Storage Path", + "@addStoragePath": { + "description": "Title when adding a new storage path" + }, + "addTag": "New Tag", + "@addTag": { + "description": "Title when adding a new tag" + }, + "aboutThisApp": "About this app", + "@aboutThisApp": { + "description": "Label for about this app tile displayed in the drawer" + }, + "loggedInAs": "Logged in as {name}", + "@loggedInAs": { + "placeholders": { + "name": {} + } + }, + "disconnect": "Disconnect", + "@disconnect": { + "description": "Logout button label" + }, + "reportABug": "Report a Bug", + "@reportABug": {}, + "settings": "Settings", + "@settings": {}, + "authenticateOnAppStart": "Authenticate on app start", + "@authenticateOnAppStart": { + "description": "Description of the biometric authentication settings tile" + }, + "biometricAuthentication": "Biometric authentication", + "@biometricAuthentication": {}, + "authenticateToToggleBiometricAuthentication": "{mode, select, enable{Authenticate to enable biometric authentication} disable{Authenticate to disable biometric authentication} other{}}", + "@authenticateToToggleBiometricAuthentication": { + "placeholders": { + "mode": {} + } + }, + "documents": "Documents", + "@documents": {}, + "inbox": "Inbox", + "@inbox": {}, + "labels": "Labels", + "@labels": {}, + "scanner": "Scanner", + "@scanner": {}, + "startTyping": "Start typing...", + "@startTyping": {}, + "doYouReallyWantToDeleteThisView": "Do you really want to delete this view?", + "@doYouReallyWantToDeleteThisView": {}, + "deleteView": "Delete view {name}?", + "@deleteView": {}, + "addedAt": "Added at", + "@addedAt": {}, + "archiveSerialNumber": "Archive Serial Number", + "@archiveSerialNumber": {}, + "asn": "ASN", + "@asn": {}, + "correspondent": "Correspondent", + "@correspondent": {}, + "createdAt": "Created at", + "@createdAt": {}, + "documentSuccessfullyDeleted": "Document successfully deleted.", + "@documentSuccessfullyDeleted": {}, + "assignAsn": "Assign ASN", + "@assignAsn": {}, + "deleteDocumentTooltip": "Delete", + "@deleteDocumentTooltip": { + "description": "Tooltip shown for the delete button on details page" + }, + "downloadDocumentTooltip": "Download", + "@downloadDocumentTooltip": { + "description": "Tooltip shown for the download button on details page" + }, + "editDocumentTooltip": "Edit", + "@editDocumentTooltip": { + "description": "Tooltip shown for the edit button on details page" + }, + "loadFullContent": "Load full content", + "@loadFullContent": {}, + "noAppToDisplayPDFFilesFound": "No app to display PDF files found!", + "@noAppToDisplayPDFFilesFound": {}, + "openInSystemViewer": "Open in system viewer", + "@openInSystemViewer": {}, + "couldNotOpenFilePermissionDenied": "Could not open file: Permission denied.", + "@couldNotOpenFilePermissionDenied": {}, + "previewTooltip": "Preview", + "@previewTooltip": { + "description": "Tooltip shown for the preview button on details page" + }, + "shareTooltip": "Share", + "@shareTooltip": { + "description": "Tooltip shown for the share button on details page" + }, + "similarDocuments": "Similar Documents", + "@similarDocuments": { + "description": "Label shown in the tabbar on details page" + }, + "content": "Content", + "@content": { + "description": "Label shown in the tabbar on details page" + }, + "metaData": "Meta Data", + "@metaData": { + "description": "Label shown in the tabbar on details page" + }, + "overview": "Overview", + "@overview": { + "description": "Label shown in the tabbar on details page" + }, + "documentType": "Document Type", + "@documentType": {}, + "archivedPdf": "Archived (pdf)", + "@archivedPdf": { + "description": "Option to chose when downloading a document" + }, + "chooseFiletype": "Choose filetype", + "@chooseFiletype": {}, + "original": "Original", + "@original": { + "description": "Option to chose when downloading a document" + }, + "documentSuccessfullyDownloaded": "Document successfully downloaded.", + "@documentSuccessfullyDownloaded": {}, + "suggestions": "Suggestions: ", + "@suggestions": {}, + "editDocument": "Edit Document", + "@editDocument": {}, + "advanced": "Advanced", + "@advanced": {}, + "apply": "Apply", + "@apply": {}, + "extended": "Extended", + "@extended": {}, + "titleAndContent": "Title & Content", + "@titleAndContent": {}, + "title": "Title", + "@title": {}, + "reset": "Reset", + "@reset": {}, + "filterDocuments": "Filter Documents", + "@filterDocuments": { + "description": "Title of the document filter" + }, + "originalMD5Checksum": "Original MD5-Checksum", + "@originalMD5Checksum": {}, + "mediaFilename": "Media Filename", + "@mediaFilename": {}, + "originalFileSize": "Original File Size", + "@originalFileSize": {}, + "originalMIMEType": "Original MIME-Type", + "@originalMIMEType": {}, + "modifiedAt": "Modified at", + "@modifiedAt": {}, + "preview": "Preview", + "@preview": { + "description": "Title of the document preview page" + }, + "scanADocument": "Scan a document", + "@scanADocument": {}, + "noDocumentsScannedYet": "No documents scanned yet.", + "@noDocumentsScannedYet": {}, + "or": "or", + "@or": { + "description": "Used on the scanner page between both main actions when no scans have been captured." + }, + "deleteAllScans": "Delete all scans", + "@deleteAllScans": {}, + "uploadADocumentFromThisDevice": "Upload a document from this device", + "@uploadADocumentFromThisDevice": { + "description": "Button label on scanner page" + }, + "noMatchesFound": "No matches found.", + "@noMatchesFound": { + "description": "Displayed when no documents were found in the document search." + }, + "removeFromSearchHistory": "Remove from search history?", + "@removeFromSearchHistory": {}, + "results": "Results", + "@results": { + "description": "Label displayed above search results in document search." + }, + "searchDocuments": "Search documents", + "@searchDocuments": {}, + "resetFilter": "Reset filter", + "@resetFilter": {}, + "lastMonth": "Last Month", + "@lastMonth": {}, + "last7Days": "Last 7 Days", + "@last7Days": {}, + "last3Months": "Last 3 Months", + "@last3Months": {}, + "lastYear": "Last Year", + "@lastYear": {}, + "search": "Search", + "@search": {}, + "documentsSuccessfullyDeleted": "Documents successfully deleted.", + "@documentsSuccessfullyDeleted": {}, + "thereSeemsToBeNothingHere": "There seems to be nothing here...", + "@thereSeemsToBeNothingHere": {}, + "oops": "Oops.", + "@oops": {}, + "newDocumentAvailable": "New document available!", + "@newDocumentAvailable": {}, + "orderBy": "Order By", + "@orderBy": {}, + "thisActionIsIrreversibleDoYouWishToProceedAnyway": "This action is irreversible. Do you wish to proceed anyway?", + "@thisActionIsIrreversibleDoYouWishToProceedAnyway": {}, + "confirmDeletion": "Confirm deletion", + "@confirmDeletion": {}, + "areYouSureYouWantToDeleteTheFollowingDocuments": "{count, plural, one{Are you sure you want to delete the following document?} other{Are you sure you want to delete the following documents?}}", + "@areYouSureYouWantToDeleteTheFollowingDocuments": { + "placeholders": { + "count": {} + } + }, + "countSelected": "{count} selected", + "@countSelected": { + "description": "Displayed in the appbar when at least one document is selected.", + "placeholders": { + "count": {} + } + }, + "storagePath": "Storage Path", + "@storagePath": {}, + "prepareDocument": "Prepare document", + "@prepareDocument": {}, + "tags": "Tags", + "@tags": {}, + "documentSuccessfullyUpdated": "Document successfully updated.", + "@documentSuccessfullyUpdated": {}, + "fileName": "File Name", + "@fileName": {}, + "synchronizeTitleAndFilename": "Synchronize title and filename", + "@synchronizeTitleAndFilename": {}, + "reload": "Reload", + "@reload": {}, + "documentSuccessfullyUploadedProcessing": "Document successfully uploaded, processing...", + "@documentSuccessfullyUploadedProcessing": {}, + "deleteLabelWarningText": "This label contains references to other documents. By deleting this label, all references will be removed. Continue?", + "@deleteLabelWarningText": {}, + "couldNotAcknowledgeTasks": "Could not acknowledge tasks.", + "@couldNotAcknowledgeTasks": {}, + "authenticationFailedPleaseTryAgain": "Authentication failed, please try again.", + "@authenticationFailedPleaseTryAgain": {}, + "anErrorOccurredWhileTryingToAutocompleteYourQuery": "An error ocurred while trying to autocomplete your query.", + "@anErrorOccurredWhileTryingToAutocompleteYourQuery": {}, + "biometricAuthenticationFailed": "Biometric authentication failed.", + "@biometricAuthenticationFailed": {}, + "biometricAuthenticationNotSupported": "Biometric authentication not supported on this device.", + "@biometricAuthenticationNotSupported": {}, + "couldNotBulkEditDocuments": "Could not bulk edit documents.", + "@couldNotBulkEditDocuments": {}, + "couldNotCreateCorrespondent": "Could not create correspondent, please try again.", + "@couldNotCreateCorrespondent": {}, + "couldNotLoadCorrespondents": "Could not load correspondents.", + "@couldNotLoadCorrespondents": {}, + "couldNotCreateSavedView": "Could not create saved view, please try again.", + "@couldNotCreateSavedView": {}, + "couldNotDeleteSavedView": "Could not delete saved view, please try again", + "@couldNotDeleteSavedView": {}, + "youAreCurrentlyOffline": "You are currently offline. Please make sure you are connected to the internet.", + "@youAreCurrentlyOffline": {}, + "couldNotAssignArchiveSerialNumber": "Could not assign archive serial number.", + "@couldNotAssignArchiveSerialNumber": {}, + "couldNotDeleteDocument": "Could not delete document, please try again.", + "@couldNotDeleteDocument": {}, + "couldNotLoadDocuments": "Could not load documents, please try again.", + "@couldNotLoadDocuments": {}, + "couldNotLoadDocumentPreview": "Could not load document preview.", + "@couldNotLoadDocumentPreview": {}, + "couldNotCreateDocument": "Could not create document, please try again.", + "@couldNotCreateDocument": {}, + "couldNotLoadDocumentTypes": "Could not load document types, please try again.", + "@couldNotLoadDocumentTypes": {}, + "couldNotUpdateDocument": "Could not update document, please try again.", + "@couldNotUpdateDocument": {}, + "couldNotUploadDocument": "Could not upload document, please try again.", + "@couldNotUploadDocument": {}, + "invalidCertificateOrMissingPassphrase": "Invalid certificate or missing passphrase, please try again", + "@invalidCertificateOrMissingPassphrase": {}, + "couldNotLoadSavedViews": "Could not load saved views.", + "@couldNotLoadSavedViews": {}, + "aClientCertificateWasExpectedButNotSent": "A client certificate was expected but not sent. Please provide a valid client certificate.", + "@aClientCertificateWasExpectedButNotSent": {}, + "userIsNotAuthenticated": "User is not authenticated.", + "@userIsNotAuthenticated": {}, + "requestTimedOut": "The request to the server timed out.", + "@requestTimedOut": {}, + "anErrorOccurredRemovingTheScans": "An error occurred removing the scans.", + "@anErrorOccurredRemovingTheScans": {}, + "couldNotReachYourPaperlessServer": "Could not reach your Paperless server, is it up and running?", + "@couldNotReachYourPaperlessServer": {}, + "couldNotLoadSimilarDocuments": "Could not load similar documents.", + "@couldNotLoadSimilarDocuments": {}, + "couldNotCreateStoragePath": "Could not create storage path, please try again.", + "@couldNotCreateStoragePath": {}, + "couldNotLoadStoragePaths": "Could not load storage paths.", + "@couldNotLoadStoragePaths": {}, + "couldNotLoadSuggestions": "Could not load suggestions.", + "@couldNotLoadSuggestions": {}, + "couldNotCreateTag": "Could not create tag, please try again.", + "@couldNotCreateTag": {}, + "couldNotLoadTags": "Could not load tags.", + "@couldNotLoadTags": {}, + "anUnknownErrorOccurred": "An unknown error occurred.", + "@anUnknownErrorOccurred": {}, + "fileFormatNotSupported": "This file format is not supported.", + "@fileFormatNotSupported": {}, + "report": "REPORT", + "@report": {}, + "absolute": "Absolute", + "@absolute": {}, + "hintYouCanAlsoSpecifyRelativeValues": "Hint: Apart from concrete dates, you can also specify a time range relative to the current date.", + "@hintYouCanAlsoSpecifyRelativeValues": { + "description": "Displayed in the extended date range picker" + }, + "amount": "Amount", + "@amount": {}, + "relative": "Relative", + "@relative": {}, + "last": "Last", + "@last": {}, + "timeUnit": "Time unit", + "@timeUnit": {}, + "selectDateRange": "Select date range", + "@selectDateRange": {}, + "after": "After", + "@after": {}, + "before": "Before", + "@before": {}, + "days": "{count, plural, zero{days} one{day} other{days}}", + "@days": { + "placeholders": { + "count": {} + } + }, + "lastNDays": "{count, plural, zero{} one{Yesterday} other{Last {count} days}}", + "@lastNDays": { + "placeholders": { + "count": {} + } + }, + "lastNMonths": "{count, plural, zero{} one{Last month} other{Last {count} months}}", + "@lastNMonths": { + "placeholders": { + "count": {} + } + }, + "lastNWeeks": "{count, plural, zero{} one{Last week} other{Last {count} weeks}}", + "@lastNWeeks": { + "placeholders": { + "count": {} + } + }, + "lastNYears": "{count, plural, zero{} one{Last year} other{Last {count} years}}", + "@lastNYears": { + "placeholders": { + "count": {} + } + }, + "months": "{count, plural, zero{} one{month} other{months}}", + "@months": { + "placeholders": { + "count": {} + } + }, + "weeks": "{count, plural, zero{} one{week} other{weeks}}", + "@weeks": { + "placeholders": { + "count": {} + } + }, + "years": "{count, plural, zero{} one{year} other{years}}", + "@years": { + "placeholders": { + "count": {} + } + }, + "gotIt": "Got it!", + "@gotIt": {}, + "cancel": "Cancel", + "@cancel": {}, + "close": "Close", + "@close": {}, + "create": "Create", + "@create": {}, + "delete": "Delete", + "@delete": {}, + "edit": "Edit", + "@edit": {}, + "ok": "Ok", + "@ok": {}, + "save": "Save", + "@save": {}, + "select": "Select", + "@select": {}, + "saveChanges": "Save changes", + "@saveChanges": {}, + "upload": "Upload", + "@upload": {}, + "youreOffline": "You're offline.", + "@youreOffline": {}, + "deleteDocument": "Delete document", + "@deleteDocument": { + "description": "Used as an action label on each inbox item" + }, + "removeDocumentFromInbox": "Document removed from inbox.", + "@removeDocumentFromInbox": {}, + "areYouSureYouWantToMarkAllDocumentsAsSeen": "Are you sure you want to mark all documents as seen? This will perform a bulk edit operation removing all inbox tags from the documents. This action is not reversible! Are you sure you want to continue?", + "@areYouSureYouWantToMarkAllDocumentsAsSeen": {}, + "markAllAsSeen": "Mark all as seen?", + "@markAllAsSeen": {}, + "allSeen": "All seen", + "@allSeen": {}, + "markAsSeen": "Mark as seen", + "@markAsSeen": {}, + "refresh": "Refresh", + "@refresh": {}, + "youDoNotHaveUnseenDocuments": "You do not have unseen documents.", + "@youDoNotHaveUnseenDocuments": {}, + "quickAction": "Quick Action", + "@quickAction": {}, + "suggestionSuccessfullyApplied": "Suggestion successfully applied.", + "@suggestionSuccessfullyApplied": {}, + "today": "Today", + "@today": {}, + "undo": "Undo", + "@undo": {}, + "nUnseen": "{count} unseen", + "@nUnseen": { + "placeholders": { + "count": {} + } + }, + "swipeLeftToMarkADocumentAsSeen": "Hint: Swipe left to mark a document as seen and remove all inbox tags from the document.", + "@swipeLeftToMarkADocumentAsSeen": {}, + "yesterday": "Yesterday", + "@yesterday": {}, + "anyAssigned": "Any assigned", + "@anyAssigned": {}, + "noItemsFound": "No items found!", + "@noItemsFound": {}, + "caseIrrelevant": "Case Irrelevant", + "@caseIrrelevant": {}, + "matchingAlgorithm": "Matching Algorithm", + "@matchingAlgorithm": {}, + "match": "Match", + "@match": {}, + "name": "Name", + "@name": {}, + "notAssigned": "Not assigned", + "@notAssigned": {}, + "addNewCorrespondent": "Add new correspondent", + "@addNewCorrespondent": {}, + "noCorrespondentsSetUp": "You don't seem to have any correspondents set up.", + "@noCorrespondentsSetUp": {}, + "correspondents": "Correspondents", + "@correspondents": {}, + "addNewDocumentType": "Add new document type", + "@addNewDocumentType": {}, + "noDocumentTypesSetUp": "You don't seem to have any document types set up.", + "@noDocumentTypesSetUp": {}, + "documentTypes": "Document Types", + "@documentTypes": {}, + "addNewStoragePath": "Add new storage path", + "@addNewStoragePath": {}, + "noStoragePathsSetUp": "You don't seem to have any storage paths set up.", + "@noStoragePathsSetUp": {}, + "storagePaths": "Storage Paths", + "@storagePaths": {}, + "addNewTag": "Add new tag", + "@addNewTag": {}, + "noTagsSetUp": "You don't seem to have any tags set up.", + "@noTagsSetUp": {}, + "linkedDocuments": "Linked Documents", + "@linkedDocuments": {}, + "advancedSettings": "Advanced Settings", + "@advancedSettings": {}, + "passphrase": "Passphrase", + "@passphrase": {}, + "configureMutualTLSAuthentication": "Configure Mutual TLS Authentication", + "@configureMutualTLSAuthentication": {}, + "invalidCertificateFormat": "Invalid certificate format, only .pfx is allowed", + "@invalidCertificateFormat": {}, + "clientcertificate": "Client Certificate", + "@clientcertificate": {}, + "selectFile": "Select file...", + "@selectFile": {}, + "continueLabel": "Continue", + "@continueLabel": {}, + "incorrectOrMissingCertificatePassphrase": "Incorrect or missing certificate passphrase.", + "@incorrectOrMissingCertificatePassphrase": {}, + "connect": "Connect", + "@connect": {}, + "password": "Password", + "@password": {}, + "passwordMustNotBeEmpty": "Password must not be empty.", + "@passwordMustNotBeEmpty": {}, + "connectionTimedOut": "Connection timed out.", + "@connectionTimedOut": {}, + "loginPageReachabilityMissingClientCertificateText": "A client certificate was expected but not sent. Please provide a certificate.", + "@loginPageReachabilityMissingClientCertificateText": {}, + "couldNotEstablishConnectionToTheServer": "Could not establish a connection to the server.", + "@couldNotEstablishConnectionToTheServer": {}, + "connectionSuccessfulylEstablished": "Connection successfully established.", + "@connectionSuccessfulylEstablished": {}, + "hostCouldNotBeResolved": "Host could not be resolved. Please check the server address and your internet connection. ", + "@hostCouldNotBeResolved": {}, + "serverAddress": "Server Address", + "@serverAddress": {}, + "invalidAddress": "Invalid address.", + "@invalidAddress": {}, + "serverAddressMustIncludeAScheme": "Server address must include a scheme.", + "@serverAddressMustIncludeAScheme": {}, + "serverAddressMustNotBeEmpty": "Server address must not be empty.", + "@serverAddressMustNotBeEmpty": {}, + "signIn": "Sign In", + "@signIn": {}, + "loginPageSignInTitle": "Sign In", + "@loginPageSignInTitle": {}, + "signInToServer": "Sign in to {serverAddress}", + "@signInToServer": { + "placeholders": { + "serverAddress": {} + } + }, + "connectToPaperless": "Connect to Paperless", + "@connectToPaperless": {}, + "username": "Username", + "@username": {}, + "usernameMustNotBeEmpty": "Username must not be empty.", + "@usernameMustNotBeEmpty": {}, + "documentContainsAllOfTheseWords": "Document contains all of these words", + "@documentContainsAllOfTheseWords": {}, + "all": "All", + "@all": {}, + "documentContainsAnyOfTheseWords": "Document contains any of these words", + "@documentContainsAnyOfTheseWords": {}, + "any": "Any", + "@any": {}, + "learnMatchingAutomatically": "Learn matching automatically", + "@learnMatchingAutomatically": {}, + "auto": "Auto", + "@auto": {}, + "documentContainsThisString": "Document contains this string", + "@documentContainsThisString": {}, + "exact": "Exact", + "@exact": {}, + "documentContainsAWordSimilarToThisWord": "Document contains a word similar to this word", + "@documentContainsAWordSimilarToThisWord": {}, + "fuzzy": "Fuzzy", + "@fuzzy": {}, + "documentMatchesThisRegularExpression": "Document matches this regular expression", + "@documentMatchesThisRegularExpression": {}, + "regularExpression": "Regular Expression", + "@regularExpression": {}, + "anInternetConnectionCouldNotBeEstablished": "An internet connection could not be established.", + "@anInternetConnectionCouldNotBeEstablished": {}, + "done": "Done", + "@done": {}, + "next": "Next", + "@next": {}, + "couldNotAccessReceivedFile": "Could not access the received file. Please try to open the app before sharing.", + "@couldNotAccessReceivedFile": {}, + "newView": "New View", + "@newView": {}, + "createsASavedViewBasedOnTheCurrentFilterCriteria": "Creates a new view based on the current filter criteria.", + "@createsASavedViewBasedOnTheCurrentFilterCriteria": {}, + "createViewsToQuicklyFilterYourDocuments": "Create views to quickly filter your documents.", + "@createViewsToQuicklyFilterYourDocuments": {}, + "nFiltersSet": "{count, plural, zero{{count} filters set} one{{count} filter set} other{{count} filters set}}", + "@nFiltersSet": { + "placeholders": { + "count": {} + } + }, + "showInSidebar": "Show in sidebar", + "@showInSidebar": {}, + "showOnDashboard": "Show on dashboard", + "@showOnDashboard": {}, + "views": "Views", + "@views": {}, + "clearAll": "Clear all", + "@clearAll": {}, + "scan": "Scan", + "@scan": {}, + "previewScan": "Preview", + "@previewScan": {}, + "scrollToTop": "Scroll to top", + "@scrollToTop": {}, + "paperlessServerVersion": "Paperless server version", + "@paperlessServerVersion": {}, + "darkTheme": "Dark Theme", + "@darkTheme": {}, + "lightTheme": "Light Theme", + "@lightTheme": {}, + "systemTheme": "Use system theme", + "@systemTheme": {}, + "appearance": "Appearance", + "@appearance": {}, + "languageAndVisualAppearance": "Language and visual appearance", + "@languageAndVisualAppearance": {}, + "applicationSettings": "Application", + "@applicationSettings": {}, + "colorSchemeHint": "Choose between a classic color scheme inspired by a traditional Paperless green or use the dynamic color scheme based on your system theme.", + "@colorSchemeHint": {}, + "colorSchemeNotSupportedWarning": "Dynamic theming is only supported for devices running Android 12 and above. Selecting the 'Dynamic' option might not have any effect depending on your OS implementation.", + "@colorSchemeNotSupportedWarning": {}, + "colors": "Colors", + "@colors": {}, + "language": "Language", + "@language": {}, + "security": "Security", + "@security": {}, + "mangeFilesAndStorageSpace": "Manage files and storage space", + "@mangeFilesAndStorageSpace": {}, + "storage": "Storage", + "@storage": {}, + "dark": "Dark", + "@dark": {}, + "light": "Light", + "@light": {}, + "system": "System", + "@system": {}, + "ascending": "Ascending", + "@ascending": {}, + "descending": "Descending", + "@descending": {}, + "storagePathDay": "day", + "@storagePathDay": {}, + "storagePathMonth": "month", + "@storagePathMonth": {}, + "storagePathYear": "year", + "@storagePathYear": {}, + "color": "Color", + "@color": {}, + "filterTags": "Filter tags...", + "@filterTags": {}, + "inboxTag": "Inbox-Tag", + "@inboxTag": {}, + "uploadInferValuesHint": "If you specify values for these fields, your paperless instance will not automatically derive a value. If you want these values to be automatically populated by your server, leave the fields blank.", + "@uploadInferValuesHint": {}, + "useTheConfiguredBiometricFactorToAuthenticate": "Use the configured biometric factor to authenticate and unlock your documents.", + "@useTheConfiguredBiometricFactorToAuthenticate": {}, + "verifyYourIdentity": "Verify your identity", + "@verifyYourIdentity": {}, + "verifyIdentity": "Verify Identity", + "@verifyIdentity": {}, + "detailed": "Detailed", + "@detailed": {}, + "grid": "Grid", + "@grid": {}, + "list": "List", + "@list": {}, + "remove": "Remove", + "removeQueryFromSearchHistory": "Remove query from search history?", + "dynamicColorScheme": "Dynamic", + "@dynamicColorScheme": {}, + "classicColorScheme": "Classic", + "@classicColorScheme": {}, + "notificationDownloadComplete": "Download complete", + "@notificationDownloadComplete": { + "description": "Notification title when a download has been completed." + }, + "notificationDownloadingDocument": "Downloading document", + "@notificationDownloadingDocument": { + "description": "Notification title shown when a document download is pending" + }, + "archiveSerialNumberUpdated": "Archive Serial Number updated.", + "@archiveSerialNumberUpdated": { + "description": "Message shown when the ASN has been updated." + }, + "donateCoffee": "Buy me a coffee", + "@donateCoffee": { + "description": "Label displayed in the app drawer" + }, + "thisFieldIsRequired": "This field is required!", + "@thisFieldIsRequired": { + "description": "Message shown below the form field when a required field has not been filled out." + }, + "confirm": "Confirm", + "confirmAction": "Confirm action", + "@confirmAction": { + "description": "Typically used as a title to confirm a previously selected action" + }, + "areYouSureYouWantToContinue": "Are you sure you want to continue?", + "bulkEditTagsAddMessage": "{count, plural, one{This operation will add the tags {tags} to the selected document.} other{This operation will add the tags {tags} to {count} selected documents.}}", + "@bulkEditTagsAddMessage": { + "description": "Message of the confirmation dialog when bulk adding tags." + }, + "bulkEditTagsRemoveMessage": "{count, plural, one{This operation will remove the tags {tags} from the selected document.} other{This operation will remove the tags {tags} from {count} selected documents.}}", + "@bulkEditTagsRemoveMessage": { + "description": "Message of the confirmation dialog when bulk removing tags." + }, + "bulkEditTagsModifyMessage": "{count, plural, one{This operation will add the tags {addTags} and remove the tags {removeTags} from the selected document.} other{This operation will add the tags {addTags} and remove the tags {removeTags} from {count} selected documents.}}", + "@bulkEditTagsModifyMessage": { + "description": "Message of the confirmation dialog when both adding and removing tags." + }, + "bulkEditCorrespondentAssignMessage": "{count, plural, one{This operation will assign the correspondent {correspondent} to the selected document.} other{This operation will assign the correspondent {correspondent} to {count} selected documents.}}", + "bulkEditDocumentTypeAssignMessage": "{count, plural, one{This operation will assign the document type {docType} to the selected document.} other{This operation will assign the documentType {docType} to {count} selected documents.}}", + "bulkEditStoragePathAssignMessage": "{count, plural, one{This operation will assign the storage path {path} to the selected document.} other{This operation will assign the storage path {path} to {count} selected documents.}}", + "bulkEditCorrespondentRemoveMessage": "{count, plural, one{This operation will remove the correspondent from the selected document.} other{This operation will remove the correspondent from {count} selected documents.}}", + "bulkEditDocumentTypeRemoveMessage": "{count, plural, one{This operation will remove the document type from the selected document.} other{This operation will remove the document type from {count} selected documents.}}", + "bulkEditStoragePathRemoveMessage": "{count, plural, one{This operation will remove the storage path from the selected document.} other{This operation will remove the storage path from {count} selected documents.}}", + "anyTag": "Any", + "@anyTag": { + "description": "Label shown when any tag should be filtered" + }, + "allTags": "All", + "@allTags": { + "description": "Label shown when a document has to be assigned to all selected tags" + }, + "switchingAccountsPleaseWait": "Switching accounts. Please wait...", + "@switchingAccountsPleaseWait": { + "description": "Message shown while switching accounts is in progress." + }, + "testConnection": "Test connection", + "@testConnection": { + "description": "Button label shown on login page. Allows user to test whether the server is reachable or not." + }, + "accounts": "Accounts", + "@accounts": { + "description": "Title of the account management dialog" + }, + "addAccount": "Add account", + "@addAccount": { + "description": "Label of add account action" + }, + "switchAccount": "Switch", + "@switchAccount": { + "description": "Label for switch account action" + }, + "logout": "Logout", + "@logout": { + "description": "Generic Logout label" + }, + "switchAccountTitle": "Switch account", + "@switchAccountTitle": { + "description": "Title of the dialog shown after adding an account, asking the user whether to switch to the newly added account or not." + }, + "switchToNewAccount": "Do you want to switch to the new account? You can switch back at any time.", + "@switchToNewAccount": { + "description": "Content of the dialog shown after adding an account, asking the user whether to switch to the newly added account or not." + }, + "sourceCode": "Source Code", + "findTheSourceCodeOn": "Find the source code on", + "@findTheSourceCodeOn": { + "description": "Text before link to Paperless Mobile GitHub" + }, + "rememberDecision": "Remember my decision", + "defaultDownloadFileType": "Default Download File Type", + "@defaultDownloadFileType": { + "description": "Label indicating the default filetype to download (one of archived, original and always ask)" + }, + "defaultShareFileType": "Default Share File Type", + "@defaultShareFileType": { + "description": "Label indicating the default filetype to share (one of archived, original and always ask)" + }, + "alwaysAsk": "Always ask", + "@alwaysAsk": { + "description": "Option to choose when the app should always ask the user which filetype to use" + }, + "disableMatching": "Do not tag documents automatically", + "@disableMatching": { + "description": "One of the options for automatic tagging of documents" + }, + "none": "None", + "@none": { + "description": "One of available enum values of matching algorithm for tags" + }, + "logInToExistingAccount": "Log in to existing account", + "@logInToExistingAccount": { + "description": "Title shown on login page if at least one user is already known to the app." + }, + "print": "Print", + "@print": { + "description": "Tooltip for print button" + }, + "managePermissions": "Manage permissions", + "@managePermissions": { + "description": "Button which leads user to manage permissions page" + }, + "errorRetrievingServerVersion": "An error occurred trying to resolve the server version.", + "@errorRetrievingServerVersion": { + "description": "Message shown at the bottom of the settings page when the remote server version could not be resolved." + }, + "resolvingServerVersion": "Resolving server version...", + "@resolvingServerVersion": { + "description": "Message shown while the app is loading the remote server version." + }, + "goToLogin": "Go to login", + "@goToLogin": { + "description": "Label of the button shown on the login page to skip logging in to existing accounts and navigate user to login page" + }, + "export": "Export", + "@export": { + "description": "Label for button that exports scanned images to pdf (before upload)" + }, + "invalidFilenameCharacter": "Invalid character(s) found in filename: {characters}", + "@invalidFilenameCharacter": { + "description": "For validating filename in export dialogue" + }, + "exportScansToPdf": "Export scans to PDF", + "@exportScansToPdf": { + "description": "title of the alert dialog when exporting scans to pdf" + }, + "allScansWillBeMerged": "All scans will be merged into a single PDF file.", + "behavior": "Behavior", + "@behavior": { + "description": "Title of the settings concerning app beahvior" + }, + "theme": "Theme", + "@theme": { + "description": "Title of the theme mode setting" + }, + "clearCache": "Clear cache", + "@clearCache": { + "description": "Title of the clear cache setting" + }, + "freeBytes": "Free {byteString}", + "@freeBytes": { + "description": "Text shown for clear storage settings" + }, + "calculatingDots": "Calculating...", + "@calculatingDots": { + "description": "Text shown when the byte size is still being calculated" + }, + "freedDiskSpace": "Successfully freed {bytes} of disk space.", + "@freedDiskSpace": { + "description": "Message shown after clearing storage" + }, + "uploadScansAsPdf": "Upload scans as PDF", + "@uploadScansAsPdf": { + "description": "Title of the setting which toggles whether scans are always uploaded as pdf" + }, + "convertSinglePageScanToPdf": "Always convert single page scans to PDF before uploading", + "@convertSinglePageScanToPdf": { + "description": "description of the upload scans as pdf setting" + }, + "loginRequiredPermissionsHint": "Using Paperless Mobile requires a minimum set of user permissions since paperless-ngx 1.14.0 and higher. Therefore, please make sure that the user to be logged in has the permission to view other users (User → View) and the settings (UISettings → View). If you do not have these permissions, please contact an administrator of your paperless-ngx server.", + "@loginRequiredPermissionsHint": { + "description": "Hint shown on the login page informing the user of the required permissions to use the app." + }, + "missingPermissions": "You do not have the necessary permissions to perform this action.", + "@missingPermissions": { + "description": "Message shown in a snackbar when a user without the reequired permissions performs an action." + }, + "editView": "Edit View", + "@editView": { + "description": "Title of the edit saved view page" + }, + "donate": "Donate", + "@donate": { + "description": "Label of the in-app donate button" + }, + "donationDialogContent": "Thank you for considering to support this app! Due to both Google's and Apple's Payment Policies, no links leading to donations may be displayed in-app. Not even linking to the project's repository page appears to be allowed in this context. Therefore, maybe have a look at the 'Donations' section in the project's README. Your support is much appreciated and keeps the development of this app alive. Thanks!", + "@donationDialogContent": { + "description": "Text displayed in the donation dialog" + }, + "noDocumentsFound": "No documents found.", + "@noDocumentsFound": { + "description": "Message shown when no documents were found." + }, + "couldNotDeleteCorrespondent": "Could not delete correspondent, please try again.", + "@couldNotDeleteCorrespondent": { + "description": "Message shown in snackbar when a correspondent could not be deleted." + }, + "couldNotDeleteDocumentType": "Could not delete document type, please try again.", + "@couldNotDeleteDocumentType": { + "description": "Message shown when a document type could not be deleted" + }, + "couldNotDeleteTag": "Could not delete tag, please try again.", + "@couldNotDeleteTag": { + "description": "Message shown when a tag could not be deleted" + }, + "couldNotDeleteStoragePath": "Could not delete storage path, please try again.", + "@couldNotDeleteStoragePath": { + "description": "Message shown when a storage path could not be deleted" + }, + "couldNotUpdateCorrespondent": "Could not update correspondent, please try again.", + "@couldNotUpdateCorrespondent": { + "description": "Message shown when a correspondent could not be updated" + }, + "couldNotUpdateDocumentType": "Could not update document type, please try again.", + "@couldNotUpdateDocumentType": { + "description": "Message shown when a document type could not be updated" + }, + "couldNotUpdateTag": "Could not update tag, please try again.", + "@couldNotUpdateTag": { + "description": "Message shown when a tag could not be updated" + }, + "couldNotLoadServerInformation": "Could not load server information.", + "@couldNotLoadServerInformation": { + "description": "Message shown when the server information could not be loaded" + }, + "couldNotLoadStatistics": "Could not load server statistics.", + "@couldNotLoadStatistics": { + "description": "Message shown when the server statistics could not be loaded" + }, + "couldNotLoadUISettings": "Could not load UI settings.", + "@couldNotLoadUISettings": { + "description": "Message shown when the UI settings could not be loaded" + }, + "couldNotLoadTasks": "Could not load tasks.", + "@couldNotLoadTasks": { + "description": "Message shown when the tasks (e.g. document consumed) could not be loaded" + }, + "userNotFound": "User could not be found.", + "@userNotFound": { + "description": "Message shown when the specified user (e.g. by id) could not be found" + }, + "couldNotUpdateSavedView": "Could not update saved view, please try again.", + "@couldNotUpdateSavedView": { + "description": "Message shown when a saved view could not be updated" + }, + "couldNotUpdateStoragePath": "Could not update storage path, please try again.", + "savedViewSuccessfullyUpdated": "Saved view successfully updated.", + "@savedViewSuccessfullyUpdated": { + "description": "Message shown when a saved view was successfully updated." + }, + "discardChanges": "Discard changes?", + "@discardChanges": { + "description": "Title of the alert dialog shown when a user tries to close a view with unsaved changes." + }, + "savedViewChangedDialogContent": "The filter conditions of the active view have changed. By resetting the filter, these changes will be lost. Do you still wish to continue?", + "@savedViewChangedDialogContent": { + "description": "Content of the alert dialog shown when all of the following applies:\r\n* User has saved view selected\r\n* User has performed changes to the current document filter\r\n* User now tries to reset this filter without having saved the changes to the view." + }, + "createFromCurrentFilter": "Create from current filter", + "@createFromCurrentFilter": { + "description": "Tooltip of the \"New saved view\" button" + }, + "home": "Home", + "@home": { + "description": "Label of the \"Home\" route" + }, + "welcomeUser": "Welcome, {name}!", + "@welcomeUser": { + "description": "Top message shown on the home page" + }, + "statistics": "Statistics", + "documentsInInbox": "Documents in inbox", + "totalDocuments": "Total documents", + "totalCharacters": "Total characters", + "showAll": "Show all", + "@showAll": { + "description": "Button label shown on a saved view preview to open this view in the documents page" + }, + "userAlreadyExists": "This user already exists.", + "@userAlreadyExists": { + "description": "Error message shown when the user tries to add an already existing account." + }, + "youDidNotSaveAnyViewsYet": "You did not save any views yet, create one and it will be shown here.", + "@youDidNotSaveAnyViewsYet": { + "description": "Message shown when there are no saved views yet." + }, + "tryAgain": "Try again", + "discardFile": "Discard file?", + "discard": "Discard", + "backToLogin": "Back to login", + "skipEditingReceivedFiles": "Skip editing received files", + "uploadWithoutPromptingUploadForm": "Always upload without prompting the upload form when sharing files with the app.", + "authenticatingDots": "Authenticating...", + "@authenticatingDots": { + "description": "Message shown when the app is authenticating the user" + }, + "persistingUserInformation": "Persisting user information...", + "fetchingUserInformation": "Fetching user information...", + "@fetchingUserInformation": { + "description": "Message shown when the app loads user data from the server" + }, + "restoringSession": "Restoring session...", + "@restoringSession": { + "description": "Message shown when the user opens the app and the previous user is tried to be authenticated and logged in" + }, + "documentsAssigned": "{count, plural, zero{No documents} one{1 document} other{{count} documents}}", + "@documentsAssigned": { + "description": "Text shown with a correspondent, document type etc. to indicate the number of documents this filter will maximally yield." + }, + "discardChangesWarning": "You have unsaved changes. By continuing, all changes will be lost. Do you want to discard these changes?", + "@discardChangesWarning": { + "description": "Warning message shown when the user tries to close a route without saving the changes." + }, + "changelog": "Changelog", + "noLogsFoundOn": "No logs found on {date}.", + "logfileBottomReached": "You have reached the bottom of this logfile.", + "appLogs": "App logs {date}", + "saveLogsToFile": "Save logs to file", + "copyToClipboard": "Copy to clipboard", + "couldNotLoadLogfileFrom": "Could not load logfile from {date}.", + "loadingLogsFrom": "Loading logs from {date}...", + "clearLogs": "Clear logs from {date}", + "showPdf": "Show PDF", + "@showPdf": { + "description": "Tooltip shown on the \"show pdf\" button on the document edit page" + }, + "hidePdf": "Hide PDF", + "@hidePdf": { + "description": "Tooltip shown on the \"show pdf\" icon button on the document edit page" + }, + "misc": "Miscellaneous", + "loggingOut": "Logging out...", + "testingConnection": "Testing connection...", + "@testingConnection": { + "description": "Text shown while the app tries to establish a connection to the specified host." + }, + "version": "Version {versionCode}" +} \ No newline at end of file diff --git a/lib/l10n/intl_pl.arb b/lib/l10n/intl_pl.arb index 5f1256a..4f15c72 100644 --- a/lib/l10n/intl_pl.arb +++ b/lib/l10n/intl_pl.arb @@ -1019,5 +1019,10 @@ "description": "Tooltip shown on the \"show pdf\" icon button on the document edit page" }, "misc": "Miscellaneous", - "loggingOut": "Logging out..." + "loggingOut": "Logging out...", + "testingConnection": "Testing connection...", + "@testingConnection": { + "description": "Text shown while the app tries to establish a connection to the specified host." + }, + "version": "Version {versionCode}" } \ No newline at end of file diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index a47d43b..940d32a 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -1019,5 +1019,10 @@ "description": "Tooltip shown on the \"show pdf\" icon button on the document edit page" }, "misc": "Miscellaneous", - "loggingOut": "Logging out..." + "loggingOut": "Logging out...", + "testingConnection": "Testing connection...", + "@testingConnection": { + "description": "Text shown while the app tries to establish a connection to the specified host." + }, + "version": "Version {versionCode}" } \ No newline at end of file diff --git a/lib/l10n/intl_tr.arb b/lib/l10n/intl_tr.arb index 2f69024..98e3675 100644 --- a/lib/l10n/intl_tr.arb +++ b/lib/l10n/intl_tr.arb @@ -1019,5 +1019,10 @@ "description": "Tooltip shown on the \"show pdf\" icon button on the document edit page" }, "misc": "Miscellaneous", - "loggingOut": "Logging out..." + "loggingOut": "Logging out...", + "testingConnection": "Testing connection...", + "@testingConnection": { + "description": "Text shown while the app tries to establish a connection to the specified host." + }, + "version": "Version {versionCode}" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 6e91739..8d3fc05 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -22,6 +22,7 @@ import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/accessibility/accessible_page.dart'; import 'package:paperless_mobile/constants.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; +import 'package:paperless_mobile/core/bloc/my_bloc_observer.dart'; import 'package:paperless_mobile/core/database/hive/hive_config.dart'; import 'package:paperless_mobile/core/database/tables/global_settings.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; @@ -123,6 +124,7 @@ Future _initHive() async { void main() async { runZonedGuarded(() async { + Bloc.observer = MyBlocObserver(); WidgetsFlutterBinding.ensureInitialized(); await FileService.instance.initialize(); @@ -371,6 +373,16 @@ class _GoRouterShellState extends State { return DynamicColorBuilder( builder: (lightDynamic, darkDynamic) { return MaterialApp.router( + builder: (context, child) { + return AnnotatedRegion( + child: child!, + value: buildOverlayStyle( + Theme.of(context), + systemNavigationBarColor: + Theme.of(context).colorScheme.background, + ), + ); + }, routerConfig: _router, debugShowCheckedModeBanner: true, title: "Paperless Mobile", diff --git a/lib/routing/routes/documents_route.dart b/lib/routing/routes/documents_route.dart index 1de12b6..c26dde7 100644 --- a/lib/routing/routes/documents_route.dart +++ b/lib/routing/routes/documents_route.dart @@ -3,6 +3,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/features/document_bulk_action/cubit/document_bulk_action_cubit.dart'; import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_label_page.dart'; import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_tags_widget.dart'; @@ -37,7 +38,7 @@ class DocumentDetailsRoute extends GoRouteData { final String? queryString; final String? thumbnailUrl; final String? title; - + const DocumentDetailsRoute({ required this.id, this.isLabelClickable = true, @@ -53,7 +54,6 @@ class DocumentDetailsRoute extends GoRouteData { context.read(), context.read(), context.read(), - context.read(), id: id, )..initialize(), lazy: false, @@ -131,9 +131,9 @@ class BulkEditDocumentsRoute extends GoRouteData { @override Widget build(BuildContext context, GoRouterState state) { + final labelRepository = context.read(); return BlocProvider( create: (_) => DocumentBulkActionCubit( - context.read(), context.read(), context.read(), selection: $extra.selection, @@ -144,9 +144,9 @@ class BulkEditDocumentsRoute extends GoRouteData { LabelType.tag => const FullscreenBulkEditTagsWidget(), _ => FullscreenBulkEditLabelPage( options: switch ($extra.type) { - LabelType.correspondent => state.correspondents, - LabelType.documentType => state.documentTypes, - LabelType.storagePath => state.storagePaths, + LabelType.correspondent => labelRepository.correspondents, + LabelType.documentType => labelRepository.documentTypes, + LabelType.storagePath => labelRepository.storagePaths, _ => throw Exception("Parameter not allowed here."), }, selection: state.selection, diff --git a/lib/routing/routes/labels_route.dart b/lib/routing/routes/labels_route.dart index b931d2a..63aa64f 100644 --- a/lib/routing/routes/labels_route.dart +++ b/lib/routing/routes/labels_route.dart @@ -14,6 +14,7 @@ import 'package:paperless_mobile/features/labels/view/pages/labels_page.dart'; import 'package:paperless_mobile/features/linked_documents/cubit/linked_documents_cubit.dart'; import 'package:paperless_mobile/features/linked_documents/view/linked_documents_page.dart'; import 'package:paperless_mobile/routing/navigation_keys.dart'; + class LabelsBranch extends StatefulShellBranchData { static final GlobalKey $navigatorKey = labelsNavigatorKey; const LabelsBranch(); @@ -81,7 +82,6 @@ class LinkedDocumentsRoute extends GoRouteData { context.read(), context.read(), context.read(), - context.read(), ), child: const LinkedDocumentsPage(), ); diff --git a/packages/paperless_api/lib/src/models/custom_field_data_type.dart b/packages/paperless_api/lib/src/models/custom_field_data_type.dart new file mode 100644 index 0000000..63e7030 --- /dev/null +++ b/packages/paperless_api/lib/src/models/custom_field_data_type.dart @@ -0,0 +1,9 @@ +enum CustomFieldDataType { + text, + boolean, + date, + url, + integer, + number, + monetary; +} diff --git a/packages/paperless_api/lib/src/models/custom_field_model.dart b/packages/paperless_api/lib/src/models/custom_field_model.dart new file mode 100644 index 0000000..c49b48b --- /dev/null +++ b/packages/paperless_api/lib/src/models/custom_field_model.dart @@ -0,0 +1,26 @@ +import 'package:equatable/equatable.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:paperless_api/src/models/custom_field_data_type.dart'; + +part 'custom_field_model.g.dart'; + +@JsonSerializable() +class CustomFieldModel with EquatableMixin { + final int? id; + final String name; + final CustomFieldDataType dataType; + + CustomFieldModel({ + this.id, + required this.name, + required this.dataType, + }); + + @override + List get props => [id, name, dataType]; + + factory CustomFieldModel.fromJson(Map json) => + _$CustomFieldModelFromJson(json); + + Map toJson() => _$CustomFieldModelToJson(this); +} diff --git a/packages/paperless_api/lib/src/models/document_model.dart b/packages/paperless_api/lib/src/models/document_model.dart index c8c8011..3c605ee 100644 --- a/packages/paperless_api/lib/src/models/document_model.dart +++ b/packages/paperless_api/lib/src/models/document_model.dart @@ -4,6 +4,7 @@ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/src/converters/local_date_time_json_converter.dart'; +import 'package:paperless_api/src/models/custom_field_model.dart'; import 'package:paperless_api/src/models/search_hit.dart'; part 'document_model.g.dart'; @@ -50,6 +51,7 @@ class DocumentModel extends Equatable { // Only present if full_perms=true final Permissions? permissions; + final Iterable? customFields; const DocumentModel({ required this.id, @@ -69,6 +71,7 @@ class DocumentModel extends Equatable { this.owner, this.userCanChange, this.permissions, + this.customFields, }); factory DocumentModel.fromJson(Map json) => @@ -89,6 +92,8 @@ class DocumentModel extends Equatable { int? Function()? archiveSerialNumber, String? originalFileName, String? archivedFileName, + int? Function()? owner, + bool? userCanChange, }) { return DocumentModel( id: id, @@ -107,6 +112,8 @@ class DocumentModel extends Equatable { ? archiveSerialNumber() : this.archiveSerialNumber, archivedFileName: archivedFileName ?? this.archivedFileName, + owner: owner != null ? owner() : this.owner, + userCanChange: userCanChange ?? this.userCanChange, ); } @@ -114,17 +121,18 @@ class DocumentModel extends Equatable { List get props => [ id, title, - content.hashCode, - tags, - documentType, - storagePath, + content, correspondent, + documentType, + tags, + storagePath, created, modified, added, archiveSerialNumber, originalFileName, archivedFileName, - storagePath, + owner, + userCanChange, ]; } diff --git a/packages/paperless_api/lib/src/models/exception/paperless_form_validation_exception.dart b/packages/paperless_api/lib/src/models/exception/paperless_form_validation_exception.dart index b300b54..2a96df5 100644 --- a/packages/paperless_api/lib/src/models/exception/paperless_form_validation_exception.dart +++ b/packages/paperless_api/lib/src/models/exception/paperless_form_validation_exception.dart @@ -19,9 +19,12 @@ class PaperlessFormValidationException implements Exception { return validationMessages[formKey]; } - static bool canParse(Map json) { - return json.values - .every((element) => element is String || element is List); + static bool canParse(dynamic json) { + if (json is Map) { + return json.values + .every((element) => element is String || element is List); + } + return false; } factory PaperlessFormValidationException.fromJson(Map json) { diff --git a/packages/paperless_api/lib/src/models/exception/paperless_server_message_exception.dart b/packages/paperless_api/lib/src/models/exception/paperless_server_message_exception.dart index b4794ea..ccf91c5 100644 --- a/packages/paperless_api/lib/src/models/exception/paperless_server_message_exception.dart +++ b/packages/paperless_api/lib/src/models/exception/paperless_server_message_exception.dart @@ -11,9 +11,8 @@ class PaperlessServerMessageException implements Exception { static bool canParse(dynamic json) { if (json is Map) { return json.containsKey('detail') && json.length == 1; - } else { - return false; } + return false; } factory PaperlessServerMessageException.fromJson(Map json) => diff --git a/packages/paperless_api/lib/src/models/paperless_api_exception.dart b/packages/paperless_api/lib/src/models/paperless_api_exception.dart index 136de68..ad60cd0 100644 --- a/packages/paperless_api/lib/src/models/paperless_api_exception.dart +++ b/packages/paperless_api/lib/src/models/paperless_api_exception.dart @@ -68,5 +68,8 @@ enum ErrorCode { loadTasksError, userNotFound, userAlreadyExists, - updateSavedViewError; + updateSavedViewError, + customFieldCreateFailed, + customFieldLoadFailed, + customFieldDeleteFailed; } diff --git a/packages/paperless_api/lib/src/models/paperless_server_statistics_model.dart b/packages/paperless_api/lib/src/models/paperless_server_statistics_model.dart index 26bfbc3..6c8ee3d 100644 --- a/packages/paperless_api/lib/src/models/paperless_server_statistics_model.dart +++ b/packages/paperless_api/lib/src/models/paperless_server_statistics_model.dart @@ -14,9 +14,15 @@ class PaperlessServerStatisticsModel { : documentsTotal = json['documents_total'] ?? 0, documentsInInbox = json['documents_inbox'] ?? 0, totalChars = json["character_count"], - fileTypeCounts = (json['document_file_type_counts'] as List? ?? []) - .map((e) => DocumentFileTypeCount.fromJson(e)) - .toList(); + fileTypeCounts = + _parseFileTypeCounts(json['document_file_type_counts']); + + static List _parseFileTypeCounts(dynamic value) { + if (value is List) { + return value.map((e) => DocumentFileTypeCount.fromJson(e)).toList(); + } + return []; + } } class DocumentFileTypeCount { diff --git a/packages/paperless_api/lib/src/modules/custom_fields/custom_fields_api.dart b/packages/paperless_api/lib/src/modules/custom_fields/custom_fields_api.dart new file mode 100644 index 0000000..cb8207f --- /dev/null +++ b/packages/paperless_api/lib/src/modules/custom_fields/custom_fields_api.dart @@ -0,0 +1,8 @@ +import 'package:paperless_api/src/models/custom_field_model.dart'; + +abstract interface class CustomFieldsApi { + Future createCustomField(CustomFieldModel customField); + Future getCustomField(int id); + Future> getCustomFields(); + Future deleteCustomField(CustomFieldModel customField); +} diff --git a/packages/paperless_api/lib/src/modules/custom_fields/custom_fields_api_impl.dart b/packages/paperless_api/lib/src/modules/custom_fields/custom_fields_api_impl.dart new file mode 100644 index 0000000..45431e3 --- /dev/null +++ b/packages/paperless_api/lib/src/modules/custom_fields/custom_fields_api_impl.dart @@ -0,0 +1,72 @@ +import 'package:dio/dio.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_api/src/extensions/dio_exception_extension.dart'; +import 'package:paperless_api/src/models/custom_field_model.dart'; +import 'package:paperless_api/src/modules/custom_fields/custom_fields_api.dart'; +import 'package:paperless_api/src/request_utils.dart'; + +class CustomFieldsApiImpl implements CustomFieldsApi { + final Dio _dio; + + const CustomFieldsApiImpl(this._dio); + + @override + Future createCustomField( + CustomFieldModel customField) async { + try { + final response = await _dio.post( + "/api/custom_fields/", + data: customField.toJson(), + options: Options( + validateStatus: (status) => status == 201, + ), + ); + return CustomFieldModel.fromJson(response.data); + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException( + ErrorCode.customFieldCreateFailed, + ), + ); + } + } + + @override + Future deleteCustomField(CustomFieldModel customField) async { + try { + await _dio.delete( + "/api/custom_fields/${customField.id}/", + options: Options( + validateStatus: (status) => status == 204, + ), + ); + return customField.id!; + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException( + ErrorCode.customFieldDeleteFailed, + ), + ); + } + } + + @override + Future getCustomField(int id) { + return getSingleResult( + '/api/custom_fields/$id/', + CustomFieldModel.fromJson, + ErrorCode.customFieldLoadFailed, + client: _dio, + ); + } + + @override + Future> getCustomFields() { + return getCollection( + '/api/custom_fields/?page=1&page_size=100000', + CustomFieldModel.fromJson, + ErrorCode.customFieldLoadFailed, + client: _dio, + ); + } +} diff --git a/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart b/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart index 4df3d92..8cbddea 100644 --- a/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart +++ b/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart @@ -109,7 +109,10 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { ); } on DioException catch (exception) { throw exception.unravel( - orElse: const PaperlessApiException(ErrorCode.documentLoadFailed), + orElse: PaperlessApiException( + ErrorCode.documentLoadFailed, + details: exception.message, + ), ); } } diff --git a/packages/paperless_api/lib/src/modules/labels_api/paperless_labels_api_impl.dart b/packages/paperless_api/lib/src/modules/labels_api/paperless_labels_api_impl.dart index 740e713..65e19ae 100644 --- a/packages/paperless_api/lib/src/modules/labels_api/paperless_labels_api_impl.dart +++ b/packages/paperless_api/lib/src/modules/labels_api/paperless_labels_api_impl.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:paperless_api/src/extensions/dio_exception_extension.dart'; import 'package:paperless_api/src/models/models.dart'; -import 'package:paperless_api/src/models/paperless_api_exception.dart'; import 'package:paperless_api/src/modules/labels_api/paperless_labels_api.dart'; import 'package:paperless_api/src/request_utils.dart'; diff --git a/packages/paperless_api/lib/src/modules/server_stats_api/paperless_server_stats_api_impl.dart b/packages/paperless_api/lib/src/modules/server_stats_api/paperless_server_stats_api_impl.dart index 9294938..fccf1f0 100644 --- a/packages/paperless_api/lib/src/modules/server_stats_api/paperless_server_stats_api_impl.dart +++ b/packages/paperless_api/lib/src/modules/server_stats_api/paperless_server_stats_api_impl.dart @@ -14,7 +14,7 @@ import 'paperless_server_stats_api.dart'; /// class PaperlessServerStatsApiImpl implements PaperlessServerStatsApi { final Dio client; - + static const _fallbackVersion = '0.0.0'; PaperlessServerStatsApiImpl(this.client); @override @@ -24,7 +24,10 @@ class PaperlessServerStatsApiImpl implements PaperlessServerStatsApi { "/api/remote_version/", options: Options(validateStatus: (status) => status == 200), ); - final version = response.data["version"] as String; + var version = response.data["version"] as String; + if (version == _fallbackVersion) { + version = response.headers.value('x-version') ?? _fallbackVersion; + } final updateAvailable = response.data["update_available"] as bool; return PaperlessServerInformationModel( apiVersion: int.parse(response.headers.value('x-api-version')!), diff --git a/pubspec.yaml b/pubspec.yaml index a5a113f..89d3616 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,7 +18,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev version: 3.1.4+58 environment: - sdk: ">=3.0.0 <4.0.0" + sdk: ">=3.1.0 <4.0.0" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions