From 6566b2b8d714a435b75484aec23d6e6a7f4d50be Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Sat, 22 Jul 2023 14:17:48 +0200 Subject: [PATCH] feat: Rework error handling, upgrade dio, fixed bugs - Fix grey screen bug when adding labels from documnet upload - Add more permission checks to conditionally show widgets --- .../dio_http_error_interceptor.dart | 107 +--- .../interceptor/dio_offline_interceptor.dart | 32 + .../dio_unauthorized_interceptor.dart | 27 + ...etry_on_connection_change_interceptor.dart | 6 +- ...server_reachability_error_interceptor.dart | 10 +- lib/core/navigation/push_routes.dart | 2 + lib/core/repository/label_repository.dart | 1 - lib/core/security/session_manager.dart | 16 +- .../service/connectivity_status_service.dart | 4 +- lib/core/service/file_service.dart | 2 +- .../error_code_localization_mapper.dart | 141 ++--- lib/core/type/types.dart | 8 - .../cubit/document_details_cubit.dart | 18 +- .../view/pages/document_details_page.dart | 8 +- .../widgets/archive_serial_number_field.dart | 21 +- .../widgets/document_download_button.dart | 2 +- .../widgets/document_overview_widget.dart | 29 +- .../view/widgets/document_share_button.dart | 2 +- .../view/document_edit_page.dart | 26 +- .../cubit/document_scanner_cubit.dart | 4 +- .../document_scan/view/scanner_page.dart | 10 +- .../view/sliver_search_bar.dart | 72 ++- .../document_upload_preparation_page.dart | 111 ++-- .../documents/view/pages/documents_page.dart | 54 +- .../widgets/search/document_filter_form.dart | 18 +- .../document_selection_sliver_app_bar.dart | 2 +- .../edit_label/view/edit_label_page.dart | 2 +- .../view/impl/edit_correspondent_page.dart | 6 +- .../view/impl/edit_document_type_page.dart | 6 +- .../view/impl/edit_storage_path_page.dart | 5 +- .../edit_label/view/impl/edit_tag_page.dart | 5 +- lib/features/edit_label/view/label_form.dart | 12 +- lib/features/home/view/home_page.dart | 72 ++- lib/features/home/view/home_route.dart | 37 +- lib/features/inbox/view/pages/inbox_page.dart | 14 +- .../inbox/view/widgets/inbox_item.dart | 6 +- .../tags/view/widgets/tags_form_field.dart | 5 +- .../labels/view/pages/labels_page.dart | 565 ++++++++++-------- .../labels/view/widgets/label_item.dart | 3 +- .../login/cubit/authentication_cubit.dart | 2 +- lib/features/login/view/login_page.dart | 5 +- .../server_address_form_field.dart | 9 +- .../login_pages/server_connection_page.dart | 8 +- .../view/document_paging_view_mixin.dart | 2 +- .../view/similar_documents_view.dart | 2 +- lib/helpers/message_helpers.dart | 2 +- lib/main.dart | 23 +- .../extensions/dio_exception_extension.dart | 7 + .../lib/src/models/exception/exceptions.dart | 3 + .../paperless_form_validation_exception.dart | 42 ++ .../paperless_server_message_exception.dart | 17 + .../paperless_unauthorized_exception.dart | 5 + .../models/exception/parseable_exception.dart | 1 + .../paperless_api/lib/src/models/models.dart | 3 +- ...tion.dart => paperless_api_exception.dart} | 9 +- .../user_permission_extension.dart | 62 +- .../authentication_api.dart | 5 + .../authentication_api_impl.dart | 29 +- .../paperless_documents_api.dart | 2 +- .../paperless_documents_api_impl.dart | 196 +++--- .../labels_api/paperless_labels_api_impl.dart | 210 +++---- .../paperless_saved_views_api_impl.dart | 33 +- .../paperless_server_stats_api_impl.dart | 43 +- .../tasks_api/paperless_tasks_api_impl.dart | 33 +- .../user_api/paperless_user_api_v2_impl.dart | 28 +- .../user_api/paperless_user_api_v3_impl.dart | 61 +- .../paperless_api/lib/src/request_utils.dart | 59 +- .../example/pubspec.lock | 46 +- pubspec.lock | 136 ++--- test/utils.dart | 21 +- 70 files changed, 1444 insertions(+), 1131 deletions(-) create mode 100644 lib/core/interceptor/dio_offline_interceptor.dart create mode 100644 lib/core/interceptor/dio_unauthorized_interceptor.dart delete mode 100644 lib/core/type/types.dart create mode 100644 packages/paperless_api/lib/src/extensions/dio_exception_extension.dart create mode 100644 packages/paperless_api/lib/src/models/exception/exceptions.dart create mode 100644 packages/paperless_api/lib/src/models/exception/paperless_form_validation_exception.dart create mode 100644 packages/paperless_api/lib/src/models/exception/paperless_server_message_exception.dart create mode 100644 packages/paperless_api/lib/src/models/exception/paperless_unauthorized_exception.dart create mode 100644 packages/paperless_api/lib/src/models/exception/parseable_exception.dart rename packages/paperless_api/lib/src/models/{paperless_server_exception.dart => paperless_api_exception.dart} (72%) diff --git a/lib/core/interceptor/dio_http_error_interceptor.dart b/lib/core/interceptor/dio_http_error_interceptor.dart index 4f3ee474..2d6a0ba8 100644 --- a/lib/core/interceptor/dio_http_error_interceptor.dart +++ b/lib/core/interceptor/dio_http_error_interceptor.dart @@ -1,99 +1,46 @@ -import 'dart:io'; - import 'package:dio/dio.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/exception/server_message_exception.dart'; -import 'package:paperless_mobile/core/type/types.dart'; class DioHttpErrorInterceptor extends Interceptor { @override - void onError(DioError err, ErrorInterceptorHandler handler) { + void onError(DioException err, ErrorInterceptorHandler handler) { if (err.response?.statusCode == 400) { - // try to parse contained error message, otherwise return response - final dynamic data = err.response?.data; - if (data is Map) { - return _handlePaperlessValidationError(data, handler, err); - } else if (data is String) { - return _handlePlainError(data, handler, err); - } - } else if (err.response?.statusCode == 403) { - var data = err.response!.data; - if (data is Map && data.containsKey("detail")) { + final data = err.response!.data; + if (PaperlessServerMessageException.canParse(data)) { + final exception = PaperlessServerMessageException.fromJson(data); + final message = exception.detail; handler.reject( - DioError( - message: data['detail'], + DioException( + message: message, requestOptions: err.requestOptions, - error: ServerMessageException(data['detail']), + error: exception, response: err.response, - type: DioErrorType.unknown, + type: DioExceptionType.badResponse, ), ); - return; - } - } else if (err.error is SocketException) { - final ex = err.error as SocketException; - if (ex.osError?.errorCode == _OsErrorCodes.serverUnreachable.code) { - return handler.reject( - DioError( - message: "The server could not be reached. Is the device offline?", - error: const PaperlessServerException(ErrorCode.deviceOffline), + } else if (PaperlessFormValidationException.canParse(data)) { + final exception = PaperlessFormValidationException.fromJson(data); + handler.reject( + DioException( requestOptions: err.requestOptions, - type: DioErrorType.connectionTimeout, + error: exception, + response: err.response, + type: DioExceptionType.badResponse, ), ); - } - } - return handler.reject(err); - } - - void _handlePaperlessValidationError( - Map json, - ErrorInterceptorHandler handler, - DioError err, - ) { - final PaperlessValidationErrors errorMessages = {}; - for (final entry in json.entries) { - if (entry.value is List) { - errorMessages.putIfAbsent( - entry.key, - () => (entry.value as List).cast().first, + } 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 (entry.value is String) { - errorMessages.putIfAbsent(entry.key, () => entry.value); - } else { - errorMessages.putIfAbsent(entry.key, () => entry.value.toString()); } + } else { + return handler.next(err); } - handler.reject( - DioError( - error: errorMessages, - requestOptions: err.requestOptions, - type: DioErrorType.badResponse, - ), - ); } - - void _handlePlainError( - String data, - ErrorInterceptorHandler handler, - DioError err, - ) { - if (data.contains("No required SSL certificate was sent")) { - handler.reject( - DioError( - requestOptions: err.requestOptions, - type: DioErrorType.badResponse, - error: const PaperlessServerException( - ErrorCode.missingClientCertificate), - ), - ); - } - } -} - -enum _OsErrorCodes { - serverUnreachable(101); - - const _OsErrorCodes(this.code); - final int code; } diff --git a/lib/core/interceptor/dio_offline_interceptor.dart b/lib/core/interceptor/dio_offline_interceptor.dart new file mode 100644 index 00000000..b7a547cf --- /dev/null +++ b/lib/core/interceptor/dio_offline_interceptor.dart @@ -0,0 +1,32 @@ +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:paperless_api/paperless_api.dart'; + +class DioOfflineInterceptor extends Interceptor { + @override + void onError(DioException err, ErrorInterceptorHandler handler) { + if (err.error is SocketException) { + final ex = err.error as SocketException; + if (ex.osError?.errorCode == _OsErrorCodes.serverUnreachable.code) { + handler.reject( + DioException( + message: "The host could not be reached. Is your device offline?", + error: const PaperlessApiException(ErrorCode.deviceOffline), + requestOptions: err.requestOptions, + type: DioExceptionType.connectionTimeout, + ), + ); + } + } else { + handler.next(err); + } + } +} + +enum _OsErrorCodes { + serverUnreachable(101); + + const _OsErrorCodes(this.code); + final int code; +} diff --git a/lib/core/interceptor/dio_unauthorized_interceptor.dart b/lib/core/interceptor/dio_unauthorized_interceptor.dart new file mode 100644 index 00000000..cf27eb0b --- /dev/null +++ b/lib/core/interceptor/dio_unauthorized_interceptor.dart @@ -0,0 +1,27 @@ +import 'package:dio/dio.dart'; +import 'package:paperless_api/paperless_api.dart'; + +class DioUnauthorizedInterceptor extends Interceptor { + @override + void onError(DioException err, ErrorInterceptorHandler handler) { + if (err.response?.statusCode == 403) { + final data = err.response!.data; + String? message; + if (PaperlessServerMessageException.canParse(data)) { + final exception = PaperlessServerMessageException.fromJson(data); + message = exception.detail; + } + handler.reject( + DioException( + message: message, + requestOptions: err.requestOptions, + error: PaperlessUnauthorizedException(message), + response: err.response, + type: DioExceptionType.badResponse, + ), + ); + } else { + handler.next(err); + } + } +} diff --git a/lib/core/interceptor/retry_on_connection_change_interceptor.dart b/lib/core/interceptor/retry_on_connection_change_interceptor.dart index 6c78173c..483347ae 100644 --- a/lib/core/interceptor/retry_on_connection_change_interceptor.dart +++ b/lib/core/interceptor/retry_on_connection_change_interceptor.dart @@ -10,7 +10,7 @@ class RetryOnConnectionChangeInterceptor extends Interceptor { }); @override - void onError(DioError err, ErrorInterceptorHandler handler) async { + void onError(DioException err, ErrorInterceptorHandler handler) async { if (_shouldRetryOnHttpException(err)) { try { handler.resolve(await DioHttpRequestRetrier(dio: dio) @@ -27,8 +27,8 @@ class RetryOnConnectionChangeInterceptor extends Interceptor { } } - bool _shouldRetryOnHttpException(DioError err) { - return err.type == DioErrorType.unknown && + bool _shouldRetryOnHttpException(DioException err) { + return err.type == DioExceptionType.unknown && (err.error is HttpException && (err.message?.contains( 'Connection closed before full header was received', diff --git a/lib/core/interceptor/server_reachability_error_interceptor.dart b/lib/core/interceptor/server_reachability_error_interceptor.dart index a895451f..52e8ee39 100644 --- a/lib/core/interceptor/server_reachability_error_interceptor.dart +++ b/lib/core/interceptor/server_reachability_error_interceptor.dart @@ -8,7 +8,7 @@ class ServerReachabilityErrorInterceptor extends Interceptor { static const _missingClientCertText = "No required SSL certificate was sent"; @override - void onError(DioError err, ErrorInterceptorHandler handler) { + void onError(DioException err, ErrorInterceptorHandler handler) { if (err.response?.statusCode == 400) { final message = err.response?.data; if (message is String && message.contains(_missingClientCertText)) { @@ -19,7 +19,7 @@ class ServerReachabilityErrorInterceptor extends Interceptor { ); } } - if (err.type == DioErrorType.connectionTimeout) { + if (err.type == DioExceptionType.connectionTimeout) { return _rejectWithStatus( ReachabilityStatus.connectionTimeout, err, @@ -48,13 +48,13 @@ class ServerReachabilityErrorInterceptor extends Interceptor { void _rejectWithStatus( ReachabilityStatus reachabilityStatus, - DioError err, + DioException err, ErrorInterceptorHandler handler, ) { - handler.reject(DioError( + handler.reject(DioException( error: reachabilityStatus, requestOptions: err.requestOptions, response: err.response, - type: DioErrorType.unknown, + type: DioExceptionType.unknown, )); } diff --git a/lib/core/navigation/push_routes.dart b/lib/core/navigation/push_routes.dart index 05a65ee3..82bf09a2 100644 --- a/lib/core/navigation/push_routes.dart +++ b/lib/core/navigation/push_routes.dart @@ -354,6 +354,7 @@ Future pushDocumentUploadPreparationPage( final labelRepo = context.read(); final docsApi = context.read(); final connectivity = context.read(); + final apiVersion = context.read(); return Navigator.of(context).push( MaterialPageRoute( builder: (_) => MultiProvider( @@ -361,6 +362,7 @@ Future pushDocumentUploadPreparationPage( Provider.value(value: labelRepo), Provider.value(value: docsApi), Provider.value(value: connectivity), + Provider.value(value: apiVersion) ], builder: (_, child) => BlocProvider( create: (_) => DocumentUploadCubit( diff --git a/lib/core/repository/label_repository.dart b/lib/core/repository/label_repository.dart index 08899bbe..563c6663 100644 --- a/lib/core/repository/label_repository.dart +++ b/lib/core/repository/label_repository.dart @@ -88,7 +88,6 @@ class LabelRepository extends PersistentRepository { if (correspondent != null) { final updatedState = {...state.correspondents}..[id] = correspondent; emit(state.copyWith(correspondents: updatedState)); - return correspondent; } return null; diff --git a/lib/core/security/session_manager.dart b/lib/core/security/session_manager.dart index 3a366e10..8281f2a3 100644 --- a/lib/core/security/session_manager.dart +++ b/lib/core/security/session_manager.dart @@ -4,13 +4,14 @@ import 'package:dio/dio.dart'; import 'package:dio/io.dart'; import 'package:flutter/material.dart'; import 'package:paperless_mobile/core/interceptor/dio_http_error_interceptor.dart'; +import 'package:paperless_mobile/core/interceptor/dio_unauthorized_interceptor.dart'; import 'package:paperless_mobile/core/interceptor/retry_on_connection_change_interceptor.dart'; import 'package:paperless_mobile/features/login/model/client_certificate.dart'; import 'package:pretty_dio_logger/pretty_dio_logger.dart'; /// Manages the security context, authentication and base request URL for /// an underlying [Dio] client which is injected into all services -/// requiring authenticated access to the Paperless HTTP API. +/// requiring authenticated access to the Paperless REST API. class SessionManager extends ValueNotifier { Dio get client => value; @@ -20,16 +21,21 @@ class SessionManager extends ValueNotifier { static Dio _initDio(List interceptors) { //en- and decoded by utf8 by default final Dio dio = Dio( - BaseOptions(contentType: Headers.jsonContentType), + BaseOptions( + contentType: Headers.jsonContentType, + followRedirects: true, + maxRedirects: 10, + ), ); dio.options ..receiveTimeout = const Duration(seconds: 30) ..sendTimeout = const Duration(seconds: 60) ..responseType = ResponseType.json; - (dio.httpClientAdapter as IOHttpClientAdapter).onHttpClientCreate = - (client) => client..badCertificateCallback = (cert, host, port) => true; + (dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = + () => HttpClient()..badCertificateCallback = (cert, host, port) => true; dio.interceptors.addAll([ ...interceptors, + DioUnauthorizedInterceptor(), DioHttpErrorInterceptor(), PrettyDioLogger( compact: true, @@ -64,7 +70,7 @@ class SessionManager extends ValueNotifier { password: clientCertificate.passphrase, ); final adapter = IOHttpClientAdapter() - ..onHttpClientCreate = (client) => HttpClient(context: context) + ..createHttpClient = () => HttpClient(context: context) ..badCertificateCallback = (X509Certificate cert, String host, int port) => true; diff --git a/lib/core/service/connectivity_status_service.dart b/lib/core/service/connectivity_status_service.dart index 0dc1cf13..0908e485 100644 --- a/lib/core/service/connectivity_status_service.dart +++ b/lib/core/service/connectivity_status_service.dart @@ -83,8 +83,8 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService { return ReachabilityStatus.reachable; } return ReachabilityStatus.notReachable; - } on DioError catch (error) { - if (error.type == DioErrorType.unknown && + } on DioException catch (error) { + if (error.type == DioExceptionType.unknown && error.error is ReachabilityStatus) { return error.error as ReachabilityStatus; } diff --git a/lib/core/service/file_service.dart b/lib/core/service/file_service.dart index 06ba344e..881452d5 100644 --- a/lib/core/service/file_service.dart +++ b/lib/core/service/file_service.dart @@ -12,7 +12,7 @@ class FileService { ) async { final dir = await documentsDirectory; if (dir == null) { - throw const PaperlessServerException.unknown(); //TODO: better handling + throw const PaperlessApiException.unknown(); //TODO: better handling } File file = File("${dir.path}/$filename"); return file..writeAsBytes(bytes); diff --git a/lib/core/translation/error_code_localization_mapper.dart b/lib/core/translation/error_code_localization_mapper.dart index 4b2be11c..24d938c7 100644 --- a/lib/core/translation/error_code_localization_mapper.dart +++ b/lib/core/translation/error_code_localization_mapper.dart @@ -3,74 +3,75 @@ import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; String translateError(BuildContext context, ErrorCode code) { - switch (code) { - case ErrorCode.unknown: - return S.of(context)!.anUnknownErrorOccurred; - case ErrorCode.authenticationFailed: - return S.of(context)!.authenticationFailedPleaseTryAgain; - case ErrorCode.notAuthenticated: - return S.of(context)!.userIsNotAuthenticated; - case ErrorCode.documentUploadFailed: - return S.of(context)!.couldNotUploadDocument; - case ErrorCode.documentUpdateFailed: - return S.of(context)!.couldNotUpdateDocument; - case ErrorCode.documentLoadFailed: - return S.of(context)!.couldNotLoadDocuments; - case ErrorCode.documentDeleteFailed: - return S.of(context)!.couldNotDeleteDocument; - case ErrorCode.documentPreviewFailed: - return S.of(context)!.couldNotLoadDocumentPreview; - case ErrorCode.documentAsnQueryFailed: - return S.of(context)!.couldNotAssignArchiveSerialNumber; - case ErrorCode.tagCreateFailed: - return S.of(context)!.couldNotCreateTag; - case ErrorCode.tagLoadFailed: - return S.of(context)!.couldNotLoadTags; - case ErrorCode.documentTypeCreateFailed: - return S.of(context)!.couldNotCreateDocument; - case ErrorCode.documentTypeLoadFailed: - return S.of(context)!.couldNotLoadDocumentTypes; - case ErrorCode.correspondentCreateFailed: - return S.of(context)!.couldNotCreateCorrespondent; - case ErrorCode.correspondentLoadFailed: - return S.of(context)!.couldNotLoadCorrespondents; - case ErrorCode.scanRemoveFailed: - return S.of(context)!.anErrorOccurredRemovingTheScans; - case ErrorCode.invalidClientCertificateConfiguration: - return S.of(context)!.invalidCertificateOrMissingPassphrase; - case ErrorCode.documentBulkActionFailed: - return S.of(context)!.couldNotBulkEditDocuments; - case ErrorCode.biometricsNotSupported: - return S.of(context)!.biometricAuthenticationNotSupported; - case ErrorCode.biometricAuthenticationFailed: - return S.of(context)!.biometricAuthenticationFailed; - case ErrorCode.deviceOffline: - return S.of(context)!.youAreCurrentlyOffline; - case ErrorCode.serverUnreachable: - return S.of(context)!.couldNotReachYourPaperlessServer; - case ErrorCode.similarQueryError: - return S.of(context)!.couldNotLoadSimilarDocuments; - case ErrorCode.autocompleteQueryError: - return S.of(context)!.anErrorOccurredWhileTryingToAutocompleteYourQuery; - case ErrorCode.storagePathLoadFailed: - return S.of(context)!.couldNotLoadStoragePaths; - case ErrorCode.storagePathCreateFailed: - return S.of(context)!.couldNotCreateStoragePath; - case ErrorCode.loadSavedViewsError: - return S.of(context)!.couldNotLoadSavedViews; - case ErrorCode.createSavedViewError: - return S.of(context)!.couldNotCreateSavedView; - case ErrorCode.deleteSavedViewError: - return S.of(context)!.couldNotDeleteSavedView; - case ErrorCode.requestTimedOut: - return S.of(context)!.requestTimedOut; - case ErrorCode.unsupportedFileFormat: - return S.of(context)!.fileFormatNotSupported; - case ErrorCode.missingClientCertificate: - return S.of(context)!.aClientCertificateWasExpectedButNotSent; - case ErrorCode.suggestionsQueryError: - return S.of(context)!.couldNotLoadSuggestions; - case ErrorCode.acknowledgeTasksError: - return S.of(context)!.couldNotAcknowledgeTasks; - } + return switch (code) { + ErrorCode.unknown => S.of(context)!.anUnknownErrorOccurred, + ErrorCode.authenticationFailed => + S.of(context)!.authenticationFailedPleaseTryAgain, + ErrorCode.notAuthenticated => S.of(context)!.userIsNotAuthenticated, + ErrorCode.documentUploadFailed => S.of(context)!.couldNotUploadDocument, + ErrorCode.documentUpdateFailed => S.of(context)!.couldNotUpdateDocument, + ErrorCode.documentLoadFailed => S.of(context)!.couldNotLoadDocuments, + ErrorCode.documentDeleteFailed => S.of(context)!.couldNotDeleteDocument, + ErrorCode.documentPreviewFailed => + S.of(context)!.couldNotLoadDocumentPreview, + ErrorCode.documentAsnQueryFailed => + S.of(context)!.couldNotAssignArchiveSerialNumber, + ErrorCode.tagCreateFailed => S.of(context)!.couldNotCreateTag, + ErrorCode.tagLoadFailed => S.of(context)!.couldNotLoadTags, + ErrorCode.documentTypeCreateFailed => S.of(context)!.couldNotCreateDocument, + ErrorCode.documentTypeLoadFailed => + S.of(context)!.couldNotLoadDocumentTypes, + ErrorCode.correspondentCreateFailed => + S.of(context)!.couldNotCreateCorrespondent, + ErrorCode.correspondentLoadFailed => + S.of(context)!.couldNotLoadCorrespondents, + ErrorCode.scanRemoveFailed => + S.of(context)!.anErrorOccurredRemovingTheScans, + ErrorCode.invalidClientCertificateConfiguration => + S.of(context)!.invalidCertificateOrMissingPassphrase, + ErrorCode.documentBulkActionFailed => + S.of(context)!.couldNotBulkEditDocuments, + ErrorCode.biometricsNotSupported => + S.of(context)!.biometricAuthenticationNotSupported, + ErrorCode.biometricAuthenticationFailed => + S.of(context)!.biometricAuthenticationFailed, + ErrorCode.deviceOffline => S.of(context)!.youAreCurrentlyOffline, + ErrorCode.serverUnreachable => + S.of(context)!.couldNotReachYourPaperlessServer, + ErrorCode.similarQueryError => S.of(context)!.couldNotLoadSimilarDocuments, + ErrorCode.autocompleteQueryError => + S.of(context)!.anErrorOccurredWhileTryingToAutocompleteYourQuery, + ErrorCode.storagePathLoadFailed => S.of(context)!.couldNotLoadStoragePaths, + ErrorCode.storagePathCreateFailed => + S.of(context)!.couldNotCreateStoragePath, + ErrorCode.loadSavedViewsError => S.of(context)!.couldNotLoadSavedViews, + ErrorCode.createSavedViewError => S.of(context)!.couldNotCreateSavedView, + ErrorCode.deleteSavedViewError => S.of(context)!.couldNotDeleteSavedView, + ErrorCode.requestTimedOut => S.of(context)!.requestTimedOut, + ErrorCode.unsupportedFileFormat => S.of(context)!.fileFormatNotSupported, + ErrorCode.missingClientCertificate => + S.of(context)!.aClientCertificateWasExpectedButNotSent, + ErrorCode.suggestionsQueryError => S.of(context)!.couldNotLoadSuggestions, + ErrorCode.acknowledgeTasksError => S.of(context)!.couldNotAcknowledgeTasks, + ErrorCode.correspondentDeleteFailed => + "Could not delete correspondent, please try again.", + ErrorCode.documentTypeDeleteFailed => + "Could not delete document type, please try again.", + ErrorCode.tagDeleteFailed => "Could not delete tag, please try again.", + ErrorCode.correspondentUpdateFailed => + "Could not update correspondent, please try again.", + ErrorCode.documentTypeUpdateFailed => + "Could not update document type, please try again.", + ErrorCode.tagUpdateFailed => "Could not update tag, please try again.", + ErrorCode.storagePathDeleteFailed => + "Could not delete storage path, please try again.", + ErrorCode.storagePathUpdateFailed => + "Could not update storage path, please try again.", + ErrorCode.serverInformationLoadFailed => + "Could not load server information.", + ErrorCode.serverStatisticsLoadFailed => "Could not load server statistics.", + ErrorCode.uiSettingsLoadFailed => "Could not load UI settings", + ErrorCode.loadTasksError => "Could not load tasks.", + ErrorCode.userNotFound => "User could not be found.", + }; } diff --git a/lib/core/type/types.dart b/lib/core/type/types.dart deleted file mode 100644 index 3ed65fa4..00000000 --- a/lib/core/type/types.dart +++ /dev/null @@ -1,8 +0,0 @@ -typedef JSON = Map; -typedef PaperlessValidationErrors = Map; -typedef PaperlessLocalizedErrorMessage = String; - -extension ValidationErrorsUtils on PaperlessValidationErrors { - bool get hasFieldUnspecificError => containsKey("non_field_errors"); - String? get fieldUnspecificError => this['non_field_errors']; -} diff --git a/lib/features/document_details/cubit/document_details_cubit.dart b/lib/features/document_details/cubit/document_details_cubit.dart index 20cec69a..3ad70070 100644 --- a/lib/features/document_details/cubit/document_details_cubit.dart +++ b/lib/features/document_details/cubit/document_details_cubit.dart @@ -70,13 +70,12 @@ class DocumentDetailsCubit extends Cubit { Future loadFullContent() async { final doc = await _api.find(state.document.id); - if (doc == null) { - return; - } - emit(state.copyWith( - isFullContentLoaded: true, - fullContent: doc.content, - )); + emit( + state.copyWith( + isFullContentLoaded: true, + fullContent: doc.content, + ), + ); } Future assignAsn( @@ -99,13 +98,12 @@ class DocumentDetailsCubit extends Cubit { Future openDocumentInSystemViewer() async { final cacheDir = await FileService.temporaryDirectory; - //TODO: Why is this cleared here? - await FileService.clearDirectoryContent(PaperlessDirectoryType.temporary); if (state.metaData == null) { await loadMetaData(); } final desc = FileDescription.fromPath( - state.metaData!.mediaFilename.replaceAll("/", " ")); + state.metaData!.mediaFilename.replaceAll("/", " "), + ); final fileName = "${desc.filename}.pdf"; final file = File("${cacheDir.path}/$fileName"); 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 2718dc49..8732da87 100644 --- a/lib/features/document_details/view/pages/document_details_page.dart +++ b/lib/features/document_details/view/pages/document_details_page.dart @@ -287,8 +287,7 @@ class _DocumentDetailsPageState extends State { Widget _buildEditButton() { bool canEdit = context.watchInternetConnection && - LocalUserAccount.current.paperlessUser - .hasPermission(PermissionAction.change, PermissionTarget.document); + LocalUserAccount.current.paperlessUser.canEditDocuments; if (!canEdit) { return const SizedBox.shrink(); } @@ -319,8 +318,7 @@ class _DocumentDetailsPageState extends State { final isConnected = connectivityState.isConnected; final canDelete = isConnected && - LocalUserAccount.current.paperlessUser.hasPermission( - PermissionAction.delete, PermissionTarget.document); + LocalUserAccount.current.paperlessUser.canDeleteDocuments; return Row( mainAxisAlignment: MainAxisAlignment.start, children: [ @@ -430,7 +428,7 @@ class _DocumentDetailsPageState extends State { try { await context.read().delete(document); showSnackBar(context, S.of(context)!.documentSuccessfullyDeleted); - } on PaperlessServerException catch (error, stackTrace) { + } on PaperlessApiException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } finally { // Document deleted => go back to primary route 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 e7101fcd..16080100 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 @@ -4,7 +4,6 @@ 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/database/tables/local_user_account.dart'; -import 'package:paperless_mobile/core/type/types.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; @@ -48,10 +47,7 @@ class _ArchiveSerialNumberFieldState extends State { @override Widget build(BuildContext context) { final userCanEditDocument = - LocalUserAccount.current.paperlessUser.hasPermission( - PermissionAction.change, - PermissionTarget.document, - ); + LocalUserAccount.current.paperlessUser.canEditDocuments; return BlocListener( listenWhen: (previous, current) => previous.document.archiveSerialNumber != @@ -124,12 +120,14 @@ class _ArchiveSerialNumberFieldState extends State { .read() .assignAsn(widget.document, asn: asn) .then((value) => _onAsnUpdated()) - .onError( + .onError( (error, stackTrace) => showErrorMessage(context, error, stackTrace), ) - .onError( - (error, stackTrace) => setState(() => _errors = error), - ); + .onError( + (error, stackTrace) { + setState(() => _errors = error.validationMessages); + }, + ); FocusScope.of(context).unfocus(); } @@ -141,9 +139,10 @@ class _ArchiveSerialNumberFieldState extends State { autoAssign: true, ) .then((value) => _onAsnUpdated()) - .onError( + .onError( (error, stackTrace) => showErrorMessage(context, error, stackTrace), - ); + ) + .catchError((error) => showGenericError(context, error)); } void _onAsnUpdated() { diff --git a/lib/features/document_details/view/widgets/document_download_button.dart b/lib/features/document_details/view/widgets/document_download_button.dart index ab1fe749..9d348312 100644 --- a/lib/features/document_details/view/widgets/document_download_button.dart +++ b/lib/features/document_details/view/widgets/document_download_button.dart @@ -95,7 +95,7 @@ class _DocumentDownloadButtonState extends State { locale: globalSettings.preferredLocaleSubtag, ); // showSnackBar(context, S.of(context)!.documentSuccessfullyDownloaded); - } on PaperlessServerException catch (error, stackTrace) { + } on PaperlessApiException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } catch (error) { showGenericError(context, error); 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 0f3edd81..3e6804ec 100644 --- a/lib/features/document_details/view/widgets/document_overview_widget.dart +++ b/lib/features/document_details/view/widgets/document_overview_widget.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; 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/widgets/highlighted_text.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/details_item.dart'; @@ -45,38 +46,35 @@ class DocumentOverviewWidget extends StatelessWidget { context: context, label: S.of(context)!.createdAt, ).paddedOnly(bottom: itemSpacing), - Visibility( - visible: document.documentType != null, - child: DetailsItem( + if (document.documentType != null && + LocalUserAccount.current.paperlessUser.canViewDocumentTypes) + DetailsItem( label: S.of(context)!.documentType, content: LabelText( style: Theme.of(context).textTheme.bodyLarge, label: availableDocumentTypes[document.documentType], ), ).paddedOnly(bottom: itemSpacing), - ), - Visibility( - visible: document.correspondent != null, - child: DetailsItem( + if (document.correspondent != null && + LocalUserAccount.current.paperlessUser.canViewCorrespondents) + DetailsItem( label: S.of(context)!.correspondent, content: LabelText( style: Theme.of(context).textTheme.bodyLarge, label: availableCorrespondents[document.correspondent], ), ).paddedOnly(bottom: itemSpacing), - ), - Visibility( - visible: document.storagePath != null, - child: DetailsItem( + if (document.storagePath != null && + LocalUserAccount.current.paperlessUser.canViewStoragePaths) + DetailsItem( label: S.of(context)!.storagePath, content: LabelText( label: availableStoragePaths[document.storagePath], ), ).paddedOnly(bottom: itemSpacing), - ), - Visibility( - visible: document.tags.isNotEmpty, - child: DetailsItem( + if (document.tags.isNotEmpty && + LocalUserAccount.current.paperlessUser.canViewTags) + DetailsItem( label: S.of(context)!.tags, content: Padding( padding: const EdgeInsets.only(top: 8.0), @@ -86,7 +84,6 @@ class DocumentOverviewWidget extends StatelessWidget { ), ), ).paddedOnly(bottom: itemSpacing), - ), ], ), ); diff --git a/lib/features/document_details/view/widgets/document_share_button.dart b/lib/features/document_details/view/widgets/document_share_button.dart index ea6a052d..257ac6b6 100644 --- a/lib/features/document_details/view/widgets/document_share_button.dart +++ b/lib/features/document_details/view/widgets/document_share_button.dart @@ -90,7 +90,7 @@ class _DocumentShareButtonState extends State { await context.read().shareDocument( shareOriginal: original, ); - } on PaperlessServerException catch (error, stackTrace) { + } on PaperlessApiException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } catch (error) { showGenericError(context, error); diff --git a/lib/features/document_edit/view/document_edit_page.dart b/lib/features/document_edit/view/document_edit_page.dart index 658d5e30..ec83909b 100644 --- a/lib/features/document_edit/view/document_edit_page.dart +++ b/lib/features/document_edit/view/document_edit_page.dart @@ -123,12 +123,8 @@ class _DocumentEditPageState extends State { name: fkCorrespondent, prefixIcon: const Icon(Icons.person_outlined), allowSelectUnassigned: true, - canCreateNewLabel: LocalUserAccount - .current.paperlessUser - .hasPermission( - PermissionAction.add, - PermissionTarget.correspondent, - ), + canCreateNewLabel: LocalUserAccount.current + .paperlessUser.canCreateCorrespondents, ), if (_filteredSuggestions ?.hasSuggestedCorrespondents ?? @@ -164,12 +160,8 @@ class _DocumentEditPageState extends State { initialName: currentInput, ), ), - canCreateNewLabel: LocalUserAccount - .current.paperlessUser - .hasPermission( - PermissionAction.add, - PermissionTarget.documentType, - ), + canCreateNewLabel: LocalUserAccount.current + .paperlessUser.canCreateDocumentTypes, addLabelText: S.of(context)!.addDocumentType, labelText: S.of(context)!.documentType, initialValue: @@ -214,12 +206,8 @@ class _DocumentEditPageState extends State { child: AddStoragePathPage( initalName: initialValue), ), - canCreateNewLabel: LocalUserAccount - .current.paperlessUser - .hasPermission( - PermissionAction.add, - PermissionTarget.storagePath, - ), + canCreateNewLabel: LocalUserAccount.current + .paperlessUser.canCreateStoragePaths, addLabelText: S.of(context)!.addStoragePath, labelText: S.of(context)!.storagePath, options: state.storagePaths, @@ -328,7 +316,7 @@ class _DocumentEditPageState extends State { try { await context.read().updateDocument(mergedDocument); showSnackBar(context, S.of(context)!.documentSuccessfullyUpdated); - } on PaperlessServerException catch (error, stackTrace) { + } on PaperlessApiException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } finally { setState(() { diff --git a/lib/features/document_scan/cubit/document_scanner_cubit.dart b/lib/features/document_scan/cubit/document_scanner_cubit.dart index 93657c2b..3e0b9a54 100644 --- a/lib/features/document_scan/cubit/document_scanner_cubit.dart +++ b/lib/features/document_scan/cubit/document_scanner_cubit.dart @@ -22,7 +22,7 @@ class DocumentScannerCubit extends Cubit> { scans.removeAt(fileIndex); emit(scans); } catch (_) { - throw const PaperlessServerException(ErrorCode.scanRemoveFailed); + throw const PaperlessApiException(ErrorCode.scanRemoveFailed); } } @@ -37,7 +37,7 @@ class DocumentScannerCubit extends Cubit> { imageCache.clear(); emit([]); } catch (_) { - throw const PaperlessServerException(ErrorCode.scanRemoveFailed); + throw const PaperlessApiException(ErrorCode.scanRemoveFailed); } } diff --git a/lib/features/document_scan/view/scanner_page.dart b/lib/features/document_scan/view/scanner_page.dart index 776c36eb..71aed2b4 100644 --- a/lib/features/document_scan/view/scanner_page.dart +++ b/lib/features/document_scan/view/scanner_page.dart @@ -73,7 +73,9 @@ class _ScannerPageState extends State headerSliverBuilder: (context, innerBoxIsScrolled) => [ SliverOverlapAbsorber( handle: searchBarHandle, - sliver: const SliverSearchBar(), + sliver: SliverSearchBar( + titleText: S.of(context)!.scanner, + ), ), SliverOverlapAbsorber( handle: actionsHandle, @@ -322,7 +324,7 @@ class _ScannerPageState extends State onDelete: () async { try { context.read().removeScan(index); - } on PaperlessServerException catch (error, stackTrace) { + } on PaperlessApiException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } }, @@ -339,7 +341,7 @@ class _ScannerPageState extends State void _reset(BuildContext context) { try { context.read().reset(); - } on PaperlessServerException catch (error, stackTrace) { + } on PaperlessApiException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } } @@ -360,7 +362,7 @@ class _ScannerPageState extends State )) { showErrorMessage( context, - const PaperlessServerException(ErrorCode.unsupportedFileFormat), + const PaperlessApiException(ErrorCode.unsupportedFileFormat), ); return; } diff --git a/lib/features/document_search/view/sliver_search_bar.dart b/lib/features/document_search/view/sliver_search_bar.dart index f716eb6f..67487b9a 100644 --- a/lib/features/document_search/view/sliver_search_bar.dart +++ b/lib/features/document_search/view/sliver_search_bar.dart @@ -1,32 +1,74 @@ import 'package:flutter/material.dart'; -import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hive/hive.dart'; +import 'package:hive_flutter/adapters.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/config/hive/hive_config.dart'; +import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/features/document_search/view/document_search_bar.dart'; +import 'package:paperless_mobile/features/home/view/model/api_version.dart'; +import 'package:paperless_mobile/features/settings/view/manage_accounts_page.dart'; +import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart'; +import 'package:paperless_mobile/features/settings/view/widgets/user_avatar.dart'; +import 'package:provider/provider.dart'; class SliverSearchBar extends StatelessWidget { final bool floating; final bool pinned; + final String titleText; const SliverSearchBar({ super.key, this.floating = false, this.pinned = false, + required this.titleText, }); @override Widget build(BuildContext context) { - return SliverPadding( - padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top), - sliver: SliverPersistentHeader( - floating: floating, - pinned: pinned, - delegate: CustomizableSliverPersistentHeaderDelegate( - minExtent: kToolbarHeight, - maxExtent: kToolbarHeight, - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 16.0), - child: const DocumentSearchBar(), - ), + if (LocalUserAccount.current.paperlessUser.canViewDocuments) { + return SliverAppBar( + toolbarHeight: kToolbarHeight, + flexibleSpace: Container( + margin: const EdgeInsets.symmetric(horizontal: 16.0), + child: const DocumentSearchBar(), ), - ), - ); + automaticallyImplyLeading: false, + ); + } else { + return SliverAppBar( + title: Text(titleText), + actions: [ + Padding( + padding: const EdgeInsets.only(right: 12.0), + child: IconButton( + padding: const EdgeInsets.all(6), + icon: GlobalSettingsBuilder( + builder: (context, settings) { + return ValueListenableBuilder( + valueListenable: + Hive.box(HiveBoxes.localUserAccount) + .listenable(), + builder: (context, box, _) { + final account = box.get(settings.currentLoggedInUser!)!; + return UserAvatar(account: account); + }, + ); + }, + ), + onPressed: () { + final apiVersion = context.read(); + showDialog( + context: context, + builder: (context) => Provider.value( + value: apiVersion, + child: const ManageAccountsPage(), + ), + ); + }, + ), + ), + ], + ); + } } } 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 7a7c6014..a1537ce5 100644 --- a/lib/features/document_upload/view/document_upload_preparation_page.dart +++ b/lib/features/document_upload/view/document_upload_preparation_page.dart @@ -12,16 +12,17 @@ import 'package:paperless_mobile/core/config/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/type/types.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart'; import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart'; import 'package:paperless_mobile/features/edit_label/view/impl/add_document_type_page.dart'; +import 'package:paperless_mobile/features/home/view/model/api_version.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart'; import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; +import 'package:provider/provider.dart'; class DocumentUploadResult { final bool success; @@ -56,7 +57,7 @@ class _DocumentUploadPreparationPageState final GlobalKey _formKey = GlobalKey(); - PaperlessValidationErrors _errors = {}; + Map _errors = {}; bool _isUploadLoading = false; late bool _syncTitleAndFilename; bool _showDatePickerDeleteIcon = false; @@ -197,54 +198,64 @@ class _DocumentUploadPreparationPageState ), ), // Correspondent - LabelFormField( - showAnyAssignedOption: false, - showNotAssignedOption: false, - addLabelPageBuilder: (initialName) => - RepositoryProvider.value( - value: context.read(), - child: AddCorrespondentPage(initialName: initialName), - ), - addLabelText: S.of(context)!.addCorrespondent, - labelText: S.of(context)!.correspondent + " *", - name: DocumentModel.correspondentKey, - options: state.correspondents, - prefixIcon: const Icon(Icons.person_outline), - allowSelectUnassigned: true, - canCreateNewLabel: - LocalUserAccount.current.paperlessUser.hasPermission( - PermissionAction.add, - PermissionTarget.correspondent, + if (LocalUserAccount + .current.paperlessUser.canViewCorrespondents) + LabelFormField( + showAnyAssignedOption: false, + showNotAssignedOption: false, + addLabelPageBuilder: (initialName) => MultiProvider( + providers: [ + Provider.value( + value: context.read(), + ), + Provider.value( + value: context.read(), + ) + ], + child: AddCorrespondentPage(initialName: initialName), + ), + addLabelText: S.of(context)!.addCorrespondent, + labelText: S.of(context)!.correspondent + " *", + name: DocumentModel.correspondentKey, + options: state.correspondents, + prefixIcon: const Icon(Icons.person_outline), + allowSelectUnassigned: true, + canCreateNewLabel: LocalUserAccount + .current.paperlessUser.canCreateCorrespondents, ), - ), // Document type - LabelFormField( - showAnyAssignedOption: false, - showNotAssignedOption: false, - addLabelPageBuilder: (initialName) => - RepositoryProvider.value( - value: context.read(), - child: AddDocumentTypePage(initialName: initialName), + if (LocalUserAccount.current.paperlessUser.canViewDocumentTypes) + LabelFormField( + showAnyAssignedOption: false, + showNotAssignedOption: false, + addLabelPageBuilder: (initialName) => MultiProvider( + providers: [ + Provider.value( + value: context.read(), + ), + Provider.value( + value: context.read(), + ) + ], + child: AddDocumentTypePage(initialName: initialName), + ), + addLabelText: S.of(context)!.addDocumentType, + labelText: S.of(context)!.documentType + " *", + name: DocumentModel.documentTypeKey, + options: state.documentTypes, + prefixIcon: const Icon(Icons.description_outlined), + allowSelectUnassigned: true, + canCreateNewLabel: LocalUserAccount + .current.paperlessUser.canCreateDocumentTypes, ), - addLabelText: S.of(context)!.addDocumentType, - labelText: S.of(context)!.documentType + " *", - name: DocumentModel.documentTypeKey, - options: state.documentTypes, - prefixIcon: const Icon(Icons.description_outlined), - allowSelectUnassigned: true, - canCreateNewLabel: - LocalUserAccount.current.paperlessUser.hasPermission( - PermissionAction.add, - PermissionTarget.documentType, + if (LocalUserAccount.current.paperlessUser.canViewTags) + TagsFormField( + name: DocumentModel.tagsKey, + allowCreation: true, + allowExclude: false, + allowOnlySelection: true, + options: state.tags, ), - ), - TagsFormField( - name: DocumentModel.tagsKey, - allowCreation: true, - allowExclude: false, - allowOnlySelection: true, - options: state.tags, - ), Text( "* " + S.of(context)!.uploadInferValuesHint, style: Theme.of(context).textTheme.bodySmall, @@ -301,14 +312,14 @@ class _DocumentUploadPreparationPageState context, DocumentUploadResult(true, taskId), ); - } on PaperlessServerException catch (error, stackTrace) { + } on PaperlessApiException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); - } on PaperlessValidationErrors catch (errors) { - setState(() => _errors = errors); + } on PaperlessFormValidationException catch (exception) { + setState(() => _errors = exception.validationMessages); } catch (unknownError, stackTrace) { debugPrint(unknownError.toString()); showErrorMessage( - context, const PaperlessServerException.unknown(), stackTrace); + context, const PaperlessApiException.unknown(), stackTrace); } finally { setState(() { _isUploadLoading = false; diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index 0673ae6a..4cdbd323 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; +import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart'; import 'package:paperless_mobile/core/navigation/push_routes.dart'; import 'package:paperless_mobile/core/widgets/material/colored_tab_bar.dart'; @@ -53,14 +54,16 @@ class _DocumentsPageState extends State @override void initState() { super.initState(); + final showSavedViews = + LocalUserAccount.current.paperlessUser.canViewSavedViews; _tabController = TabController( - length: 2, + length: showSavedViews ? 2 : 1, vsync: this, ); Future.wait([ context.read().reload(), context.read().reload(), - ]).onError( + ]).onError( (error, stackTrace) { showErrorMessage(context, error, stackTrace); return []; @@ -105,7 +108,7 @@ class _DocumentsPageState extends State listener: (context, state) { try { context.read().reload(); - } on PaperlessServerException catch (error, stackTrace) { + } on PaperlessApiException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } }, @@ -197,7 +200,10 @@ class _DocumentsPageState extends State sliver: BlocBuilder( builder: (context, state) { if (state.selection.isEmpty) { - return const SliverSearchBar(floating: true); + return SliverSearchBar( + floating: true, + titleText: S.of(context)!.documents, + ); } else { return DocumentSelectionSliverAppBar( state: state, @@ -226,7 +232,9 @@ class _DocumentsPageState extends State controller: _tabController, tabs: [ Tab(text: S.of(context)!.documents), - Tab(text: S.of(context)!.views), + if (LocalUserAccount.current.paperlessUser + .canViewSavedViews) + Tab(text: S.of(context)!.views), ], ), ), @@ -268,14 +276,16 @@ class _DocumentsPageState extends State ); }, ), - Builder( - builder: (context) { - return _buildSavedViewsTab( - connectivityState, - context, - ); - }, - ), + if (LocalUserAccount + .current.paperlessUser.canViewSavedViews) + Builder( + builder: (context) { + return _buildSavedViewsTab( + connectivityState, + context, + ); + }, + ), ], ), ), @@ -334,7 +344,7 @@ class _DocumentsPageState extends State context .read() .loadMore() - .onError( + .onError( (error, stackTrace) => showErrorMessage( context, error, @@ -419,7 +429,7 @@ class _DocumentsPageState extends State if (newView != null) { try { await context.read().add(newView); - } on PaperlessServerException catch (error, stackTrace) { + } on PaperlessApiException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } } @@ -472,7 +482,7 @@ class _DocumentsPageState extends State .read() .updateFilter(filter: filterIntent.filter!); } - } on PaperlessServerException catch (error, stackTrace) { + } on PaperlessApiException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } } @@ -524,7 +534,7 @@ class _DocumentsPageState extends State ); }, ); - } on PaperlessServerException catch (error, stackTrace) { + } on PaperlessApiException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } } @@ -555,7 +565,7 @@ class _DocumentsPageState extends State ); }, ); - } on PaperlessServerException catch (error, stackTrace) { + } on PaperlessApiException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } } @@ -586,7 +596,7 @@ class _DocumentsPageState extends State ); }, ); - } on PaperlessServerException catch (error, stackTrace) { + } on PaperlessApiException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } } @@ -617,7 +627,7 @@ class _DocumentsPageState extends State ); }, ); - } on PaperlessServerException catch (error, stackTrace) { + } on PaperlessApiException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } } @@ -626,7 +636,7 @@ class _DocumentsPageState extends State try { // We do not await here on purpose so we can show a linear progress indicator below the app bar. await context.read().reload(); - } on PaperlessServerException catch (error, stackTrace) { + } on PaperlessApiException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } } @@ -635,7 +645,7 @@ class _DocumentsPageState extends State try { // We do not await here on purpose so we can show a linear progress indicator below the app bar. await context.read().reload(); - } on PaperlessServerException catch (error, stackTrace) { + } on PaperlessApiException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } } 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 0f7d72ed..41a9ca09 100644 --- a/lib/features/documents/view/widgets/search/document_filter_form.dart +++ b/lib/features/documents/view/widgets/search/document_filter_form.dart @@ -160,10 +160,8 @@ class _DocumentFilterFormState extends State { initialValue: widget.initialFilter.documentType, prefixIcon: const Icon(Icons.description_outlined), allowSelectUnassigned: false, - canCreateNewLabel: LocalUserAccount.current.paperlessUser.hasPermission( - PermissionAction.add, - PermissionTarget.documentType, - ), + canCreateNewLabel: + LocalUserAccount.current.paperlessUser.canCreateDocumentTypes, ); } @@ -175,10 +173,8 @@ class _DocumentFilterFormState extends State { initialValue: widget.initialFilter.correspondent, prefixIcon: const Icon(Icons.person_outline), allowSelectUnassigned: false, - canCreateNewLabel: LocalUserAccount.current.paperlessUser.hasPermission( - PermissionAction.add, - PermissionTarget.correspondent, - ), + canCreateNewLabel: + LocalUserAccount.current.paperlessUser.canCreateCorrespondents, ); } @@ -190,10 +186,8 @@ class _DocumentFilterFormState extends State { initialValue: widget.initialFilter.storagePath, prefixIcon: const Icon(Icons.folder_outlined), allowSelectUnassigned: false, - canCreateNewLabel: LocalUserAccount.current.paperlessUser.hasPermission( - PermissionAction.add, - PermissionTarget.storagePath, - ), + canCreateNewLabel: + LocalUserAccount.current.paperlessUser.canCreateStoragePaths, ); } diff --git a/lib/features/documents/view/widgets/selection/document_selection_sliver_app_bar.dart b/lib/features/documents/view/widgets/selection/document_selection_sliver_app_bar.dart index 5fba3d13..76b47849 100644 --- a/lib/features/documents/view/widgets/selection/document_selection_sliver_app_bar.dart +++ b/lib/features/documents/view/widgets/selection/document_selection_sliver_app_bar.dart @@ -47,7 +47,7 @@ class DocumentSelectionSliverAppBar extends StatelessWidget { S.of(context)!.documentsSuccessfullyDeleted, ); context.read().resetSelection(); - } on PaperlessServerException catch (error, stackTrace) { + } on PaperlessApiException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } } diff --git a/lib/features/edit_label/view/edit_label_page.dart b/lib/features/edit_label/view/edit_label_page.dart index 7b3044ff..7ad57e40 100644 --- a/lib/features/edit_label/view/edit_label_page.dart +++ b/lib/features/edit_label/view/edit_label_page.dart @@ -114,7 +114,7 @@ class EditLabelForm extends StatelessWidget { if (shouldDelete) { try { onDelete(context, label); - } on PaperlessServerException catch (error) { + } on PaperlessApiException catch (error) { showErrorMessage(context, error); } catch (error, stackTrace) { log("An error occurred!", error: error, stackTrace: stackTrace); 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 3df872f8..1dce099e 100644 --- a/lib/features/edit_label/view/impl/edit_correspondent_page.dart +++ b/lib/features/edit_label/view/impl/edit_correspondent_page.dart @@ -24,10 +24,8 @@ class EditCorrespondentPage extends StatelessWidget { context.read().replaceCorrespondent(label), onDelete: (context, label) => context.read().removeCorrespondent(label), - canDelete: LocalUserAccount.current.paperlessUser.hasPermission( - PermissionAction.delete, - PermissionTarget.correspondent, - ), + canDelete: + LocalUserAccount.current.paperlessUser.canDeleteCorrespondents, ); }), ); 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 e2635846..afd2f7ff 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 @@ -22,10 +22,8 @@ class EditDocumentTypePage extends StatelessWidget { context.read().replaceDocumentType(label), onDelete: (context, label) => context.read().removeDocumentType(label), - canDelete: LocalUserAccount.current.paperlessUser.hasPermission( - PermissionAction.delete, - PermissionTarget.documentType, - ), + canDelete: + LocalUserAccount.current.paperlessUser.canDeleteDocumentTypes, ), ); } 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 f951cd23..3a56b554 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 @@ -23,10 +23,7 @@ class EditStoragePathPage extends StatelessWidget { context.read().replaceStoragePath(label), onDelete: (context, label) => context.read().removeStoragePath(label), - canDelete: LocalUserAccount.current.paperlessUser.hasPermission( - PermissionAction.delete, - PermissionTarget.storagePath, - ), + canDelete: LocalUserAccount.current.paperlessUser.canDeleteStoragePaths, additionalFields: [ StoragePathAutofillFormBuilderField( name: StoragePath.pathKey, 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 3c53d1cc..fbd62af8 100644 --- a/lib/features/edit_label/view/impl/edit_tag_page.dart +++ b/lib/features/edit_label/view/impl/edit_tag_page.dart @@ -26,10 +26,7 @@ class EditTagPage extends StatelessWidget { context.read().replaceTag(label), onDelete: (context, label) => context.read().removeTag(label), - canDelete: LocalUserAccount.current.paperlessUser.hasPermission( - PermissionAction.delete, - PermissionTarget.tag, - ), + canDelete: LocalUserAccount.current.paperlessUser.canDeleteTags, additionalFields: [ FormBuilderColorPickerField( initialValue: tag.color, diff --git a/lib/features/edit_label/view/label_form.dart b/lib/features/edit_label/view/label_form.dart index 00b33c32..0a266003 100644 --- a/lib/features/edit_label/view/label_form.dart +++ b/lib/features/edit_label/view/label_form.dart @@ -4,7 +4,6 @@ import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/translation/matching_algorithm_localization_mapper.dart'; -import 'package:paperless_mobile/core/type/types.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/home/view/model/api_version.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; @@ -54,7 +53,7 @@ class _LabelFormState extends State> { late bool _enableMatchFormField; - PaperlessValidationErrors _errors = {}; + Map _errors = {}; @override void initState() { @@ -69,7 +68,8 @@ class _LabelFormState extends State> { Widget build(BuildContext context) { List selectableMatchingAlgorithmValues = getSelectableMatchingAlgorithmValues( - context.watch().hasMultiUserSupport); + context.watch().hasMultiUserSupport, + ); return Scaffold( resizeToAvoidBottomInset: false, floatingActionButton: FloatingActionButton.extended( @@ -168,10 +168,10 @@ class _LabelFormState extends State> { final parsed = widget.fromJsonT(mergedJson); final createdLabel = await widget.submitButtonConfig.onSubmit(parsed); Navigator.pop(context, createdLabel); - } on PaperlessServerException catch (error, stackTrace) { + } on PaperlessApiException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); - } on PaperlessValidationErrors catch (errors) { - setState(() => _errors = errors); + } on PaperlessFormValidationException catch (exception) { + setState(() => _errors = exception.validationMessages); } } } diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index f34ab538..ae05192a 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -44,14 +44,14 @@ class HomePage extends StatefulWidget { class _HomePageState extends State with WidgetsBindingObserver { int _currentIndex = 0; - late Timer _inboxTimer; + Timer? _inboxTimer; late final StreamSubscription _shareMediaSubscription; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); - _listenToInboxChanges(); + final currentUser = Hive.box(HiveBoxes.globalSettings) .getValue()! .currentLoggedInUser!; @@ -73,13 +73,15 @@ class _HomePageState extends State with WidgetsBindingObserver { } void _listenToInboxChanges() { - _inboxTimer = Timer.periodic(const Duration(seconds: 60), (timer) { - if (!mounted) { - timer.cancel(); - } else { - context.read().refreshItemsInInboxCount(); - } - }); + if (LocalUserAccount.current.paperlessUser.canViewTags) { + _inboxTimer = Timer.periodic(const Duration(seconds: 60), (timer) { + if (!mounted) { + timer.cancel(); + } else { + context.read().refreshItemsInInboxCount(); + } + }); + } } @override @@ -89,7 +91,7 @@ class _HomePageState extends State with WidgetsBindingObserver { log('App is now in foreground'); context.read().reload(); log("Reloaded device connectivity state"); - if (!_inboxTimer.isActive) { + if (!(_inboxTimer?.isActive ?? true)) { _listenToInboxChanges(); } break; @@ -98,7 +100,7 @@ class _HomePageState extends State with WidgetsBindingObserver { case AppLifecycleState.detached: default: log('App is now in background'); - _inboxTimer.cancel(); + _inboxTimer?.cancel(); break; } } @@ -106,7 +108,7 @@ class _HomePageState extends State with WidgetsBindingObserver { @override void dispose() { WidgetsBinding.instance.removeObserver(this); - _inboxTimer.cancel(); + _inboxTimer?.cancel(); _shareMediaSubscription.cancel(); super.dispose(); } @@ -158,8 +160,7 @@ class _HomePageState extends State with WidgetsBindingObserver { return; } - if (!LocalUserAccount.current.paperlessUser - .hasPermission(PermissionAction.add, PermissionTarget.document)) { + if (!LocalUserAccount.current.paperlessUser.canCreateDocuments) { Fluttertoast.showToast( msg: "You do not have the permissions to upload documents.", ); @@ -200,8 +201,7 @@ class _HomePageState extends State with WidgetsBindingObserver { ), label: S.of(context)!.documents, ), - if (LocalUserAccount.current.paperlessUser - .hasPermission(PermissionAction.add, PermissionTarget.document)) + if (LocalUserAccount.current.paperlessUser.canCreateDocuments) RouteDescription( icon: const Icon(Icons.document_scanner_outlined), selectedIcon: Icon( @@ -218,33 +218,31 @@ class _HomePageState extends State with WidgetsBindingObserver { ), label: S.of(context)!.labels, ), - RouteDescription( - icon: const Icon(Icons.inbox_outlined), - selectedIcon: Icon( - Icons.inbox, - color: Theme.of(context).colorScheme.primary, - ), - label: S.of(context)!.inbox, - badgeBuilder: (icon) => BlocBuilder( - builder: (context, state) { - return Badge.count( - isLabelVisible: state.itemsInInboxCount > 0, - count: state.itemsInInboxCount, - child: icon, - ); - }, + if (LocalUserAccount.current.paperlessUser.canViewTags) + RouteDescription( + icon: const Icon(Icons.inbox_outlined), + selectedIcon: Icon( + Icons.inbox, + color: Theme.of(context).colorScheme.primary, + ), + label: S.of(context)!.inbox, + badgeBuilder: (icon) => BlocBuilder( + builder: (context, state) { + return Badge.count( + isLabelVisible: state.itemsInInboxCount > 0, + count: state.itemsInInboxCount, + child: icon, + ); + }, + ), ), - ), ]; final routes = [ const DocumentsPage(), - if (LocalUserAccount.current.paperlessUser.hasPermission( - PermissionAction.add, - PermissionTarget.document, - )) + if (LocalUserAccount.current.paperlessUser.canCreateDocuments) const ScannerPage(), const LabelsPage(), - const InboxPage(), + if (LocalUserAccount.current.paperlessUser.canViewTags) const InboxPage(), ]; return MultiBlocListener( listeners: [ diff --git a/lib/features/home/view/home_route.dart b/lib/features/home/view/home_route.dart index f27d48c0..7b3b0829 100644 --- a/lib/features/home/view/home_route.dart +++ b/lib/features/home/view/home_route.dart @@ -3,6 +3,7 @@ import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:hive_flutter/adapters.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/config/hive/hive_config.dart'; +import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart'; import 'package:paperless_mobile/core/factory/paperless_api_factory.dart'; import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; @@ -43,7 +44,14 @@ class HomeRoute extends StatelessWidget { Widget build(BuildContext context) { return GlobalSettingsBuilder( builder: (context, settings) { - final currentLocalUserId = settings.currentLoggedInUser!; + final currentLocalUserId = settings.currentLoggedInUser; + if (currentLocalUserId == null) { + // This is the case when the current user logs out of the app. + return SizedBox.shrink(); + } + final currentUser = + Hive.box(HiveBoxes.localUserAccount) + .get(currentLocalUserId)!; final apiVersion = ApiVersion(paperlessApiVersion); return MultiProvider( providers: [ @@ -104,12 +112,31 @@ class HomeRoute extends StatelessWidget { return MultiProvider( providers: [ ProxyProvider( - update: (context, value, previous) => - LabelRepository(value)..initialize(), + update: (context, value, previous) { + final repo = LabelRepository(value); + if (currentUser.paperlessUser.canViewCorrespondents) { + repo.findAllCorrespondents(); + } + if (currentUser.paperlessUser.canViewDocumentTypes) { + repo.findAllDocumentTypes(); + } + if (currentUser.paperlessUser.canViewTags) { + repo.findAllTags(); + } + if (currentUser.paperlessUser.canViewStoragePaths) { + repo.findAllStoragePaths(); + } + return repo; + }, ), ProxyProvider( - update: (context, value, previous) => - SavedViewRepository(value)..initialize(), + update: (context, value, previous) { + final repo = SavedViewRepository(value); + if (currentUser.paperlessUser.canViewSavedViews) { + repo.initialize(); + } + return repo; + }, ), ], builder: (context, child) { diff --git a/lib/features/inbox/view/pages/inbox_page.dart b/lib/features/inbox/view/pages/inbox_page.dart index 37fad1df..32b79344 100644 --- a/lib/features/inbox/view/pages/inbox_page.dart +++ b/lib/features/inbox/view/pages/inbox_page.dart @@ -38,8 +38,8 @@ class _InboxPageState extends State @override Widget build(BuildContext context) { - final canEditDocument = LocalUserAccount.current.paperlessUser - .hasPermission(PermissionAction.change, PermissionTarget.document); + final canEditDocument = + LocalUserAccount.current.paperlessUser.canEditDocuments; return Scaffold( drawer: const AppDrawer(), floatingActionButton: BlocBuilder( @@ -65,7 +65,9 @@ class _InboxPageState extends State headerSliverBuilder: (context, innerBoxIsScrolled) => [ SliverOverlapAbsorber( handle: searchBarHandle, - sliver: const SliverSearchBar(), + sliver: SliverSearchBar( + titleText: S.of(context)!.inbox, + ), ) ], body: BlocBuilder( @@ -222,14 +224,14 @@ class _InboxPageState extends State ), ); return true; - } on PaperlessServerException catch (error, stackTrace) { + } on PaperlessApiException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } on ServerMessageException catch (error) { showGenericError(context, error.message); } catch (error) { showErrorMessage( context, - const PaperlessServerException.unknown(), + const PaperlessApiException.unknown(), ); } return false; @@ -243,7 +245,7 @@ class _InboxPageState extends State await context .read() .undoRemoveFromInbox(document, removedTags); - } on PaperlessServerException catch (error, stackTrace) { + } on PaperlessApiException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } } diff --git a/lib/features/inbox/view/widgets/inbox_item.dart b/lib/features/inbox/view/widgets/inbox_item.dart index a538b41e..c0da6050 100644 --- a/lib/features/inbox/view/widgets/inbox_item.dart +++ b/lib/features/inbox/view/widgets/inbox_item.dart @@ -238,10 +238,8 @@ class _InboxItemState extends State { } Widget _buildActions(BuildContext context) { - final canEdit = LocalUserAccount.current.paperlessUser - .hasPermission(PermissionAction.change, PermissionTarget.document); - final canDelete = LocalUserAccount.current.paperlessUser - .hasPermission(PermissionAction.delete, PermissionTarget.document); + final canEdit = LocalUserAccount.current.paperlessUser.canEditDocuments; + final canDelete = LocalUserAccount.current.paperlessUser.canDeleteDocuments; final chipShape = RoundedRectangleBorder( borderRadius: BorderRadius.circular(32), ); diff --git a/lib/features/labels/tags/view/widgets/tags_form_field.dart b/lib/features/labels/tags/view/widgets/tags_form_field.dart index 2f1781f1..1339f70f 100644 --- a/lib/features/labels/tags/view/widgets/tags_form_field.dart +++ b/lib/features/labels/tags/view/widgets/tags_form_field.dart @@ -73,10 +73,7 @@ class TagsFormField extends StatelessWidget { initialValue: field.value, allowOnlySelection: allowOnlySelection, allowCreation: allowCreation && - LocalUserAccount.current.paperlessUser.hasPermission( - PermissionAction.add, - PermissionTarget.tag, - ), + LocalUserAccount.current.paperlessUser.canCreateTags, allowExclude: allowExclude, ), onClosed: (data) { diff --git a/lib/features/labels/view/pages/labels_page.dart b/lib/features/labels/view/pages/labels_page.dart index 4ebcf2f0..71bd9a42 100644 --- a/lib/features/labels/view/pages/labels_page.dart +++ b/lib/features/labels/view/pages/labels_page.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hive_flutter/adapters.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; +import 'package:paperless_mobile/core/config/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/delegate/customizable_sliver_persistent_header_delegate.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; @@ -39,291 +42,327 @@ class _LabelsPageState extends State late final TabController _tabController; int _currentIndex = 0; + int _calculateTabCount(UserModel user) => [ + user.canViewCorrespondents, + user.canViewDocumentTypes, + user.canViewTags, + user.canViewStoragePaths, + ].fold(0, (value, element) => value + (element ? 1 : 0)); + @override void initState() { super.initState(); - - _tabController = TabController(length: 4, vsync: this) + final user = LocalUserAccount.current.paperlessUser; + _tabController = TabController( + length: _calculateTabCount(user), vsync: this) ..addListener(() => setState(() => _currentIndex = _tabController.index)); } @override Widget build(BuildContext context) { - return DefaultTabController( - length: 3, - child: BlocBuilder( - builder: (context, connectedState) { - return SafeArea( - child: Scaffold( - drawer: const AppDrawer(), - floatingActionButton: FloatingActionButton( - onPressed: [ - _openAddCorrespondentPage, - _openAddDocumentTypePage, - _openAddTagPage, - _openAddStoragePathPage, - ][_currentIndex], - child: const Icon(Icons.add), - ), - body: NestedScrollView( - floatHeaderSlivers: true, - headerSliverBuilder: (context, innerBoxIsScrolled) => [ - SliverOverlapAbsorber( - handle: searchBarHandle, - sliver: const SliverSearchBar(), - ), - SliverOverlapAbsorber( - handle: tabBarHandle, - sliver: SliverPersistentHeader( - pinned: true, - delegate: CustomizableSliverPersistentHeaderDelegate( - child: ColoredTabBar( - tabBar: TabBar( - controller: _tabController, - tabs: [ - Tab( - icon: Icon( - Icons.person_outline, - color: Theme.of(context) - .colorScheme - .onPrimaryContainer, - ), - ), - Tab( - icon: Icon( - Icons.description_outlined, - color: Theme.of(context) - .colorScheme - .onPrimaryContainer, - ), - ), - Tab( - icon: Icon( - Icons.label_outline, - color: Theme.of(context) - .colorScheme - .onPrimaryContainer, - ), - ), - Tab( - icon: Icon( - Icons.folder_open, - color: Theme.of(context) - .colorScheme - .onPrimaryContainer, - ), - ), - ], - ), - ), - minExtent: kTextTabBarHeight, - maxExtent: kTextTabBarHeight), - ), - ), - ], - body: BlocBuilder( - builder: (context, state) { - return NotificationListener( - onNotification: (notification) { - final metrics = notification.metrics; - if (metrics.maxScrollExtent == 0) { - return true; - } - final desiredTab = - ((metrics.pixels / metrics.maxScrollExtent) * - (_tabController.length - 1)) - .round(); + return ValueListenableBuilder( + valueListenable: + Hive.box(HiveBoxes.localUserAccount).listenable(), + builder: (context, box, child) { + final currentUserId = + Hive.box(HiveBoxes.globalSettings) + .getValue()! + .currentLoggedInUser; + final user = box.get(currentUserId)!.paperlessUser; - if (metrics.axis == Axis.horizontal && - _currentIndex != desiredTab) { - setState(() => _currentIndex = desiredTab); - } - return true; - }, - child: RefreshIndicator( - edgeOffset: kTextTabBarHeight, - notificationPredicate: (notification) => - connectedState.isConnected, - onRefresh: () async { - try { - await [ - context.read().reloadCorrespondents, - context.read().reloadDocumentTypes, - context.read().reloadTags, - context.read().reloadStoragePaths, - ][_currentIndex] - .call(); - } catch (error, stackTrace) { - debugPrint( - "[LabelsPage] RefreshIndicator.onRefresh " - "${[ - "correspondents", - "document types", - "tags", - "storage paths" - ][_currentIndex]}: " - "An error occurred (${error.toString()})", - ); - debugPrintStack(stackTrace: stackTrace); - } - }, - child: TabBarView( - controller: _tabController, - children: [ - Builder( - builder: (context) { - return CustomScrollView( - slivers: [ - SliverOverlapInjector( - handle: searchBarHandle), - SliverOverlapInjector(handle: tabBarHandle), - LabelTabView( - labels: state.correspondents, - filterBuilder: (label) => DocumentFilter( - correspondent: - IdQueryParameter.fromId(label.id!), + return BlocBuilder( + builder: (context, connectedState) { + return SafeArea( + child: Scaffold( + drawer: const AppDrawer(), + floatingActionButton: FloatingActionButton( + onPressed: [ + if (user.canViewCorrespondents) _openAddCorrespondentPage, + if (user.canViewDocumentTypes) _openAddDocumentTypePage, + if (user.canViewTags) _openAddTagPage, + if (user.canViewStoragePaths) _openAddStoragePathPage, + ][_currentIndex], + child: const Icon(Icons.add), + ), + body: NestedScrollView( + floatHeaderSlivers: true, + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SliverOverlapAbsorber( + handle: searchBarHandle, + sliver: SliverSearchBar( + titleText: S.of(context)!.labels, + ), + ), + SliverOverlapAbsorber( + handle: tabBarHandle, + sliver: SliverPersistentHeader( + pinned: true, + delegate: CustomizableSliverPersistentHeaderDelegate( + child: ColoredTabBar( + tabBar: TabBar( + controller: _tabController, + tabs: [ + if (user.canViewCorrespondents) + Tab( + icon: Tooltip( + message: S.of(context)!.correspondents, + child: Icon( + Icons.person_outline, + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), ), - canEdit: LocalUserAccount - .current.paperlessUser - .hasPermission( - PermissionAction.change, - PermissionTarget.correspondent), - canAddNew: LocalUserAccount - .current.paperlessUser - .hasPermission(PermissionAction.add, - PermissionTarget.correspondent), - onEdit: _openEditCorrespondentPage, - emptyStateActionButtonLabel: - S.of(context)!.addNewCorrespondent, - emptyStateDescription: - S.of(context)!.noCorrespondentsSetUp, - onAddNew: _openAddCorrespondentPage, ), - ], - ); - }, - ), - Builder( - builder: (context) { - return CustomScrollView( - slivers: [ - SliverOverlapInjector( - handle: searchBarHandle), - SliverOverlapInjector(handle: tabBarHandle), - LabelTabView( - labels: state.documentTypes, - filterBuilder: (label) => DocumentFilter( - documentType: - IdQueryParameter.fromId(label.id!), + if (user.canViewDocumentTypes) + Tab( + icon: Tooltip( + message: S.of(context)!.documentTypes, + child: Icon( + Icons.description_outlined, + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), ), - canEdit: LocalUserAccount - .current.paperlessUser - .hasPermission( - PermissionAction.change, - PermissionTarget.documentType), - canAddNew: LocalUserAccount - .current.paperlessUser - .hasPermission(PermissionAction.add, - PermissionTarget.documentType), - onEdit: _openEditDocumentTypePage, - emptyStateActionButtonLabel: - S.of(context)!.addNewDocumentType, - emptyStateDescription: - S.of(context)!.noDocumentTypesSetUp, - onAddNew: _openAddDocumentTypePage, ), - ], - ); - }, - ), - Builder( - builder: (context) { - return CustomScrollView( - slivers: [ - SliverOverlapInjector( - handle: searchBarHandle), - SliverOverlapInjector(handle: tabBarHandle), - LabelTabView( - labels: state.tags, - filterBuilder: (label) => DocumentFilter( - tags: - TagsQuery.ids(include: [label.id!]), + if (user.canViewTags) + Tab( + icon: Tooltip( + message: S.of(context)!.tags, + child: Icon( + Icons.label_outline, + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), ), - canEdit: LocalUserAccount - .current.paperlessUser - .hasPermission( - PermissionAction.change, - PermissionTarget.tag), - canAddNew: LocalUserAccount - .current.paperlessUser - .hasPermission(PermissionAction.add, - PermissionTarget.tag), - onEdit: _openEditTagPage, - leadingBuilder: (t) => CircleAvatar( - backgroundColor: t.color, - child: t.isInboxTag - ? Icon( - Icons.inbox, - color: t.textColor, - ) - : null, - ), - emptyStateActionButtonLabel: - S.of(context)!.addNewTag, - emptyStateDescription: - S.of(context)!.noTagsSetUp, - onAddNew: _openAddTagPage, ), - ], - ); - }, - ), - Builder( - builder: (context) { - return CustomScrollView( - slivers: [ - SliverOverlapInjector( - handle: searchBarHandle), - SliverOverlapInjector(handle: tabBarHandle), - LabelTabView( - labels: state.storagePaths, - onEdit: _openEditStoragePathPage, - filterBuilder: (label) => DocumentFilter( - storagePath: - IdQueryParameter.fromId(label.id!), + if (user.canViewStoragePaths) + Tab( + icon: Tooltip( + message: S.of(context)!.storagePaths, + child: Icon( + Icons.folder_open, + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), ), - canEdit: LocalUserAccount - .current.paperlessUser - .hasPermission( - PermissionAction.change, - PermissionTarget.storagePath), - canAddNew: LocalUserAccount - .current.paperlessUser - .hasPermission(PermissionAction.add, - PermissionTarget.storagePath), - contentBuilder: (path) => Text(path.path), - emptyStateActionButtonLabel: - S.of(context)!.addNewStoragePath, - emptyStateDescription: - S.of(context)!.noStoragePathsSetUp, - onAddNew: _openAddStoragePathPage, ), - ], - ); - }, + ], + ), ), - ], + minExtent: kTextTabBarHeight, + maxExtent: kTextTabBarHeight, + ), ), ), - ); - }, + ], + body: BlocBuilder( + builder: (context, state) { + return NotificationListener( + onNotification: (notification) { + final metrics = notification.metrics; + if (metrics.maxScrollExtent == 0) { + return true; + } + final desiredTab = + ((metrics.pixels / metrics.maxScrollExtent) * + (_tabController.length - 1)) + .round(); + + if (metrics.axis == Axis.horizontal && + _currentIndex != desiredTab) { + setState(() => _currentIndex = desiredTab); + } + return true; + }, + child: RefreshIndicator( + edgeOffset: kTextTabBarHeight, + notificationPredicate: (notification) => + connectedState.isConnected, + onRefresh: () async { + try { + await [ + context + .read() + .reloadCorrespondents, + context + .read() + .reloadDocumentTypes, + context.read().reloadTags, + context.read().reloadStoragePaths, + ][_currentIndex] + .call(); + } catch (error, stackTrace) { + debugPrint( + "[LabelsPage] RefreshIndicator.onRefresh " + "${[ + "correspondents", + "document types", + "tags", + "storage paths" + ][_currentIndex]}: " + "An error occurred (${error.toString()})", + ); + debugPrintStack(stackTrace: stackTrace); + } + }, + child: TabBarView( + controller: _tabController, + children: [ + if (user.canViewCorrespondents) + Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: searchBarHandle), + SliverOverlapInjector( + handle: tabBarHandle), + LabelTabView( + labels: state.correspondents, + filterBuilder: (label) => + DocumentFilter( + correspondent: + IdQueryParameter.fromId( + label.id!), + ), + canEdit: user.canEditCorrespondents, + canAddNew: + user.canCreateCorrespondents, + onEdit: _openEditCorrespondentPage, + emptyStateActionButtonLabel: S + .of(context)! + .addNewCorrespondent, + emptyStateDescription: S + .of(context)! + .noCorrespondentsSetUp, + onAddNew: _openAddCorrespondentPage, + ), + ], + ); + }, + ), + if (user.canViewDocumentTypes) + Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: searchBarHandle), + SliverOverlapInjector( + handle: tabBarHandle), + LabelTabView( + labels: state.documentTypes, + filterBuilder: (label) => + DocumentFilter( + documentType: + IdQueryParameter.fromId( + label.id!), + ), + canEdit: user.canEditDocumentTypes, + canAddNew: + user.canCreateDocumentTypes, + onEdit: _openEditDocumentTypePage, + emptyStateActionButtonLabel: S + .of(context)! + .addNewDocumentType, + emptyStateDescription: S + .of(context)! + .noDocumentTypesSetUp, + onAddNew: _openAddDocumentTypePage, + ), + ], + ); + }, + ), + if (user.canViewTags) + Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: searchBarHandle), + SliverOverlapInjector( + handle: tabBarHandle), + LabelTabView( + labels: state.tags, + filterBuilder: (label) => + DocumentFilter( + tags: TagsQuery.ids( + include: [label.id!]), + ), + canEdit: user.canEditTags, + canAddNew: user.canCreateTags, + onEdit: _openEditTagPage, + leadingBuilder: (t) => CircleAvatar( + backgroundColor: t.color, + child: t.isInboxTag + ? Icon( + Icons.inbox, + color: t.textColor, + ) + : null, + ), + emptyStateActionButtonLabel: + S.of(context)!.addNewTag, + emptyStateDescription: + S.of(context)!.noTagsSetUp, + onAddNew: _openAddTagPage, + ), + ], + ); + }, + ), + if (user.canViewStoragePaths) + Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: searchBarHandle), + SliverOverlapInjector( + handle: tabBarHandle), + LabelTabView( + labels: state.storagePaths, + onEdit: _openEditStoragePathPage, + filterBuilder: (label) => + DocumentFilter( + storagePath: + IdQueryParameter.fromId( + label.id!), + ), + canEdit: user.canEditStoragePaths, + canAddNew: + user.canCreateStoragePaths, + contentBuilder: (path) => + Text(path.path), + emptyStateActionButtonLabel: S + .of(context)! + .addNewStoragePath, + emptyStateDescription: S + .of(context)! + .noStoragePathsSetUp, + onAddNew: _openAddStoragePathPage, + ), + ], + ); + }, + ), + ], + ), + ), + ); + }, + ), + ), ), - ), - ), + ); + }, ); - }, - ), - ); + }); } void _openEditCorrespondentPage(Correspondent correspondent) { diff --git a/lib/features/labels/view/widgets/label_item.dart b/lib/features/labels/view/widgets/label_item.dart index 52c950b7..de59604d 100644 --- a/lib/features/labels/view/widgets/label_item.dart +++ b/lib/features/labels/view/widgets/label_item.dart @@ -36,8 +36,7 @@ class LabelItem extends StatelessWidget { Widget _buildReferencedDocumentsWidget(BuildContext context) { final canOpen = (label.documentCount ?? 0) > 0 && - LocalUserAccount.current.paperlessUser - .hasPermission(PermissionAction.view, PermissionTarget.document); + LocalUserAccount.current.paperlessUser.canViewDocuments; return TextButton.icon( label: const Icon(Icons.link), icon: Text(formatMaxCount(label.documentCount)), diff --git a/lib/features/login/cubit/authentication_cubit.dart b/lib/features/login/cubit/authentication_cubit.dart index 63c3b19f..fc11adfe 100644 --- a/lib/features/login/cubit/authentication_cubit.dart +++ b/lib/features/login/cubit/authentication_cubit.dart @@ -365,7 +365,7 @@ class AuthenticationCubit extends Cubit { apiVersion: apiVersion, ) .findCurrentUser(); - } on DioError catch (error, stackTrace) { + } on DioException catch (error, stackTrace) { _debugPrintMessage( "_addUser", "An error occurred: ${error.message}", diff --git a/lib/features/login/view/login_page.dart b/lib/features/login/view/login_page.dart index cb8abe32..578585cb 100644 --- a/lib/features/login/view/login_page.dart +++ b/lib/features/login/view/login_page.dart @@ -105,7 +105,7 @@ class _LoginPageState extends State { ), ), ServerConnectionPage( - titleString: widget.titleString, + titleText: widget.titleString, formBuilderKey: _formKey, onContinue: () { _pageController.nextPage( @@ -126,7 +126,6 @@ class _LoginPageState extends State { } Future _login() async { - FocusScope.of(context).unfocus(); if (_formKey.currentState?.saveAndValidate() ?? false) { final form = _formKey.currentState!.value; @@ -150,7 +149,7 @@ class _LoginPageState extends State { form[ServerAddressFormField.fkServerAddress], clientCert, ); - } on PaperlessServerException catch (error) { + } on PaperlessApiException catch (error) { showErrorMessage(context, error); } on ServerMessageException catch (error) { showLocalizedError(context, error.message); 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 3b03c518..fe090063 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 @@ -66,7 +66,10 @@ class _ServerAddressFormFieldState extends State { .values .where((element) => element.contains(textEditingValue.text)); }, - onSelected: (option) => _formatInput(), + onSelected: (option) { + _formatInput(); + field.didChange(_textEditingController.text); + }, fieldViewBuilder: (context, textEditingController, focusNode, onFieldSubmitted) { return TextField( @@ -111,6 +114,10 @@ class _ServerAddressFormFieldState extends State { String address = _textEditingController.text.trim(); address = address.replaceAll(RegExp(r'^\/+|\/+$'), ''); _textEditingController.text = address; + _textEditingController.selection = TextSelection( + baseOffset: address.length, + extentOffset: address.length, + ); widget.onSubmit(address); } } diff --git a/lib/features/login/view/widgets/login_pages/server_connection_page.dart b/lib/features/login/view/widgets/login_pages/server_connection_page.dart index 5d72ccc0..29a3c960 100644 --- a/lib/features/login/view/widgets/login_pages/server_connection_page.dart +++ b/lib/features/login/view/widgets/login_pages/server_connection_page.dart @@ -13,14 +13,14 @@ import 'package:provider/provider.dart'; class ServerConnectionPage extends StatefulWidget { final GlobalKey formBuilderKey; - final void Function() onContinue; - final String titleString; + final VoidCallback onContinue; + final String titleText; const ServerConnectionPage({ super.key, required this.formBuilderKey, required this.onContinue, - required this.titleString, + required this.titleText, }); @override @@ -36,7 +36,7 @@ class _ServerConnectionPageState extends State { return Scaffold( appBar: AppBar( toolbarHeight: kToolbarHeight - 4, - title: Text(widget.titleString), + title: Text(widget.titleText), bottom: PreferredSize( child: _isCheckingConnection ? const LinearProgressIndicator() diff --git a/lib/features/paged_document_view/view/document_paging_view_mixin.dart b/lib/features/paged_document_view/view/document_paging_view_mixin.dart index c5f0e3e8..3811bdda 100644 --- a/lib/features/paged_document_view/view/document_paging_view_mixin.dart +++ b/lib/features/paged_document_view/view/document_paging_view_mixin.dart @@ -26,7 +26,7 @@ mixin DocumentPagingViewMixin super.initState(); try { context.read().initialize(); - } on PaperlessServerException catch (error, stackTrace) { + } on PaperlessApiException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } } diff --git a/lib/helpers/message_helpers.dart b/lib/helpers/message_helpers.dart index 09ddaeee..0ac415a7 100644 --- a/lib/helpers/message_helpers.dart +++ b/lib/helpers/message_helpers.dart @@ -92,7 +92,7 @@ void showLocalizedError( void showErrorMessage( BuildContext context, - PaperlessServerException error, [ + PaperlessApiException error, [ StackTrace? stackTrace, ]) { showSnackBar( diff --git a/lib/main.dart b/lib/main.dart index a9b131fd..d86dd734 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -30,7 +30,6 @@ import 'package:paperless_mobile/core/interceptor/language_header.interceptor.da import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/security/session_manager.dart'; import 'package:paperless_mobile/core/service/connectivity_status_service.dart'; -import 'package:paperless_mobile/core/type/types.dart'; import 'package:paperless_mobile/features/app_intro/application_intro_slideshow.dart'; import 'package:paperless_mobile/features/home/view/home_route.dart'; import 'package:paperless_mobile/features/home/view/widget/verify_identity_page.dart'; @@ -168,7 +167,7 @@ void main() async { ); }, (error, stack) { String message = switch (error) { - PaperlessServerException e => e.details ?? error.toString(), + PaperlessApiException e => e.details ?? error.toString(), ServerMessageException e => e.message, _ => error.toString() }; @@ -315,8 +314,10 @@ class _AuthenticationWrapperState extends State { ) async { try { await context.read().login( - credentials: - LoginFormCredentials(username: username, password: password), + credentials: LoginFormCredentials( + username: username, + password: password, + ), serverUrl: serverUrl, clientCertificate: clientCertificate, ); @@ -335,13 +336,17 @@ class _AuthenticationWrapperState extends State { globalSettings.save(); }); } - } on PaperlessServerException catch (error, stackTrace) { + } on PaperlessApiException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); - } on PaperlessValidationErrors catch (error, stackTrace) { - if (error.hasFieldUnspecificError) { - showLocalizedError(context, error.fieldUnspecificError!); + } on PaperlessFormValidationException catch (exception, stackTrace) { + if (exception.hasUnspecificErrorMessage()) { + showLocalizedError(context, exception.unspecificErrorMessage()!); } else { - showGenericError(context, error.values.first, stackTrace); + showGenericError( + context, + exception.validationMessages.values.first, + stackTrace, + ); //TODO: Check if we can show error message directly on field here. } } catch (unknownError, stackTrace) { showGenericError(context, unknownError.toString(), stackTrace); diff --git a/packages/paperless_api/lib/src/extensions/dio_exception_extension.dart b/packages/paperless_api/lib/src/extensions/dio_exception_extension.dart new file mode 100644 index 00000000..1f20f451 --- /dev/null +++ b/packages/paperless_api/lib/src/extensions/dio_exception_extension.dart @@ -0,0 +1,7 @@ +import 'package:dio/dio.dart'; + +extension DioExceptionUnravelExtension on DioException { + Object unravel({Object? orElse}) { + return error ?? orElse ?? Exception("Unknown"); + } +} diff --git a/packages/paperless_api/lib/src/models/exception/exceptions.dart b/packages/paperless_api/lib/src/models/exception/exceptions.dart new file mode 100644 index 00000000..51844a76 --- /dev/null +++ b/packages/paperless_api/lib/src/models/exception/exceptions.dart @@ -0,0 +1,3 @@ +export 'paperless_server_message_exception.dart'; +export 'paperless_form_validation_exception.dart'; +export 'paperless_unauthorized_exception.dart'; 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 new file mode 100644 index 00000000..55993087 --- /dev/null +++ b/packages/paperless_api/lib/src/models/exception/paperless_form_validation_exception.dart @@ -0,0 +1,42 @@ +class PaperlessFormValidationException implements Exception { + final Map validationMessages; + + PaperlessFormValidationException(this.validationMessages); + + bool hasMessageForField(String formKey) { + return validationMessages.containsKey(formKey); + } + + bool hasUnspecificErrorMessage() { + return validationMessages.containsKey("non_field_errors"); + } + + String? unspecificErrorMessage() { + return validationMessages["non_field_errors"]; + } + + String? messageForField(String formKey) { + return validationMessages[formKey]; + } + + static bool canParse(Map json) { + return json.values.every((element) => element is String); + } + + factory PaperlessFormValidationException.fromJson(Map json) { + final Map validationMessages = {}; + for (final entry in json.entries) { + if (entry.value is List) { + validationMessages.putIfAbsent( + entry.key, + () => (entry.value as List).first as String, + ); + } else if (entry.value is String) { + validationMessages.putIfAbsent(entry.key, () => entry.value); + } else { + validationMessages.putIfAbsent(entry.key, () => entry.value.toString()); + } + } + return PaperlessFormValidationException(validationMessages); + } +} 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 new file mode 100644 index 00000000..51cc0669 --- /dev/null +++ b/packages/paperless_api/lib/src/models/exception/paperless_server_message_exception.dart @@ -0,0 +1,17 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'paperless_server_exception.g.dart'; + +@JsonSerializable(createToJson: false) +class PaperlessServerMessageException implements Exception { + final String detail; + + PaperlessServerMessageException(this.detail); + + static bool canParse(Map json) { + return json.containsKey('detail') && json.length == 1; + } + + factory PaperlessServerMessageException.fromJson(Map json) => + _$PaperlessServerExceptionFromJson(json); +} diff --git a/packages/paperless_api/lib/src/models/exception/paperless_unauthorized_exception.dart b/packages/paperless_api/lib/src/models/exception/paperless_unauthorized_exception.dart new file mode 100644 index 00000000..5bb38737 --- /dev/null +++ b/packages/paperless_api/lib/src/models/exception/paperless_unauthorized_exception.dart @@ -0,0 +1,5 @@ +class PaperlessUnauthorizedException implements Exception { + final String? message; + + PaperlessUnauthorizedException(this.message); +} diff --git a/packages/paperless_api/lib/src/models/exception/parseable_exception.dart b/packages/paperless_api/lib/src/models/exception/parseable_exception.dart new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/packages/paperless_api/lib/src/models/exception/parseable_exception.dart @@ -0,0 +1 @@ + diff --git a/packages/paperless_api/lib/src/models/models.dart b/packages/paperless_api/lib/src/models/models.dart index a48ce1f2..d9ebaa85 100644 --- a/packages/paperless_api/lib/src/models/models.dart +++ b/packages/paperless_api/lib/src/models/models.dart @@ -12,7 +12,7 @@ export 'labels/matching_algorithm.dart'; export 'labels/storage_path_model.dart'; export 'labels/tag_model.dart'; export 'paged_search_result.dart'; -export 'paperless_server_exception.dart'; +export 'paperless_api_exception.dart'; export 'paperless_server_information_model.dart'; export 'paperless_server_statistics_model.dart'; export 'permissions/inherited_permissions.dart'; @@ -31,3 +31,4 @@ export 'saved_view_model.dart'; export 'task/task.dart'; export 'task/task_status.dart'; export 'user_model.dart'; +export 'exception/exceptions.dart'; diff --git a/packages/paperless_api/lib/src/models/paperless_server_exception.dart b/packages/paperless_api/lib/src/models/paperless_api_exception.dart similarity index 72% rename from packages/paperless_api/lib/src/models/paperless_server_exception.dart rename to packages/paperless_api/lib/src/models/paperless_api_exception.dart index 10ada4db..ac5fc02d 100644 --- a/packages/paperless_api/lib/src/models/paperless_server_exception.dart +++ b/packages/paperless_api/lib/src/models/paperless_api_exception.dart @@ -1,17 +1,17 @@ -class PaperlessServerException implements Exception { +class PaperlessApiException implements Exception { final ErrorCode code; final String? details; final StackTrace? stackTrace; final int? httpStatusCode; - const PaperlessServerException( + const PaperlessApiException( this.code, { this.details, this.stackTrace, this.httpStatusCode, }); - const PaperlessServerException.unknown() : this(ErrorCode.unknown); + const PaperlessApiException.unknown() : this(ErrorCode.unknown); @override String toString() { @@ -53,5 +53,6 @@ enum ErrorCode { requestTimedOut, unsupportedFileFormat, missingClientCertificate, - acknowledgeTasksError; + acknowledgeTasksError, + correspondentDeleteFailed, documentTypeDeleteFailed, tagDeleteFailed, correspondentUpdateFailed, documentTypeUpdateFailed, tagUpdateFailed, storagePathDeleteFailed, storagePathUpdateFailed, serverInformationLoadFailed, serverStatisticsLoadFailed, uiSettingsLoadFailed, loadTasksError, userNotFound; } diff --git a/packages/paperless_api/lib/src/models/permissions/user_permission_extension.dart b/packages/paperless_api/lib/src/models/permissions/user_permission_extension.dart index 7bf51856..87b49351 100644 --- a/packages/paperless_api/lib/src/models/permissions/user_permission_extension.dart +++ b/packages/paperless_api/lib/src/models/permissions/user_permission_extension.dart @@ -6,13 +6,15 @@ extension UserPermissionExtension on UserModel { v3: (user) { final permission = [action.value, target.value].join("_"); return user.userPermissions.any((element) => element == permission) || - user.inheritedPermissions.any((element) => element.split(".").last == permission); + user.inheritedPermissions + .any((element) => element.split(".").last == permission); }, v2: (_) => true, ); } - bool hasPermissions(List actions, List targets) { + bool hasPermissions( + List actions, List targets) { return map( v3: (user) { final permissions = [ @@ -21,10 +23,62 @@ extension UserPermissionExtension on UserModel { ]; return permissions.every((requestedPermission) => user.userPermissions.contains(requestedPermission) || - user.inheritedPermissions - .any((element) => element.split(".").last == requestedPermission)); + user.inheritedPermissions.any( + (element) => element.split(".").last == requestedPermission)); }, v2: (_) => true, ); } + + bool get canViewDocuments => + hasPermission(PermissionAction.view, PermissionTarget.document); + bool get canViewCorrespondents => + hasPermission(PermissionAction.view, PermissionTarget.correspondent); + bool get canViewDocumentTypes => + hasPermission(PermissionAction.view, PermissionTarget.documentType); + bool get canViewTags => + hasPermission(PermissionAction.view, PermissionTarget.tag); + bool get canViewStoragePaths => + hasPermission(PermissionAction.view, PermissionTarget.storagePath); + bool get canViewSavedViews => + hasPermission(PermissionAction.view, PermissionTarget.savedView); + + bool get canEditDocuments => + hasPermission(PermissionAction.change, PermissionTarget.document); + bool get canEditCorrespondents => + hasPermission(PermissionAction.change, PermissionTarget.correspondent); + bool get canEditDocumentTypes => + hasPermission(PermissionAction.change, PermissionTarget.documentType); + bool get canEditTags => + hasPermission(PermissionAction.change, PermissionTarget.tag); + bool get canEditStoragePaths => + hasPermission(PermissionAction.change, PermissionTarget.storagePath); + bool get canEditavedViews => + hasPermission(PermissionAction.change, PermissionTarget.savedView); + + bool get canDeleteDocuments => + hasPermission(PermissionAction.delete, PermissionTarget.document); + bool get canDeleteCorrespondents => + hasPermission(PermissionAction.delete, PermissionTarget.correspondent); + bool get canDeleteDocumentTypes => + hasPermission(PermissionAction.delete, PermissionTarget.documentType); + bool get canDeleteTags => + hasPermission(PermissionAction.delete, PermissionTarget.tag); + bool get canDeleteStoragePaths => + hasPermission(PermissionAction.delete, PermissionTarget.storagePath); + bool get canDeleteSavedViews => + hasPermission(PermissionAction.delete, PermissionTarget.savedView); + + bool get canCreateDocuments => + hasPermission(PermissionAction.add, PermissionTarget.document); + bool get canCreateCorrespondents => + hasPermission(PermissionAction.add, PermissionTarget.correspondent); + bool get canCreateDocumentTypes => + hasPermission(PermissionAction.add, PermissionTarget.documentType); + bool get canCreateTags => + hasPermission(PermissionAction.add, PermissionTarget.tag); + bool get canCreateStoragePaths => + hasPermission(PermissionAction.add, PermissionTarget.storagePath); + bool get canCreateSavedViews => + hasPermission(PermissionAction.add, PermissionTarget.savedView); } diff --git a/packages/paperless_api/lib/src/modules/authentication_api/authentication_api.dart b/packages/paperless_api/lib/src/modules/authentication_api/authentication_api.dart index f1d685ea..7735979a 100644 --- a/packages/paperless_api/lib/src/modules/authentication_api/authentication_api.dart +++ b/packages/paperless_api/lib/src/modules/authentication_api/authentication_api.dart @@ -1,4 +1,9 @@ +import 'package:paperless_api/src/models/exception/exceptions.dart'; + abstract class PaperlessAuthenticationApi { + /// + /// @throws [PaperlessUnauthorizedException] + /// Future login({ required String username, required String password, diff --git a/packages/paperless_api/lib/src/modules/authentication_api/authentication_api_impl.dart b/packages/paperless_api/lib/src/modules/authentication_api/authentication_api_impl.dart index 611b261d..fb7e9f20 100644 --- a/packages/paperless_api/lib/src/modules/authentication_api/authentication_api_impl.dart +++ b/packages/paperless_api/lib/src/modules/authentication_api/authentication_api_impl.dart @@ -1,6 +1,5 @@ import 'package:dio/dio.dart'; -import 'package:flutter/foundation.dart'; -import 'package:paperless_api/src/models/paperless_server_exception.dart'; +import 'package:paperless_api/src/extensions/dio_exception_extension.dart'; import 'package:paperless_api/src/modules/authentication_api/authentication_api.dart'; class PaperlessAuthenticationApiImpl implements PaperlessAuthenticationApi { @@ -13,34 +12,20 @@ class PaperlessAuthenticationApiImpl implements PaperlessAuthenticationApi { required String username, required String password, }) async { - late Response response; try { - response = await client.post( + final response = await client.post( "/api/token/", data: { "username": username, "password": password, }, + options: Options( + validateStatus: (status) => status == 200, + ), ); - } on DioError catch (error) { - if (error.error is PaperlessServerException || - error.error is Map) { - throw error.error as Map; - } else { - throw PaperlessServerException( - ErrorCode.authenticationFailed, - details: error.message, - ); - } - } - - if (response.statusCode == 200) { return response.data['token']; - } else { - throw PaperlessServerException( - ErrorCode.authenticationFailed, - httpStatusCode: response.statusCode, - ); + } on DioException catch (exception) { + throw exception.unravel(); } } } diff --git a/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api.dart b/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api.dart index 5e463e96..4227f832 100644 --- a/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api.dart +++ b/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api.dart @@ -18,7 +18,7 @@ abstract class PaperlessDocumentsApi { Future update(DocumentModel doc); Future findNextAsn(); Future> findAll(DocumentFilter filter); - Future find(int id); + Future find(int id); Future delete(DocumentModel doc); Future getMetaData(DocumentModel document); Future> bulkAction(BulkAction action); 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 eebe3142..016a2604 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 @@ -4,6 +4,8 @@ import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/src/constants.dart'; +import 'package:paperless_api/src/extensions/dio_exception_extension.dart'; +import 'package:paperless_api/src/models/paperless_api_exception.dart'; class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { final Dio client; @@ -55,20 +57,17 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { onSendProgress: (count, total) { debugPrint("Uploading ${(count / total) * 100}%..."); }, + options: Options(validateStatus: (status) => status == 200), ); - if (response.statusCode == 200) { - if (response.data is String && response.data != "OK") { - return response.data; - } - return null; + if (response.data != "OK") { + return response.data as String; } else { - throw PaperlessServerException( - ErrorCode.documentUploadFailed, - httpStatusCode: response.statusCode, - ); + return null; } - } on DioError catch (err) { - throw err.error ?? const PaperlessServerException.unknown(); + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException(ErrorCode.documentUploadFailed), + ); } } @@ -78,14 +77,13 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { final response = await client.put( "/api/documents/${doc.id}/", data: doc.toJson(), + options: Options(validateStatus: (status) => status == 200), + ); + return DocumentModel.fromJson(response.data); + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException(ErrorCode.documentUpdateFailed), ); - if (response.statusCode == 200) { - return DocumentModel.fromJson(response.data); - } else { - throw const PaperlessServerException(ErrorCode.documentUpdateFailed); - } - } on DioError catch (err) { - throw err.error ?? const PaperlessServerException.unknown(); } } @@ -93,39 +91,41 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { Future> findAll( DocumentFilter filter, ) async { - final filterParams = filter.toQueryParameters()..addAll({'truncate_content': "true"}); + final filterParams = filter.toQueryParameters() + ..addAll({'truncate_content': "true"}); try { final response = await client.get( "/api/documents/", queryParameters: filterParams, + options: Options(validateStatus: (status) => status == 200), + ); + return compute( + PagedSearchResult.fromJsonSingleParam, + PagedSearchResultJsonSerializer( + response.data, + DocumentModelJsonConverter(), + ), + ); + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException(ErrorCode.documentLoadFailed), ); - if (response.statusCode == 200) { - return compute( - PagedSearchResult.fromJsonSingleParam, - PagedSearchResultJsonSerializer( - response.data, - DocumentModelJsonConverter(), - ), - ); - } else { - throw const PaperlessServerException(ErrorCode.documentLoadFailed); - } - } on DioError catch (err) { - throw err.error ?? const PaperlessServerException.unknown(); } } @override Future delete(DocumentModel doc) async { try { - final response = await client.delete("/api/documents/${doc.id}/"); + await client.delete( + "/api/documents/${doc.id}/", + options: Options(validateStatus: (status) => status == 204), + ); - if (response.statusCode == 204) { - return Future.value(doc.id); - } - throw const PaperlessServerException(ErrorCode.documentDeleteFailed); - } on DioError catch (err) { - throw err.error ?? const PaperlessServerException.unknown(); + return Future.value(doc.id); + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException(ErrorCode.documentDeleteFailed), + ); } } @@ -143,15 +143,16 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { try { final response = await client.get( getPreviewUrl(documentId), - options: - Options(responseType: ResponseType.bytes), //TODO: Check if bytes or stream is required + options: Options( + responseType: ResponseType.bytes, + validateStatus: (status) => status == 200, + ), //TODO: Check if bytes or stream is required + ); + return response.data; + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException(ErrorCode.documentPreviewFailed), ); - if (response.statusCode == 200) { - return response.data; - } - throw const PaperlessServerException(ErrorCode.documentPreviewFailed); - } on DioError catch (err) { - throw err.error ?? const PaperlessServerException.unknown(); } } @@ -170,29 +171,30 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { .map((e) => e.archiveSerialNumber) .firstWhere((asn) => asn != null, orElse: () => 0)! + 1; - } on PaperlessServerException { - throw const PaperlessServerException(ErrorCode.documentAsnQueryFailed); - } on DioError catch (err) { - throw err.error ?? const PaperlessServerException.unknown(); + } on PaperlessApiException { + throw const PaperlessApiException(ErrorCode.documentAsnQueryFailed); + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException(ErrorCode.documentAsnQueryFailed), + ); } } @override Future> bulkAction(BulkAction action) async { try { - final response = await client.post( + await client.post( "/api/documents/bulk_edit/", data: action.toJson(), + options: Options(validateStatus: (status) => status == 200), ); - if (response.statusCode == 200) { - return action.documentIds; - } else { - throw const PaperlessServerException( + return action.documentIds; + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException( ErrorCode.documentBulkActionFailed, - ); - } - } on DioError catch (err) { - throw err.error ?? const PaperlessServerException.unknown(); + ), + ); } } @@ -208,8 +210,10 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { options: Options(responseType: ResponseType.bytes), ); return response.data; - } on DioError catch (err) { - throw err.error ?? const PaperlessServerException.unknown(); + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException.unknown(), + ); } } @@ -224,25 +228,31 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { final response = await client.download( "/api/documents/${document.id}/download/", localFilePath, - onReceiveProgress: (count, total) => onProgressChanged?.call(count / total), + onReceiveProgress: (count, total) => + onProgressChanged?.call(count / total), queryParameters: {'original': original}, ); return response.data; - } on DioError catch (err) { - throw err.error ?? const PaperlessServerException.unknown(); + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException.unknown(), + ); } } @override Future getMetaData(DocumentModel document) async { try { - final response = await client.get("/api/documents/${document.id}/metadata/"); + final response = + await client.get("/api/documents/${document.id}/metadata/"); return compute( DocumentMetaData.fromJson, response.data as Map, ); - } on DioError catch (err) { - throw err.error ?? const PaperlessServerException.unknown(); + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException.unknown(), + ); } } @@ -255,40 +265,46 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { 'term': query, 'limit': limit, }, + options: Options(validateStatus: (status) => status == 200), + ); + return (response.data as List).cast(); + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException( + ErrorCode.autocompleteQueryError, + ), ); - if (response.statusCode == 200) { - return (response.data as List).cast(); - } - throw const PaperlessServerException(ErrorCode.autocompleteQueryError); - } on DioError catch (err) { - throw err.error ?? const PaperlessServerException.unknown(); } } @override Future findSuggestions(DocumentModel document) async { try { - final response = await client.get("/api/documents/${document.id}/suggestions/"); - if (response.statusCode == 200) { - return FieldSuggestions.fromJson(response.data).forDocumentId(document.id); - } - throw const PaperlessServerException(ErrorCode.suggestionsQueryError); - } on DioError catch (err) { - throw err.error ?? const PaperlessServerException.unknown(); + final response = await client.get( + "/api/documents/${document.id}/suggestions/", + options: Options(validateStatus: (status) => status == 200), + ); + return FieldSuggestions.fromJson(response.data) + .forDocumentId(document.id); + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException(ErrorCode.suggestionsQueryError), + ); } } @override - Future find(int id) async { + Future find(int id) async { try { - final response = await client.get("/api/documents/$id/"); - if (response.statusCode == 200) { - return DocumentModel.fromJson(response.data); - } else { - return null; - } - } on DioError catch (err) { - throw err.error ?? const PaperlessServerException.unknown(); + final response = await client.get( + "/api/documents/$id/", + options: Options(validateStatus: (status) => status == 200), + ); + return DocumentModel.fromJson(response.data); + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException.unknown(), + ); } } } 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 5af3579e..e45dbf8c 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 @@ -2,11 +2,12 @@ import 'dart:convert'; import 'dart:io'; import 'package:dio/dio.dart'; +import 'package:paperless_api/src/extensions/dio_exception_extension.dart'; import 'package:paperless_api/src/models/labels/correspondent_model.dart'; import 'package:paperless_api/src/models/labels/document_type_model.dart'; import 'package:paperless_api/src/models/labels/storage_path_model.dart'; import 'package:paperless_api/src/models/labels/tag_model.dart'; -import 'package:paperless_api/src/models/paperless_server_exception.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'; @@ -94,16 +95,15 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { final response = await _client.post( '/api/correspondents/', data: correspondent.toJson(), + options: Options(validateStatus: (status) => status == 201), ); - if (response.statusCode == HttpStatus.created) { - return Correspondent.fromJson(response.data); - } - throw PaperlessServerException( - ErrorCode.correspondentCreateFailed, - httpStatusCode: response.statusCode, + return Correspondent.fromJson(response.data); + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException( + ErrorCode.correspondentCreateFailed, + ), ); - } on DioError catch (err) { - throw err.error!; } } @@ -113,16 +113,17 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { final response = await _client.post( '/api/document_types/', data: type.toJson(), + options: Options( + validateStatus: (status) => status == 201, + ), ); - if (response.statusCode == HttpStatus.created) { - return DocumentType.fromJson(response.data); - } - throw PaperlessServerException( - ErrorCode.documentTypeCreateFailed, - httpStatusCode: response.statusCode, + return DocumentType.fromJson(response.data); + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException( + ErrorCode.documentTypeCreateFailed, + ), ); - } on DioError catch (err) { - throw err.error!; } } @@ -132,17 +133,18 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { final response = await _client.post( '/api/tags/', data: tag.toJson(), - options: Options(headers: {"Accept": "application/json; version=2"}), + options: Options( + headers: {"Accept": "application/json; version=2"}, + validateStatus: (status) => status == 201, + ), ); - if (response.statusCode == HttpStatus.created) { - return Tag.fromJson(response.data); - } - throw PaperlessServerException( - ErrorCode.tagCreateFailed, - httpStatusCode: response.statusCode, + return Tag.fromJson(response.data); + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException( + ErrorCode.tagCreateFailed, + ), ); - } on DioError catch (err) { - throw err.error!; } } @@ -150,17 +152,17 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { Future deleteCorrespondent(Correspondent correspondent) async { assert(correspondent.id != null); try { - final response = - await _client.delete('/api/correspondents/${correspondent.id}/'); - if (response.statusCode == HttpStatus.noContent) { - return correspondent.id!; - } - throw PaperlessServerException( - ErrorCode.unknown, - httpStatusCode: response.statusCode, + await _client.delete( + '/api/correspondents/${correspondent.id}/', + options: Options(validateStatus: (status) => status == 204), + ); + return correspondent.id!; + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException( + ErrorCode.correspondentDeleteFailed, + ), ); - } on DioError catch (err) { - throw err.error!; } } @@ -168,17 +170,17 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { Future deleteDocumentType(DocumentType documentType) async { assert(documentType.id != null); try { - final response = - await _client.delete('/api/document_types/${documentType.id}/'); - if (response.statusCode == HttpStatus.noContent) { - return documentType.id!; - } - throw PaperlessServerException( - ErrorCode.unknown, - httpStatusCode: response.statusCode, + final response = await _client.delete( + '/api/document_types/${documentType.id}/', + options: Options(validateStatus: (status) => status == 204), + ); + return documentType.id!; + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException( + ErrorCode.documentTypeDeleteFailed, + ), ); - } on DioError catch (err) { - throw err.error!; } } @@ -186,16 +188,17 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { Future deleteTag(Tag tag) async { assert(tag.id != null); try { - final response = await _client.delete('/api/tags/${tag.id}/'); - if (response.statusCode == HttpStatus.noContent) { - return tag.id!; - } - throw PaperlessServerException( - ErrorCode.unknown, - httpStatusCode: response.statusCode, + await _client.delete( + '/api/tags/${tag.id}/', + options: Options(validateStatus: (status) => status == 204), + ); + return tag.id!; + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException( + ErrorCode.tagDeleteFailed, + ), ); - } on DioError catch (err) { - throw err.error!; } } @@ -206,16 +209,15 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { final response = await _client.put( '/api/correspondents/${correspondent.id}/', data: json.encode(correspondent.toJson()), + options: Options(validateStatus: (status) => status == 200), ); - if (response.statusCode == HttpStatus.ok) { - return Correspondent.fromJson(response.data); - } - throw PaperlessServerException( - ErrorCode.unknown, //TODO: Add correct error code mapping. - httpStatusCode: response.statusCode, + return Correspondent.fromJson(response.data); + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException( + ErrorCode.correspondentUpdateFailed, + ), ); - } on DioError catch (err) { - throw err.error!; } } @@ -226,16 +228,15 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { final response = await _client.put( '/api/document_types/${documentType.id}/', data: documentType.toJson(), + options: Options(validateStatus: (status) => status == 200), ); - if (response.statusCode == HttpStatus.ok) { - return DocumentType.fromJson(response.data); - } - throw PaperlessServerException( - ErrorCode.unknown, - httpStatusCode: response.statusCode, + return DocumentType.fromJson(response.data); + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException( + ErrorCode.documentTypeUpdateFailed, + ), ); - } on DioError catch (err) { - throw err.error!; } } @@ -245,18 +246,19 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { try { final response = await _client.put( '/api/tags/${tag.id}/', - options: Options(headers: {"Accept": "application/json; version=2"}), + options: Options( + headers: {"Accept": "application/json; version=2"}, + validateStatus: (status) => status == 200, + ), data: tag.toJson(), ); - if (response.statusCode == HttpStatus.ok) { - return Tag.fromJson(response.data); - } - throw PaperlessServerException( - ErrorCode.unknown, - httpStatusCode: response.statusCode, + return Tag.fromJson(response.data); + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException( + ErrorCode.tagUpdateFailed, + ), ); - } on DioError catch (err) { - throw err.error!; } } @@ -264,16 +266,17 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { Future deleteStoragePath(StoragePath path) async { assert(path.id != null); try { - final response = await _client.delete('/api/storage_paths/${path.id}/'); - if (response.statusCode == HttpStatus.noContent) { - return path.id!; - } - throw PaperlessServerException( - ErrorCode.unknown, - httpStatusCode: response.statusCode, + final response = await _client.delete( + '/api/storage_paths/${path.id}/', + options: Options(validateStatus: (status) => status == 204), + ); + return path.id!; + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException( + ErrorCode.storagePathDeleteFailed, + ), ); - } on DioError catch (err) { - throw err.error!; } } @@ -307,16 +310,15 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { final response = await _client.post( '/api/storage_paths/', data: path.toJson(), + options: Options(validateStatus: (status) => status == 201), ); - if (response.statusCode == HttpStatus.created) { - return StoragePath.fromJson(response.data); - } - throw PaperlessServerException( - ErrorCode.storagePathCreateFailed, - httpStatusCode: response.statusCode, + return StoragePath.fromJson(response.data); + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException( + ErrorCode.storagePathCreateFailed, + ), ); - } on DioError catch (err) { - throw err.error!; } } @@ -327,13 +329,15 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { final response = await _client.put( '/api/storage_paths/${path.id}/', data: path.toJson(), + options: Options(validateStatus: (status) => status == 200), + ); + return StoragePath.fromJson(response.data); + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException( + ErrorCode.storagePathUpdateFailed, + ), ); - if (response.statusCode == HttpStatus.ok) { - return StoragePath.fromJson(response.data); - } - throw const PaperlessServerException(ErrorCode.unknown); - } on DioError catch (err) { - throw err.error!; } } } diff --git a/packages/paperless_api/lib/src/modules/saved_views_api/paperless_saved_views_api_impl.dart b/packages/paperless_api/lib/src/modules/saved_views_api/paperless_saved_views_api_impl.dart index 4ad183c2..976ba1c7 100644 --- a/packages/paperless_api/lib/src/modules/saved_views_api/paperless_saved_views_api_impl.dart +++ b/packages/paperless_api/lib/src/modules/saved_views_api/paperless_saved_views_api_impl.dart @@ -1,7 +1,8 @@ import 'dart:io'; import 'package:dio/dio.dart'; -import 'package:paperless_api/src/models/paperless_server_exception.dart'; +import 'package:paperless_api/src/extensions/dio_exception_extension.dart'; +import 'package:paperless_api/src/models/paperless_api_exception.dart'; import 'package:paperless_api/src/models/saved_view_model.dart'; import 'package:paperless_api/src/request_utils.dart'; @@ -30,32 +31,28 @@ class PaperlessSavedViewsApiImpl implements PaperlessSavedViewsApi { final response = await _client.post( "/api/saved_views/", data: view.toJson(), + options: Options(validateStatus: (status) => status == 201), ); - if (response.statusCode == HttpStatus.created) { - return SavedView.fromJson(response.data); - } - throw PaperlessServerException( - ErrorCode.createSavedViewError, - httpStatusCode: response.statusCode, + return SavedView.fromJson(response.data); + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException(ErrorCode.createSavedViewError), ); - } on DioError catch (err) { - throw err.error!; } } @override Future delete(SavedView view) async { try { - final response = await _client.delete("/api/saved_views/${view.id}/"); - if (response.statusCode == HttpStatus.noContent) { - return view.id!; - } - throw PaperlessServerException( - ErrorCode.deleteSavedViewError, - httpStatusCode: response.statusCode, + await _client.delete( + "/api/saved_views/${view.id}/", + options: Options(validateStatus: (status) => status == 204), + ); + return view.id!; + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException(ErrorCode.deleteSavedViewError), ); - } on DioError catch (err) { - throw err.error!; } } 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 e9c4ff58..9294938d 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 @@ -1,5 +1,6 @@ import 'package:dio/dio.dart'; -import 'package:paperless_api/src/models/paperless_server_exception.dart'; +import 'package:paperless_api/src/extensions/dio_exception_extension.dart'; +import 'package:paperless_api/src/models/paperless_api_exception.dart'; import 'package:paperless_api/src/models/paperless_server_information_model.dart'; import 'package:paperless_api/src/models/paperless_server_statistics_model.dart'; import 'package:paperless_api/src/models/paperless_ui_settings_model.dart'; @@ -18,8 +19,11 @@ class PaperlessServerStatsApiImpl implements PaperlessServerStatsApi { @override Future getServerInformation() async { - final response = await client.get("/api/remote_version/"); - if (response.statusCode == 200) { + try { + final response = await client.get( + "/api/remote_version/", + options: Options(validateStatus: (status) => status == 200), + ); final version = response.data["version"] as String; final updateAvailable = response.data["update_available"] as bool; return PaperlessServerInformationModel( @@ -27,25 +31,44 @@ class PaperlessServerStatsApiImpl implements PaperlessServerStatsApi { version: version, isUpdateAvailable: updateAvailable, ); + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException( + ErrorCode.serverInformationLoadFailed, + ), + ); } - throw const PaperlessServerException.unknown(); } @override Future getServerStatistics() async { - final response = await client.get('/api/statistics/'); - if (response.statusCode == 200) { + try { + final response = await client.get( + '/api/statistics/', + options: Options(validateStatus: (status) => status == 200), + ); return PaperlessServerStatisticsModel.fromJson(response.data); + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException( + ErrorCode.serverStatisticsLoadFailed, + ), + ); } - throw const PaperlessServerException.unknown(); } @override Future getUiSettings() async { - final response = await client.get("/api/ui_settings/"); - if (response.statusCode == 200) { + try { + final response = await client.get( + "/api/ui_settings/", + options: Options(validateStatus: (status) => status == 200), + ); return PaperlessUiSettingsModel.fromJson(response.data); + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException(ErrorCode.uiSettingsLoadFailed), + ); } - throw const PaperlessServerException.unknown(); } } diff --git a/packages/paperless_api/lib/src/modules/tasks_api/paperless_tasks_api_impl.dart b/packages/paperless_api/lib/src/modules/tasks_api/paperless_tasks_api_impl.dart index c40be651..106c54b6 100644 --- a/packages/paperless_api/lib/src/modules/tasks_api/paperless_tasks_api_impl.dart +++ b/packages/paperless_api/lib/src/modules/tasks_api/paperless_tasks_api_impl.dart @@ -2,6 +2,8 @@ import 'dart:developer'; 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/paperless_api_exception.dart'; class PaperlessTasksApiImpl implements PaperlessTasksApi { final Dio _client; @@ -41,11 +43,17 @@ class PaperlessTasksApiImpl implements PaperlessTasksApi { @override Future> findAll([Iterable? ids]) async { - final response = await _client.get("/api/tasks/"); - if (response.statusCode == 200) { + try { + final response = await _client.get( + "/api/tasks/", + options: Options(validateStatus: (status) => status == 200), + ); return (response.data as List).map((e) => Task.fromJson(e)); + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException(ErrorCode.loadTasksError), + ); } - return []; } @override @@ -74,15 +82,22 @@ class PaperlessTasksApiImpl implements PaperlessTasksApi { @override Future> acknowledgeTasks(Iterable tasks) async { - final response = await _client.post("/api/acknowledge_tasks/", data: { - 'tasks': tasks.map((e) => e.id).toList(), - }); - if (response.statusCode == 200) { + try { + final response = await _client.post( + "/api/acknowledge_tasks/", + data: { + 'tasks': tasks.map((e) => e.id).toList(), + }, + options: Options(validateStatus: (status) => status == 200), + ); if (response.data['result'] != tasks.length) { - throw const PaperlessServerException(ErrorCode.acknowledgeTasksError); + throw const PaperlessApiException(ErrorCode.acknowledgeTasksError); } return tasks.map((e) => e.copyWith(acknowledged: true)).toList(); + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException(ErrorCode.acknowledgeTasksError), + ); } - throw const PaperlessServerException(ErrorCode.acknowledgeTasksError); } } diff --git a/packages/paperless_api/lib/src/modules/user_api/paperless_user_api_v2_impl.dart b/packages/paperless_api/lib/src/modules/user_api/paperless_user_api_v2_impl.dart index 007a1c92..34d62fcd 100644 --- a/packages/paperless_api/lib/src/modules/user_api/paperless_user_api_v2_impl.dart +++ b/packages/paperless_api/lib/src/modules/user_api/paperless_user_api_v2_impl.dart @@ -1,5 +1,7 @@ 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/paperless_api_exception.dart'; class PaperlessUserApiV2Impl implements PaperlessUserApi { final Dio client; @@ -8,19 +10,33 @@ class PaperlessUserApiV2Impl implements PaperlessUserApi { @override Future findCurrentUserId() async { - final response = await client.get("/api/ui_settings/"); - if (response.statusCode == 200) { + try { + final response = await client.get( + "/api/ui_settings/", + options: Options( + validateStatus: (status) => status == 200, + ), + ); return response.data['user_id']; + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException(ErrorCode.userNotFound), + ); } - throw const PaperlessServerException.unknown(); } @override Future findCurrentUser() async { - final response = await client.get("/api/ui_settings/"); - if (response.statusCode == 200) { + try { + final response = await client.get( + "/api/ui_settings/", + options: Options(validateStatus: (status) => status == 200), + ); return UserModelV2.fromJson(response.data); + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException(ErrorCode.userNotFound), + ); } - throw const PaperlessServerException.unknown(); } } diff --git a/packages/paperless_api/lib/src/modules/user_api/paperless_user_api_v3_impl.dart b/packages/paperless_api/lib/src/modules/user_api/paperless_user_api_v3_impl.dart index e0d22469..892d3be2 100644 --- a/packages/paperless_api/lib/src/modules/user_api/paperless_user_api_v3_impl.dart +++ b/packages/paperless_api/lib/src/modules/user_api/paperless_user_api_v3_impl.dart @@ -1,5 +1,7 @@ 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/paperless_api_exception.dart'; class PaperlessUserApiV3Impl implements PaperlessUserApi, PaperlessUserApiV3 { final Dio dio; @@ -8,11 +10,17 @@ class PaperlessUserApiV3Impl implements PaperlessUserApi, PaperlessUserApiV3 { @override Future find(int id) async { - final response = await dio.get("/api/users/$id/"); - if (response.statusCode == 200) { + try { + final response = await dio.get( + "/api/users/$id/", + options: Options(validateStatus: (status) => status == 200), + ); return UserModelV3.fromJson(response.data); + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException(ErrorCode.userNotFound), + ); } - throw const PaperlessServerException.unknown(); } @override @@ -22,40 +30,59 @@ class PaperlessUserApiV3Impl implements PaperlessUserApi, PaperlessUserApiV3 { String contains = '', String username = '', }) async { - final response = await dio.get("/api/users/", queryParameters: { - "username__istartswith": startsWith, - "username__iendswith": endsWith, - "username__icontains": contains, - "username__iexact": username, - }); - if (response.statusCode == 200) { + try { + final response = await dio.get( + "/api/users/", + queryParameters: { + "username__istartswith": startsWith, + "username__iendswith": endsWith, + "username__icontains": contains, + "username__iexact": username, + }, + options: Options(validateStatus: (status) => status == 200), + ); return PagedSearchResult.fromJson( response.data, UserModelV3.fromJson as UserModelV3 Function(Object?), ).results; + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException(ErrorCode.userNotFound), + ); } - throw const PaperlessServerException.unknown(); } @override Future findCurrentUserId() async { - final response = await dio.get("/api/ui_settings/"); - if (response.statusCode == 200) { + try { + final response = await dio.get( + "/api/ui_settings/", + options: Options(validateStatus: (status) => status == 200), + ); return response.data['user']['id']; + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException(ErrorCode.userNotFound), + ); } - throw const PaperlessServerException.unknown(); } @override Future> findAll() async { - final response = await dio.get("/api/users/"); - if (response.statusCode == 200) { + try { + final response = await dio.get( + "/api/users/", + options: Options(validateStatus: (status) => status == 200), + ); return PagedSearchResult.fromJson( response.data, (json) => UserModelV3.fromJson(json as dynamic), ).results; + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException(ErrorCode.userNotFound), + ); } - throw const PaperlessServerException.unknown(); } @override diff --git a/packages/paperless_api/lib/src/request_utils.dart b/packages/paperless_api/lib/src/request_utils.dart index 6c0fcfa5..a0c87c17 100644 --- a/packages/paperless_api/lib/src/request_utils.dart +++ b/packages/paperless_api/lib/src/request_utils.dart @@ -2,7 +2,8 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; -import 'package:paperless_api/src/models/paperless_server_exception.dart'; +import 'package:paperless_api/src/extensions/dio_exception_extension.dart'; +import 'package:paperless_api/src/models/paperless_api_exception.dart'; Future getSingleResult( String url, @@ -16,20 +17,15 @@ Future getSingleResult( url, options: Options( headers: {'accept': 'application/json; version=$minRequiredApiVersion'}, + validateStatus: (status) => status == 200, ), ); - if (response.statusCode == HttpStatus.ok) { - return compute( - fromJson, - response.data as Map, - ); - } - throw PaperlessServerException( - errorCode, - httpStatusCode: response.statusCode, + return compute( + fromJson, + response.data as Map, ); - } on DioError catch (err) { - throw err.error!; + } on DioException catch (exception) { + throw exception.unravel(orElse: PaperlessApiException(errorCode)); } } @@ -43,30 +39,25 @@ Future> getCollection( try { final response = await client.get( url, - options: Options(headers: { - 'accept': 'application/json; version=$minRequiredApiVersion' - }), + options: Options( + headers: {'accept': 'application/json; version=$minRequiredApiVersion'}, + validateStatus: (status) => status == 200, + ), ); - if (response.statusCode == HttpStatus.ok) { - final Map body = response.data; - if (body.containsKey('count')) { - if (body['count'] == 0) { - return []; - } else { - return compute( - _collectionFromJson, - _CollectionFromJsonSerializationParams(fromJson, - (body['results'] as List).cast>()), - ); - } - } + final Map body = response.data; + if (body['count'] == 0) { + return []; + } else { + return compute( + _collectionFromJson, + _CollectionFromJsonSerializationParams( + fromJson, + (body['results'] as List).cast>(), + ), + ); } - throw PaperlessServerException( - errorCode, - httpStatusCode: response.statusCode, - ); - } on DioError catch (err) { - throw err.error!; + } on DioException catch (exception) { + throw exception.unravel(orElse: PaperlessApiException(errorCode)); } } diff --git a/packages/paperless_document_scanner/example/pubspec.lock b/packages/paperless_document_scanner/example/pubspec.lock index 6b2cc090..a5c17787 100644 --- a/packages/paperless_document_scanner/example/pubspec.lock +++ b/packages/paperless_document_scanner/example/pubspec.lock @@ -29,42 +29,42 @@ packages: dependency: "direct main" description: name: camera - sha256: "309b823e61f15ff6b5b2e4c0ff2e1512ea661cad5355f71fc581e510ae5b26bb" + sha256: ebebead3d5ec3d148249331d751d462d7e8c98102b8830a9b45ec96a2bd4333f url: "https://pub.dev" source: hosted - version: "0.10.5" + version: "0.10.5+2" camera_android: dependency: transitive description: name: camera_android - sha256: e0f9b7eea2d1f4d4f5460f178522f0d02c095d2ae00b01a77419ce61c4184bfe + sha256: f43d07f9d7228ea1ca87d22e30881bd68da4b78484a1fbd1f1408b412a41cefb url: "https://pub.dev" source: hosted - version: "0.10.7" + version: "0.10.8+3" camera_avfoundation: dependency: transitive description: name: camera_avfoundation - sha256: "7ac8b950672716722af235eed7a7c37896853669800b7da706bb0a9fd41d3737" + sha256: "1a416e452b30955b392f4efbf23291d3f2ba3660a85e1628859eb62d2a2bab26" url: "https://pub.dev" source: hosted - version: "0.9.13+1" + version: "0.9.13+2" camera_platform_interface: dependency: transitive description: name: camera_platform_interface - sha256: "525017018d116c5db8c4c43ec2d9b1663216b369c9f75149158280168a7ce472" + sha256: "60fa0bb62a4f3bf3a7c413e31e4cd01b69c779ccc8e4668904a24581b86c316b" url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.5.1" camera_web: dependency: transitive description: name: camera_web - sha256: d77965f32479ee6d8f48205dcf10f845d7210595c6c00faa51eab265d1cae993 + sha256: bcbd775fb3a9d51cc3ece899d54ad66f6306410556bac5759f78e13f9228841f url: "https://pub.dev" source: hosted - version: "0.3.1+3" + version: "0.3.1+4" camerawesome: dependency: transitive description: @@ -178,18 +178,18 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + sha256: "2118df84ef0c3ca93f96123a616ae8540879991b8b57af2f81b76a7ada49b2a4" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "96af49aa6b57c10a312106ad6f71deed5a754029c24789bbf620ba784f0bd0b0" + sha256: "950e77c2bbe1692bc0874fc7fb491b96a4dc340457f4ea1641443d0a6c1ea360" url: "https://pub.dev" source: hosted - version: "2.0.14" + version: "2.0.15" flutter_test: dependency: "direct dev" description: flutter @@ -220,10 +220,10 @@ packages: dependency: transitive description: name: lints - sha256: "6b0206b0bf4f04961fc5438198ccb3a885685cd67d4d4a32cc20ad7f8adbe015" + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" matcher: dependency: transitive description: @@ -299,10 +299,10 @@ packages: dependency: transitive description: name: path_provider_linux - sha256: "2ae08f2216225427e64ad224a24354221c2c7907e448e6e0e8b57b1eb9f10ad1" + sha256: ffbb8cc9ed2c9ec0e4b7a541e56fd79b138e8f47d2fb86815f15358a349b3b57 url: "https://pub.dev" source: hosted - version: "2.1.10" + version: "2.1.11" path_provider_platform_interface: dependency: transitive description: @@ -315,10 +315,10 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: d3f80b32e83ec208ac95253e0cd4d298e104fbc63cb29c5c69edaed43b0c69d6 + sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96" url: "https://pub.dev" source: hosted - version: "2.1.6" + version: "2.1.7" petitparser: dependency: transitive description: @@ -456,10 +456,10 @@ packages: dependency: transitive description: name: win32 - sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c" + sha256: dfdf0136e0aa7a1b474ea133e67cb0154a0acd2599c4f3ada3b49d38d38793ee url: "https://pub.dev" source: hosted - version: "4.1.4" + version: "5.0.5" xdg_directories: dependency: transitive description: @@ -477,5 +477,5 @@ packages: source: hosted version: "6.3.0" sdks: - dart: ">=3.0.0-417 <4.0.0" + dart: ">=3.0.0 <4.0.0" flutter: ">=3.3.0" diff --git a/pubspec.lock b/pubspec.lock index eeb8838b..16734631 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -101,10 +101,10 @@ packages: dependency: transitive description: name: bidi - sha256: dc00274c7edabae2ab30c676e736ea1eb0b1b7a1b436cb5fe372e431ccb39ab0 + sha256: "6794b226bc939731308b8539c49bb6c2fdbf0e78c3a65e9b9e81e727c256dfe6" url: "https://pub.dev" source: hosted - version: "2.0.6" + version: "2.0.7" bloc: dependency: transitive description: @@ -133,10 +133,10 @@ packages: dependency: transitive description: name: build - sha256: "43865b79fbb78532e4bff7c33087aa43b1d488c4fdef014eaef568af6d8016dc" + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" build_config: dependency: transitive description: @@ -157,18 +157,18 @@ packages: dependency: transitive description: name: build_resolvers - sha256: db49b8609ef8c81cca2b310618c3017c00f03a92af44c04d310b907b2d692d95 + sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "5e1929ad37d48bd382b124266cb8e521de5548d406a45a5ae6656c13dab73e37" + sha256: "10c6bcdbf9d049a0b666702cf1cee4ddfdc38f02a19d35ae392863b47519848b" url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "2.4.6" build_runner_core: dependency: transitive description: @@ -333,10 +333,10 @@ packages: dependency: transitive description: name: dart_style - sha256: f4f1f73ab3fd2afcbcca165ee601fe980d966af6a21b5970c6c9376955c528ad + sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" dbus: dependency: transitive description: @@ -389,27 +389,27 @@ packages: dependency: transitive description: name: dots_indicator - sha256: f1599baa429936ba87f06ae5f2adc920a367b16d08f74db58c3d0f6e93bcdb5c + sha256: "58b6a365744aa62aa1b70c4ea29e5106fbe064f5edaf7e9652e9b856edbfd9bb" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "3.0.0" dynamic_color: dependency: "direct main" description: name: dynamic_color - sha256: "74dff1435a695887ca64899b8990004f8d1232b0e84bfc4faa1fdda7c6f57cc1" + sha256: de4798a7069121aee12d5895315680258415de9b00e717723a1bd73d58f0126d url: "https://pub.dev" source: hosted - version: "1.6.5" + version: "1.6.6" edge_detection: dependency: "direct main" description: path: "." ref: master - resolved-ref: "6ca5e015fc9cb4603890bddacdea0cafb839650d" + resolved-ref: "01636d9050d409177934ec64876c1c83c2567513" url: "https://github.com/sawankumarbundelkhandi/edge_detection" source: git - version: "1.1.1" + version: "1.1.2" equatable: dependency: "direct main" description: @@ -483,10 +483,10 @@ packages: dependency: "direct main" description: name: flutter_cache_manager - sha256: "32cd900555219333326a2d0653aaaf8671264c29befa65bbd9856d204a4c9fb3" + sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba" url: "https://pub.dev" source: hosted - version: "3.3.0" + version: "3.3.1" flutter_colorpicker: dependency: "direct main" description: @@ -698,10 +698,10 @@ packages: dependency: "direct main" description: name: flutter_typeahead - sha256: d72e7079d01b4ec109a12b01b06a85c09a41ae4531f8a0ca5ef9f759ce4e64a2 + sha256: a3539f7a90246b152f569029dedcf0b842532d3f2a440701b520e0bf2acbcf42 url: "https://pub.dev" source: hosted - version: "4.6.1" + version: "4.6.2" flutter_web_plugins: dependency: transitive description: flutter @@ -719,10 +719,10 @@ packages: dependency: "direct main" description: name: font_awesome_flutter - sha256: "959ef4add147753f990b4a7c6cccb746d5792dbdc81b1cde99e62e7edb31b206" + sha256: "5fb789145cae1f4c3245c58b3f8fb287d055c26323879eab57a7bf0cfd1e45f3" url: "https://pub.dev" source: hosted - version: "10.4.0" + version: "10.5.0" freezed: dependency: "direct dev" description: @@ -764,10 +764,10 @@ packages: dependency: transitive description: name: graphs - sha256: f9e130f3259f52d26f0cfc0e964513796dafed572fa52e45d2f8d6ca14db39b2 + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.1" hive: dependency: "direct main" description: @@ -804,10 +804,10 @@ packages: dependency: "direct main" description: name: http - sha256: "4c3f04bfb64d3efd508d06b41b825542f08122d30bda4933fb95c069d22a4fa3" + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" http_methods: dependency: transitive description: @@ -836,10 +836,10 @@ packages: dependency: "direct main" description: name: hydrated_bloc - sha256: "0ea117b32259d9a79c2a2d33eef92e9dd676b88ec4f1ef94102c5889ca1673b6" + sha256: "24994e61f64904d911683cce1a31dc4ef611619da5253f1de2b7b8fc6f79a118" url: "https://pub.dev" source: hosted - version: "9.1.1" + version: "9.1.2" image: dependency: "direct main" description: @@ -865,10 +865,10 @@ packages: dependency: "direct main" description: name: introduction_screen - sha256: f194ae655a84b945a2aedb7961d09948d789fc91088efb032666112923bcbc1e + sha256: f39be426026785b8fea4ed93e226e7fc28ef49a4c78c3f86c958bae26dabef00 url: "https://pub.dev" source: hosted - version: "3.1.8" + version: "3.1.9" io: dependency: transitive description: @@ -905,10 +905,10 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: "61a60716544392a82726dd0fa1dd6f5f1fd32aec66422b6e229e7b90d52325c4" + sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969 url: "https://pub.dev" source: hosted - version: "6.7.0" + version: "6.7.1" lints: dependency: transitive description: @@ -961,10 +961,10 @@ packages: dependency: transitive description: name: local_auth_windows - sha256: "19323b75ab781d5362dbb15dcb7e0916d2431c7a6dbdda016ec9708689877f73" + sha256: "5af808e108c445d0cf702a8c5f8242f1363b7970320334f82e6e1e8ad0b0d7d4" url: "https://pub.dev" source: hosted - version: "1.0.8" + version: "1.0.9" logging: dependency: transitive description: @@ -1179,54 +1179,46 @@ packages: url: "https://pub.dev" source: hosted version: "3.10.4" - pedantic: - dependency: transitive - description: - name: pedantic - sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602" - url: "https://pub.dev" - source: hosted - version: "1.11.1" permission_handler: dependency: "direct main" description: name: permission_handler - sha256: "1b6b3e73f0bcbc856548bbdfb1c33084a401c4f143e220629a9055233d76c331" + sha256: "415af30ba76a84faccfe1eb251fe1e4fdc790f876924c65ad7d6ed7a1404bcd6" url: "https://pub.dev" source: hosted - version: "10.3.0" + version: "10.4.2" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: "8f6a95ccbca13766882f95d32684d7c9bfe6c45650c32bedba948ef1c6a4ddf7" + sha256: "3b61f3da3b1c83bc3fb6a2b431e8dab01d0e5b45f6a3d9c7609770ec88b2a89e" url: "https://pub.dev" source: hosted - version: "10.2.3" + version: "10.3.0" permission_handler_apple: dependency: transitive description: name: permission_handler_apple - sha256: "08dcb6ce628ac0b257e429944b4c652c2a4e6af725bdf12b498daa2c6b2b1edb" + sha256: "7a187b671a39919462af2b5e813148365b71a615979165a119868d667fe90c03" url: "https://pub.dev" source: hosted - version: "9.1.0" + version: "9.1.3" permission_handler_platform_interface: dependency: transitive description: name: permission_handler_platform_interface - sha256: de20a5c3269229c1ae2e5a6b822f6cb59578b23e8255c93fbeebfc82116e6b11 + sha256: "463a07cb7cc6c758a7a1c7da36ce666bb80a0b4b5e92df0fa36872e0ed456993" url: "https://pub.dev" source: hosted - version: "3.10.0" + version: "3.11.1" permission_handler_windows: dependency: transitive description: name: permission_handler_windows - sha256: f67cab14b4328574938ecea2db3475dad7af7ead6afab6338772c5f88963e38b + sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098 url: "https://pub.dev" source: hosted - version: "0.1.2" + version: "0.1.3" petitparser: dependency: transitive description: @@ -1327,10 +1319,10 @@ packages: dependency: transitive description: name: pub_updater - sha256: "05ae70703e06f7fdeb05f7f02dd680b8aad810e87c756a618f33e1794635115c" + sha256: b06600619c8c219065a548f8f7c192b3e080beff95488ed692780f48f69c0625 url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.3.1" pubspec_parse: dependency: transitive description: @@ -1484,18 +1476,18 @@ packages: dependency: transitive description: name: source_gen - sha256: "373f96cf5a8744bc9816c1ff41cf5391bbdbe3d7a96fe98c622b6738a8a7bd33" + sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" source_helper: dependency: transitive description: name: source_helper - sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" + sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.4" source_map_stack_trace: dependency: transitive description: @@ -1652,18 +1644,18 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: eb1e00ab44303d50dd487aab67ebc575456c146c6af44422f9c13889984c00f3 + sha256: "781bd58a1eb16069412365c98597726cd8810ae27435f04b3b4d3a470bacd61e" url: "https://pub.dev" source: hosted - version: "6.1.11" + version: "6.1.12" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: eed4e6a1164aa9794409325c3b707ff424d4d1c2a785e7db67f8bbda00e36e51 + sha256: "15f5acbf0dce90146a0f5a2c4a002b1814a6303c4c5c075aa2623b2d16156f03" url: "https://pub.dev" source: hosted - version: "6.0.35" + version: "6.0.36" url_launcher_ios: dependency: transitive description: @@ -1692,26 +1684,26 @@ packages: dependency: transitive description: name: url_launcher_platform_interface - sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370" + sha256: bfdfa402f1f3298637d71ca8ecfe840b4696698213d5346e9d12d4ab647ee2ea url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: "6bb1e5d7fe53daf02a8fee85352432a40b1f868a81880e99ec7440113d5cfcab" + sha256: cc26720eefe98c1b71d85f9dc7ef0cada5132617046369d9dc296b3ecaa5cbb4 url: "https://pub.dev" source: hosted - version: "2.0.17" + version: "2.0.18" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "254708f17f7c20a9c8c471f67d86d76d4a3f9c1591aad1e15292008aceb82771" + sha256: "7967065dd2b5fccc18c653b97958fdf839c5478c28e767c61ee879f4e7882422" url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.0.7" uuid: dependency: "direct main" description: @@ -1780,10 +1772,10 @@ packages: dependency: transitive description: name: webview_flutter_android - sha256: "57a22c86065375c1598b57224f92d6008141be0c877c64100de8bfb6f71083d8" + sha256: "1c93e96f3069bacdc734fad6b7e1d3a480fd516a3ae5b8858becf7f07515a2f3" url: "https://pub.dev" source: hosted - version: "3.7.1" + version: "3.8.2" webview_flutter_platform_interface: dependency: transitive description: @@ -1796,10 +1788,10 @@ packages: dependency: transitive description: name: webview_flutter_wkwebview - sha256: "6bbc6ade302b842999b27cbaa7171241c273deea8a9c73f92ceb3d811c767de2" + sha256: a8d7e8b4be2a79e83b70235369971ec97d14df4cdbb40d305a8eeae67d8e6432 url: "https://pub.dev" source: hosted - version: "3.4.4" + version: "3.6.2" win32: dependency: transitive description: diff --git a/test/utils.dart b/test/utils.dart index c29c4737..44d0c93e 100644 --- a/test/utils.dart +++ b/test/utils.dart @@ -2,9 +2,9 @@ import 'dart:convert'; import 'dart:math'; import 'package:flutter/services.dart'; -import 'package:paperless_mobile/core/type/types.dart'; -Future loadOne(String filePath, T Function(JSON) transformFn, int? id) async { +Future loadOne(String filePath, + T Function(Map) transformFn, int? id) async { if (id != null) { final coll = await loadCollection(filePath, transformFn); return coll.firstWhere((dynamic element) => element.id == id); @@ -13,22 +13,27 @@ Future loadOne(String filePath, T Function(JSON) transformFn, int? id) asy return transformFn(jsonDecode(response)); } -Future> loadCollection(String filePath, T Function(JSON) transformFn, +Future> loadCollection( + String filePath, T Function(Map) transformFn, {int? numItems, List? ids}) async { - assert(((numItems != null) ^ (ids != null)) || (numItems == null && ids == null)); + assert(((numItems != null) ^ (ids != null)) || + (numItems == null && ids == null)); final String response = await rootBundle.loadString(filePath); final lst = (jsonDecode(response) as List); - final res = (jsonDecode(response) as List).map((e) => transformFn(e)).toList(); + final res = (jsonDecode(response) as List) + .map((e) => transformFn(e)) + .toList(); if (ids != null) { return res.where((dynamic element) => ids.contains(element.id)).toList(); } if (numItems != null && lst.length < numItems) { - throw Exception("The requested collection contains only ${lst.length} items!"); + throw Exception( + "The requested collection contains only ${lst.length} items!"); } else { return res.sublist(0, numItems); } } const _chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890'; -String getRandomString(int length) => String.fromCharCodes( - Iterable.generate(length, (_) => _chars.codeUnitAt(Random().nextInt(_chars.length)))); +String getRandomString(int length) => String.fromCharCodes(Iterable.generate( + length, (_) => _chars.codeUnitAt(Random().nextInt(_chars.length))));