From 738ef99bc5c4aaa95882cd8bf3593daa8917ed07 Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Thu, 5 Jan 2023 01:38:00 +0100 Subject: [PATCH] WIP - Redesigned login flow --- lib/core/bloc/connectivity_cubit.dart | 2 +- .../dio_http_error_interceptor.dart | 21 +- .../language_header.interceptor.dart | 1 - ...etry_on_connection_change_interceptor.dart | 1 + lib/core/repository/base_repository.dart | 18 ++ .../impl/correspondent_repository_impl.dart | 28 +- .../impl/document_type_repository_impl.dart | 24 +- .../impl/saved_view_repository_impl.dart | 15 +- .../impl/storage_path_repository_impl.dart | 24 +- .../repository/impl/tag_repository_impl.dart | 26 +- lib/core/repository/label_repository.dart | 16 +- .../repository/saved_view_repository.dart | 13 +- .../authentication_aware_dio_manager.dart | 4 - .../security/security_context_subject.dart | 26 -- .../service/connectivity_status.service.dart | 49 --- .../service/connectivity_status_service.dart | 113 +++++++ ...orm_builder_relative_date_range_field.dart | 1 - lib/core/widgets/offline_banner.dart | 2 +- lib/core/widgets/paperless_logo.dart | 14 +- .../application_intro_slideshow.dart | 2 - .../configuration_done_intro_slide.dart | 27 -- .../view/pages/document_details_page.dart | 2 +- .../cubit/document_upload_cubit.dart | 8 +- .../widgets/search/document_filter_panel.dart | 3 - .../cubit/edit_document_cubit.dart | 16 +- .../edit_label/cubit/edit_label_cubit.dart | 7 +- .../edit_label/view/edit_label_page.dart | 13 +- lib/features/edit_label/view/label_form.dart | 2 - .../home/view/widget/info_drawer.dart | 185 ++++++++---- lib/features/inbox/view/pages/inbox_page.dart | 2 +- .../inbox/view/widgets/inbox_item.dart | 1 - lib/features/labels/bloc/label_cubit.dart | 17 +- lib/features/labels/bloc/label_state.dart | 4 +- .../view/widgets/correspondent_widget.dart | 4 +- .../view/widgets/document_type_widget.dart | 1 - ...rage_path_autofill_form_builder_field.dart | 1 - .../view/widgets/storage_path_widget.dart | 1 - .../labels/view/pages/labels_page.dart | 1 - .../labels/view/widgets/label_item.dart | 1 - .../labels/view/widgets/label_tab_view.dart | 13 +- .../view/pages/linked_documents_page.dart | 1 - .../login/bloc/authentication_cubit.dart | 59 ++-- .../login/model/reachability_status.dart | 8 + lib/features/login/view/login_page.dart | 112 ++++--- .../client_certificate_form_field.dart | 106 ++++--- .../never_scrollable_scroll_behavior.dart | 8 + .../widgets/server_address_form_field.dart | 79 +---- .../view/widgets/server_connection_page.dart | 133 ++++++++ .../login/view/widgets/server_login_page.dart | 49 +++ .../saved_view/cubit/saved_view_cubit.dart | 2 +- .../scan/bloc/document_scanner_cubit.dart | 2 - lib/l10n/intl_de.arb | 2 +- lib/l10n/intl_en.arb | 2 +- lib/main.dart | 8 +- .../models/paperless_server_exception.dart | 2 +- .../authentication_api_impl.dart | 7 +- .../paperless_documents_api_impl.dart | 212 +++++++------ .../labels_api/paperless_labels_api_impl.dart | 284 ++++++++++-------- .../paperless_saved_views_api_impl.dart | 51 ++-- .../paperless_api/lib/src/request_utils.dart | 78 ++--- 60 files changed, 1159 insertions(+), 755 deletions(-) create mode 100644 lib/core/repository/base_repository.dart delete mode 100644 lib/core/security/security_context_subject.dart delete mode 100644 lib/core/service/connectivity_status.service.dart create mode 100644 lib/core/service/connectivity_status_service.dart delete mode 100644 lib/features/app_intro/widgets/configuration_done_intro_slide.dart create mode 100644 lib/features/login/model/reachability_status.dart create mode 100644 lib/features/login/view/widgets/never_scrollable_scroll_behavior.dart create mode 100644 lib/features/login/view/widgets/server_connection_page.dart create mode 100644 lib/features/login/view/widgets/server_login_page.dart diff --git a/lib/core/bloc/connectivity_cubit.dart b/lib/core/bloc/connectivity_cubit.dart index c7fa6e1..1dab162 100644 --- a/lib/core/bloc/connectivity_cubit.dart +++ b/lib/core/bloc/connectivity_cubit.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_mobile/core/service/connectivity_status.service.dart'; +import 'package:paperless_mobile/core/service/connectivity_status_service.dart'; class ConnectivityCubit extends Cubit { final ConnectivityStatusService connectivityStatusService; diff --git a/lib/core/interceptor/dio_http_error_interceptor.dart b/lib/core/interceptor/dio_http_error_interceptor.dart index 92e83d6..f7f5670 100644 --- a/lib/core/interceptor/dio_http_error_interceptor.dart +++ b/lib/core/interceptor/dio_http_error_interceptor.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:dio/dio.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/type/types.dart'; @@ -9,12 +11,22 @@ class DioHttpErrorInterceptor extends Interceptor { // try to parse contained error message, otherwise return response final dynamic data = err.response?.data; if (data is Map) { - return _handlePaperlessValidationError(data, handler, err); + _handlePaperlessValidationError(data, handler, err); } else if (data is String) { - return _handlePlainError(data, handler, err); + _handlePlainError(data, handler, err); } + } else if (err.error is SocketException) { + // Offline + handler.reject( + DioError( + error: const PaperlessServerException(ErrorCode.deviceOffline), + requestOptions: err.requestOptions, + type: DioErrorType.connectTimeout, + ), + ); + } else { + handler.reject(err); } - handler.reject(err); } void _handlePaperlessValidationError( @@ -54,7 +66,8 @@ class DioHttpErrorInterceptor extends Interceptor { DioError( requestOptions: err.requestOptions, type: DioErrorType.response, - error: ErrorCode.missingClientCertificate, + error: const PaperlessServerException( + ErrorCode.missingClientCertificate), ), ); } diff --git a/lib/core/interceptor/language_header.interceptor.dart b/lib/core/interceptor/language_header.interceptor.dart index 1261676..4d81272 100644 --- a/lib/core/interceptor/language_header.interceptor.dart +++ b/lib/core/interceptor/language_header.interceptor.dart @@ -1,5 +1,4 @@ import 'package:dio/dio.dart'; -import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; class LanguageHeaderInterceptor extends Interceptor { String preferredLocaleSubtag; diff --git a/lib/core/interceptor/retry_on_connection_change_interceptor.dart b/lib/core/interceptor/retry_on_connection_change_interceptor.dart index 74ed9be..e6f90a6 100644 --- a/lib/core/interceptor/retry_on_connection_change_interceptor.dart +++ b/lib/core/interceptor/retry_on_connection_change_interceptor.dart @@ -15,6 +15,7 @@ class RetryOnConnectionChangeInterceptor extends Interceptor { try { handler.resolve(await DioHttpRequestRetrier(dio: dio) .requestRetry(err.requestOptions) + // ignore: body_might_complete_normally_catch_error .catchError((e) { handler.next(err); })); diff --git a/lib/core/repository/base_repository.dart b/lib/core/repository/base_repository.dart new file mode 100644 index 0000000..168f737 --- /dev/null +++ b/lib/core/repository/base_repository.dart @@ -0,0 +1,18 @@ +/// +/// Base repository class which all repositories should implement +/// +abstract class BaseRepository { + Stream get values; + + State? get current; + + bool get isInitialized; + + Future create(Object object); + Future find(int id); + Future> findAll([Iterable? ids]); + Future update(Object object); + Future delete(Object object); + + void clear(); +} diff --git a/lib/core/repository/impl/correspondent_repository_impl.dart b/lib/core/repository/impl/correspondent_repository_impl.dart index 52af5ec..20c53c5 100644 --- a/lib/core/repository/impl/correspondent_repository_impl.dart +++ b/lib/core/repository/impl/correspondent_repository_impl.dart @@ -7,35 +7,43 @@ import 'package:rxdart/rxdart.dart' show BehaviorSubject; class CorrespondentRepositoryImpl implements LabelRepository { final PaperlessLabelsApi _api; - final _subject = BehaviorSubject>.seeded(const {}); + final _subject = BehaviorSubject?>(); CorrespondentRepositoryImpl(this._api); + @override - Stream> get labels => + bool get isInitialized => _subject.valueOrNull != null; + + @override + Stream?> get values => _subject.stream.asBroadcastStream(); + Map get _currentValueOrEmpty => + _subject.valueOrNull ?? {}; + @override Future create(Correspondent correspondent) async { final created = await _api.saveCorrespondent(correspondent); - final updatedState = {..._subject.value} + final updatedState = {..._currentValueOrEmpty} ..putIfAbsent(created.id!, () => created); _subject.add(updatedState); return created; } @override - Future delete(Correspondent correspondent) async { + Future delete(Correspondent correspondent) async { await _api.deleteCorrespondent(correspondent); - final updatedState = {..._subject.value} + final updatedState = {..._currentValueOrEmpty} ..removeWhere((k, v) => k == correspondent.id); _subject.add(updatedState); + return correspondent.id!; } @override Future find(int id) async { final correspondent = await _api.getCorrespondent(id); if (correspondent != null) { - final updatedState = {..._subject.value}..[id] = correspondent; + final updatedState = {..._currentValueOrEmpty}..[id] = correspondent; _subject.add(updatedState); return correspondent; } @@ -45,7 +53,7 @@ class CorrespondentRepositoryImpl implements LabelRepository { @override Future> findAll([Iterable? ids]) async { final correspondents = await _api.getCorrespondents(ids); - final updatedState = {..._subject.value} + final updatedState = {..._currentValueOrEmpty} ..addEntries(correspondents.map((e) => MapEntry(e.id!, e))); _subject.add(updatedState); return correspondents; @@ -54,7 +62,7 @@ class CorrespondentRepositoryImpl implements LabelRepository { @override Future update(Correspondent correspondent) async { final updated = await _api.updateCorrespondent(correspondent); - final updatedState = {..._subject.value} + final updatedState = {..._currentValueOrEmpty} ..update(updated.id!, (_) => updated); _subject.add(updatedState); return updated; @@ -62,9 +70,9 @@ class CorrespondentRepositoryImpl implements LabelRepository { @override void clear() { - _subject.add(const {}); + _subject.add(null); } @override - Map get current => _subject.value; + Map? get current => _subject.valueOrNull; } diff --git a/lib/core/repository/impl/document_type_repository_impl.dart b/lib/core/repository/impl/document_type_repository_impl.dart index 2dbb079..8c58cbf 100644 --- a/lib/core/repository/impl/document_type_repository_impl.dart +++ b/lib/core/repository/impl/document_type_repository_impl.dart @@ -5,36 +5,42 @@ import 'package:rxdart/rxdart.dart' show BehaviorSubject; class DocumentTypeRepositoryImpl implements LabelRepository { final PaperlessLabelsApi _api; - final _subject = BehaviorSubject>.seeded(const {}); + final _subject = BehaviorSubject?>(); DocumentTypeRepositoryImpl(this._api); @override - Stream> get labels => + Stream?> get values => _subject.stream.asBroadcastStream(); + @override + bool get isInitialized => _subject.valueOrNull != null; + + Map get _currentValueOrEmpty => _subject.valueOrNull ?? {}; + @override Future create(DocumentType documentType) async { final created = await _api.saveDocumentType(documentType); - final updatedState = {..._subject.value} + final updatedState = {..._currentValueOrEmpty} ..putIfAbsent(created.id!, () => created); _subject.add(updatedState); return created; } @override - Future delete(DocumentType documentType) async { + Future delete(DocumentType documentType) async { await _api.deleteDocumentType(documentType); - final updatedState = {..._subject.value} + final updatedState = {..._currentValueOrEmpty} ..removeWhere((k, v) => k == documentType.id); _subject.add(updatedState); + return documentType.id!; } @override Future find(int id) async { final documentType = await _api.getDocumentType(id); if (documentType != null) { - final updatedState = {..._subject.value}..[id] = documentType; + final updatedState = {..._currentValueOrEmpty}..[id] = documentType; _subject.add(updatedState); return documentType; } @@ -44,7 +50,7 @@ class DocumentTypeRepositoryImpl implements LabelRepository { @override Future> findAll([Iterable? ids]) async { final documentTypes = await _api.getDocumentTypes(ids); - final updatedState = {..._subject.value} + final updatedState = {..._currentValueOrEmpty} ..addEntries(documentTypes.map((e) => MapEntry(e.id!, e))); _subject.add(updatedState); return documentTypes; @@ -53,7 +59,7 @@ class DocumentTypeRepositoryImpl implements LabelRepository { @override Future update(DocumentType documentType) async { final updated = await _api.updateDocumentType(documentType); - final updatedState = {..._subject.value} + final updatedState = {..._currentValueOrEmpty} ..update(updated.id!, (_) => updated); _subject.add(updatedState); return updated; @@ -65,5 +71,5 @@ class DocumentTypeRepositoryImpl implements LabelRepository { } @override - Map get current => _subject.value; + Map? get current => _subject.valueOrNull; } diff --git a/lib/core/repository/impl/saved_view_repository_impl.dart b/lib/core/repository/impl/saved_view_repository_impl.dart index dfeae1e..b0847be 100644 --- a/lib/core/repository/impl/saved_view_repository_impl.dart +++ b/lib/core/repository/impl/saved_view_repository_impl.dart @@ -7,10 +7,10 @@ class SavedViewRepositoryImpl implements SavedViewRepository { SavedViewRepositoryImpl(this._api); - final BehaviorSubject?> _subject = BehaviorSubject(); + final _subject = BehaviorSubject?>(); @override - Stream?> get savedViews => + Stream?> get values => _subject.stream.asBroadcastStream(); @override @@ -54,4 +54,15 @@ class SavedViewRepositoryImpl implements SavedViewRepository { _subject.add(updatedState); return found; } + + @override + Map? get current => _subject.valueOrNull; + + @override + bool get isInitialized => _subject.hasValue; + + @override + Future update(SavedView object) { + throw UnimplementedError("Saved view update is not yet implemented"); + } } diff --git a/lib/core/repository/impl/storage_path_repository_impl.dart b/lib/core/repository/impl/storage_path_repository_impl.dart index c555fa9..2a64f22 100644 --- a/lib/core/repository/impl/storage_path_repository_impl.dart +++ b/lib/core/repository/impl/storage_path_repository_impl.dart @@ -5,36 +5,39 @@ import 'package:rxdart/rxdart.dart' show BehaviorSubject; class StoragePathRepositoryImpl implements LabelRepository { final PaperlessLabelsApi _api; - final _subject = BehaviorSubject>.seeded(const {}); + final _subject = BehaviorSubject?>(); StoragePathRepositoryImpl(this._api); @override - Stream> get labels => + Stream?> get values => _subject.stream.asBroadcastStream(); + Map get _currentValueOrEmpty => _subject.valueOrNull ?? {}; + @override Future create(StoragePath storagePath) async { final created = await _api.saveStoragePath(storagePath); - final updatedState = {..._subject.value} + final updatedState = {..._currentValueOrEmpty} ..putIfAbsent(created.id!, () => created); _subject.add(updatedState); return created; } @override - Future delete(StoragePath storagePath) async { + Future delete(StoragePath storagePath) async { await _api.deleteStoragePath(storagePath); - final updatedState = {..._subject.value} + final updatedState = {..._currentValueOrEmpty} ..removeWhere((k, v) => k == storagePath.id); _subject.add(updatedState); + return storagePath.id!; } @override Future find(int id) async { final storagePath = await _api.getStoragePath(id); if (storagePath != null) { - final updatedState = {..._subject.value}..[id] = storagePath; + final updatedState = {..._currentValueOrEmpty}..[id] = storagePath; _subject.add(updatedState); return storagePath; } @@ -44,7 +47,7 @@ class StoragePathRepositoryImpl implements LabelRepository { @override Future> findAll([Iterable? ids]) async { final storagePaths = await _api.getStoragePaths(ids); - final updatedState = {..._subject.value} + final updatedState = {..._currentValueOrEmpty} ..addEntries(storagePaths.map((e) => MapEntry(e.id!, e))); _subject.add(updatedState); return storagePaths; @@ -53,7 +56,7 @@ class StoragePathRepositoryImpl implements LabelRepository { @override Future update(StoragePath storagePath) async { final updated = await _api.updateStoragePath(storagePath); - final updatedState = {..._subject.value} + final updatedState = {..._currentValueOrEmpty} ..update(updated.id!, (_) => updated); _subject.add(updatedState); return updated; @@ -65,5 +68,8 @@ class StoragePathRepositoryImpl implements LabelRepository { } @override - Map get current => _subject.value; + Map? get current => _subject.valueOrNull; + + @override + bool get isInitialized => _subject.valueOrNull != null; } diff --git a/lib/core/repository/impl/tag_repository_impl.dart b/lib/core/repository/impl/tag_repository_impl.dart index 437512d..fcfdad0 100644 --- a/lib/core/repository/impl/tag_repository_impl.dart +++ b/lib/core/repository/impl/tag_repository_impl.dart @@ -5,35 +5,38 @@ import 'package:rxdart/rxdart.dart' show BehaviorSubject; class TagRepositoryImpl implements LabelRepository { final PaperlessLabelsApi _api; - final _subject = BehaviorSubject>.seeded(const {}); + final _subject = BehaviorSubject?>(); TagRepositoryImpl(this._api); @override - Stream> get labels => _subject.stream.asBroadcastStream(); + Stream?> get values => _subject.stream.asBroadcastStream(); + + Map get _currentValueOrEmpty => _subject.valueOrNull ?? {}; @override Future create(Tag tag) async { final created = await _api.saveTag(tag); - final updatedState = {..._subject.value} + final updatedState = {..._currentValueOrEmpty} ..putIfAbsent(created.id!, () => created); _subject.add(updatedState); return created; } @override - Future delete(Tag tag) async { + Future delete(Tag tag) async { await _api.deleteTag(tag); - final updatedState = {..._subject.value} + final updatedState = {..._currentValueOrEmpty} ..removeWhere((k, v) => k == tag.id); _subject.add(updatedState); + return tag.id!; } @override Future find(int id) async { final tag = await _api.getTag(id); if (tag != null) { - final updatedState = {..._subject.value}..[id] = tag; + final updatedState = {..._currentValueOrEmpty}..[id] = tag; _subject.add(updatedState); return tag; } @@ -43,7 +46,7 @@ class TagRepositoryImpl implements LabelRepository { @override Future> findAll([Iterable? ids]) async { final tags = await _api.getTags(ids); - final updatedState = {..._subject.value} + final updatedState = {..._currentValueOrEmpty} ..addEntries(tags.map((e) => MapEntry(e.id!, e))); _subject.add(updatedState); return tags; @@ -52,7 +55,7 @@ class TagRepositoryImpl implements LabelRepository { @override Future update(Tag tag) async { final updated = await _api.updateTag(tag); - final updatedState = {..._subject.value} + final updatedState = {..._currentValueOrEmpty} ..update(updated.id!, (_) => updated); _subject.add(updatedState); return updated; @@ -60,9 +63,12 @@ class TagRepositoryImpl implements LabelRepository { @override void clear() { - _subject.add(const {}); + _subject.add(null); } @override - Map get current => _subject.value; + Map? get current => _subject.valueOrNull; + + @override + bool get isInitialized => _subject.valueOrNull != null; } diff --git a/lib/core/repository/label_repository.dart b/lib/core/repository/label_repository.dart index 20b38c2..7d88d06 100644 --- a/lib/core/repository/label_repository.dart +++ b/lib/core/repository/label_repository.dart @@ -1,15 +1,5 @@ import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/base_repository.dart'; -abstract class LabelRepository { - Stream> get labels; - - Map get current; - - Future create(T label); - Future find(int id); - Future> findAll([Iterable? ids]); - Future update(T label); - Future delete(T label); - - void clear(); -} +abstract class LabelRepository + implements BaseRepository, T> {} diff --git a/lib/core/repository/saved_view_repository.dart b/lib/core/repository/saved_view_repository.dart index 50dce85..0b19b05 100644 --- a/lib/core/repository/saved_view_repository.dart +++ b/lib/core/repository/saved_view_repository.dart @@ -1,12 +1,5 @@ import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/base_repository.dart'; -abstract class SavedViewRepository { - Stream?> get savedViews; - - Future create(SavedView view); - Future find(int id); - Future> findAll([Iterable? ids]); - Future delete(SavedView view); - - void clear(); -} +abstract class SavedViewRepository + implements BaseRepository, SavedView> {} diff --git a/lib/core/security/authentication_aware_dio_manager.dart b/lib/core/security/authentication_aware_dio_manager.dart index 60db727..888b927 100644 --- a/lib/core/security/authentication_aware_dio_manager.dart +++ b/lib/core/security/authentication_aware_dio_manager.dart @@ -1,18 +1,14 @@ -import 'dart:async'; import 'dart:io'; import 'package:dio/adapter.dart'; import 'package:dio/dio.dart'; import 'package:paperless_mobile/core/interceptor/retry_on_connection_change_interceptor.dart'; -import 'package:paperless_mobile/extensions/security_context_extension.dart'; import 'package:paperless_mobile/features/login/model/client_certificate.dart'; class AuthenticationAwareDioManager { final Dio client; final List interceptors; - /// Some dependencies require an [HttpClient], therefore this is also maintained here. - AuthenticationAwareDioManager([this.interceptors = const []]) : client = _initDio(interceptors); diff --git a/lib/core/security/security_context_subject.dart b/lib/core/security/security_context_subject.dart deleted file mode 100644 index 036b720..0000000 --- a/lib/core/security/security_context_subject.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:dio/dio.dart'; -import 'package:http/io_client.dart'; -import 'package:http_interceptor/http_interceptor.dart'; -import 'package:paperless_mobile/features/login/model/client_certificate.dart'; -import 'package:rxdart/rxdart.dart'; - -extension SecurityContextAwareBaseClientSubjectExtension - on BehaviorSubject { - /// - /// Registers new security context in a new [HttpClient]. - /// - - BaseClient _createSecurityContextAwareHttpClient( - SecurityContext context, { - List interceptors = const [], - }) { - Dio(BaseOptions()); - return InterceptedClient.build( - client: IOClient(HttpClient(context: context)), - interceptors: interceptors, - ); - } -} diff --git a/lib/core/service/connectivity_status.service.dart b/lib/core/service/connectivity_status.service.dart deleted file mode 100644 index 9b81267..0000000 --- a/lib/core/service/connectivity_status.service.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'dart:io'; - -import 'package:connectivity_plus/connectivity_plus.dart'; - -abstract class ConnectivityStatusService { - Future isConnectedToInternet(); - Future isServerReachable(String serverAddress); - Stream connectivityChanges(); -} - -class ConnectivityStatusServiceImpl implements ConnectivityStatusService { - final Connectivity connectivity; - - ConnectivityStatusServiceImpl(this.connectivity); - - @override - Stream connectivityChanges() { - return connectivity.onConnectivityChanged - .map(_hasActiveInternetConnection) - .asBroadcastStream(); - } - - @override - Future isConnectedToInternet() async { - return _hasActiveInternetConnection( - await (Connectivity().checkConnectivity())); - } - - @override - Future isServerReachable(String serverAddress) async { - try { - var uri = Uri.parse(serverAddress); - final result = await InternetAddress.lookup(uri.host); - if (result.isNotEmpty && result.first.rawAddress.isNotEmpty) { - return true; - } else { - return false; - } - } on SocketException catch (_) { - return false; - } - } - - bool _hasActiveInternetConnection(ConnectivityResult conn) { - return conn == ConnectivityResult.mobile || - conn == ConnectivityResult.wifi || - conn == ConnectivityResult.ethernet; - } -} diff --git a/lib/core/service/connectivity_status_service.dart b/lib/core/service/connectivity_status_service.dart new file mode 100644 index 0000000..f7ed264 --- /dev/null +++ b/lib/core/service/connectivity_status_service.dart @@ -0,0 +1,113 @@ +import 'dart:io'; + +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:dio/adapter.dart'; +import 'package:dio/dio.dart'; +import 'package:paperless_mobile/features/login/model/client_certificate.dart'; +import 'package:paperless_mobile/features/login/model/reachability_status.dart'; + +abstract class ConnectivityStatusService { + Future isConnectedToInternet(); + Future isServerReachable(String serverAddress); + Stream connectivityChanges(); + Future isPaperlessServerReachable( + String serverAddress, [ + ClientCertificate? clientCertificate, + ]); +} + +class ConnectivityStatusServiceImpl implements ConnectivityStatusService { + final Connectivity _connectivity; + + ConnectivityStatusServiceImpl(this._connectivity); + + @override + Stream connectivityChanges() { + return _connectivity.onConnectivityChanged + .map(_hasActiveInternetConnection) + .asBroadcastStream(); + } + + @override + Future isConnectedToInternet() async { + return _hasActiveInternetConnection( + await (Connectivity().checkConnectivity())); + } + + @override + Future isServerReachable(String serverAddress) async { + try { + var uri = Uri.parse(serverAddress); + final result = await InternetAddress.lookup(uri.host); + if (result.isNotEmpty && result.first.rawAddress.isNotEmpty) { + return true; + } else { + return false; + } + } on SocketException catch (_) { + return false; + } + } + + bool _hasActiveInternetConnection(ConnectivityResult conn) { + return conn == ConnectivityResult.mobile || + conn == ConnectivityResult.wifi || + conn == ConnectivityResult.ethernet; + } + + @override + Future isPaperlessServerReachable( + String serverAddress, [ + ClientCertificate? clientCertificate, + ]) async { + if (!RegExp(r"^https?://.*").hasMatch(serverAddress)) { + return ReachabilityStatus.unknown; + } + late SecurityContext context = SecurityContext(); + try { + if (clientCertificate != null) { + context + ..usePrivateKeyBytes( + clientCertificate.bytes, + password: clientCertificate.passphrase, + ) + ..useCertificateChainBytes( + clientCertificate.bytes, + password: clientCertificate.passphrase, + ) + ..setTrustedCertificatesBytes( + clientCertificate.bytes, + password: clientCertificate.passphrase, + ); + } + + final adapter = DefaultHttpClientAdapter() + ..onHttpClientCreate = (client) => HttpClient(context: context) + ..badCertificateCallback = + (X509Certificate cert, String host, int port) => true; + final Dio dio = Dio()..httpClientAdapter = adapter; + + final response = await dio.get('$serverAddress/api/'); + if (response.statusCode == 200) { + return ReachabilityStatus.reachable; + } + return ReachabilityStatus.notReachable; + } on DioError catch (error) { + if (error.error is String) { + if (error.response?.data is String) { + if ((error.response!.data as String) + .contains("No required SSL certificate was sent")) { + return ReachabilityStatus.missingClientCertificate; + } + } + } + return ReachabilityStatus.notReachable; + } on TlsException catch (error) { + if (error.osError?.errorCode == 318767212) { + //INCORRECT_PASSWORD for certificate + return ReachabilityStatus.invalidClientCertificateConfiguration; + } + return ReachabilityStatus.notReachable; + } + } +} diff --git a/lib/core/widgets/form_builder_fields/extended_date_range_form_field/form_builder_relative_date_range_field.dart b/lib/core/widgets/form_builder_fields/extended_date_range_form_field/form_builder_relative_date_range_field.dart index 41d992a..703782a 100644 --- a/lib/core/widgets/form_builder_fields/extended_date_range_form_field/form_builder_relative_date_range_field.dart +++ b/lib/core/widgets/form_builder_fields/extended_date_range_form_field/form_builder_relative_date_range_field.dart @@ -3,7 +3,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/widgets/form_builder_fields/extended_date_range_form_field/relative_date_range_picker_helper.dart'; import 'package:paperless_mobile/generated/l10n.dart'; class FormBuilderRelativeDateRangePicker extends StatefulWidget { diff --git a/lib/core/widgets/offline_banner.dart b/lib/core/widgets/offline_banner.dart index 334be93..3d9308a 100644 --- a/lib/core/widgets/offline_banner.dart +++ b/lib/core/widgets/offline_banner.dart @@ -13,7 +13,7 @@ class OfflineBanner extends StatelessWidget with PreferredSizeWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ Padding( - padding: EdgeInsets.symmetric(horizontal: 8.0), + padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Icon( Icons.cloud_off, size: 24, diff --git a/lib/core/widgets/paperless_logo.dart b/lib/core/widgets/paperless_logo.dart index dd359b7..1f5b08b 100644 --- a/lib/core/widgets/paperless_logo.dart +++ b/lib/core/widgets/paperless_logo.dart @@ -4,7 +4,16 @@ import 'package:flutter_svg/flutter_svg.dart'; class PaperlessLogo extends StatelessWidget { final double? height; final double? width; - const PaperlessLogo({Key? key, this.height, this.width}) : super(key: key); + final String _path; + + const PaperlessLogo.white({super.key, this.height, this.width}) + : _path = "assets/logos/paperless_logo_white.svg"; + + const PaperlessLogo.green({super.key, this.height, this.width}) + : _path = "assets/logos/paperless_logo_green.svg"; + + const PaperlessLogo.black({super.key, this.height, this.width}) + : _path = "assets/logos/paperless_logo_black.svg"; @override Widget build(BuildContext context) { @@ -15,8 +24,7 @@ class PaperlessLogo extends StatelessWidget { ), padding: const EdgeInsets.only(right: 8), child: SvgPicture.asset( - "assets/logo/paperless_ng_logo_light.svg", - color: Theme.of(context).primaryColor, + _path, ), ); } diff --git a/lib/features/app_intro/application_intro_slideshow.dart b/lib/features/app_intro/application_intro_slideshow.dart index 02a3231..f59e222 100644 --- a/lib/features/app_intro/application_intro_slideshow.dart +++ b/lib/features/app_intro/application_intro_slideshow.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:introduction_screen/introduction_screen.dart'; import 'package:paperless_mobile/core/global/asset_images.dart'; -import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; import 'package:paperless_mobile/features/settings/view/widgets/biometric_authentication_setting.dart'; import 'package:paperless_mobile/features/settings/view/widgets/language_selection_setting.dart'; import 'package:paperless_mobile/features/settings/view/widgets/theme_mode_setting.dart'; diff --git a/lib/features/app_intro/widgets/configuration_done_intro_slide.dart b/lib/features/app_intro/widgets/configuration_done_intro_slide.dart deleted file mode 100644 index e5c0b95..0000000 --- a/lib/features/app_intro/widgets/configuration_done_intro_slide.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/material.dart'; - -class ConfigurationDoneIntroSlide extends StatelessWidget { - const ConfigurationDoneIntroSlide({super.key}); - - @override - Widget build(BuildContext context) { - return Column( - //TODO: INTL - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Text( - "All set up!", - style: Theme.of(context).textTheme.titleLarge, - ), - Icon( - Icons.emoji_emotions_outlined, - size: 64, - ), - Text( - "You've successfully configured Paperless Mobile! Press 'GO' to get started managing your documents.", - textAlign: TextAlign.center, - ), - ], - ); - } -} 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 4f767f7..3bd300d 100644 --- a/lib/features/document_details/view/pages/document_details_page.dart +++ b/lib/features/document_details/view/pages/document_details_page.dart @@ -449,7 +449,7 @@ class _DetailsItem extends StatelessWidget { children: [ Text( label, - style: Theme.of(context).textTheme.caption, + style: Theme.of(context).textTheme.bodySmall, ), content, ], diff --git a/lib/features/document_upload/cubit/document_upload_cubit.dart b/lib/features/document_upload/cubit/document_upload_cubit.dart index 9295727..c2db423 100644 --- a/lib/features/document_upload/cubit/document_upload_cubit.dart +++ b/lib/features/document_upload/cubit/document_upload_cubit.dart @@ -10,7 +10,6 @@ import 'package:paperless_mobile/core/store/local_vault.dart'; part 'document_upload_state.dart'; class DocumentUploadCubit extends Cubit { - final LocalVault _localVault; final PaperlessDocumentsApi _documentApi; final LabelRepository _tagRepository; @@ -29,7 +28,6 @@ class DocumentUploadCubit extends Cubit { _tagRepository = tagRepository, _correspondentRepository = correspondentRepository, _documentTypeRepository = documentTypeRepository, - _localVault = localVault, super( const DocumentUploadState( tags: {}, @@ -37,13 +35,13 @@ class DocumentUploadCubit extends Cubit { documentTypes: {}, ), ) { - _subs.add(_tagRepository.labels.listen( + _subs.add(_tagRepository.values.listen( (tags) => emit(state.copyWith(tags: tags)), )); - _subs.add(_correspondentRepository.labels.listen( + _subs.add(_correspondentRepository.values.listen( (correspondents) => emit(state.copyWith(correspondents: correspondents)), )); - _subs.add(_documentTypeRepository.labels.listen( + _subs.add(_documentTypeRepository.values.listen( (documentTypes) => emit(state.copyWith(documentTypes: documentTypes)), )); } diff --git a/lib/features/documents/view/widgets/search/document_filter_panel.dart b/lib/features/documents/view/widgets/search/document_filter_panel.dart index 221031d..ba4942f 100644 --- a/lib/features/documents/view/widgets/search/document_filter_panel.dart +++ b/lib/features/documents/view/widgets/search/document_filter_panel.dart @@ -1,7 +1,5 @@ -import 'dart:developer' as dev; import 'dart:math'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; @@ -14,7 +12,6 @@ import 'package:paperless_mobile/features/labels/bloc/label_state.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.dart'; -import 'package:paperless_mobile/util.dart'; enum DateRangeSelection { before, after } diff --git a/lib/features/edit_document/cubit/edit_document_cubit.dart b/lib/features/edit_document/cubit/edit_document_cubit.dart index 565fb0d..4db131a 100644 --- a/lib/features/edit_document/cubit/edit_document_cubit.dart +++ b/lib/features/edit_document/cubit/edit_document_cubit.dart @@ -34,26 +34,26 @@ class EditDocumentCubit extends Cubit { super( EditDocumentState( document: document, - correspondents: correspondentRepository.current, - documentTypes: documentTypeRepository.current, - storagePaths: storagePathRepository.current, - tags: tagRepository.current, + correspondents: correspondentRepository.current ?? {}, + documentTypes: documentTypeRepository.current ?? {}, + storagePaths: storagePathRepository.current ?? {}, + tags: tagRepository.current ?? {}, ), ) { _subscriptions.add( - _correspondentRepository.labels + _correspondentRepository.values .listen((v) => emit(state.copyWith(correspondents: v))), ); _subscriptions.add( - _documentTypeRepository.labels + _documentTypeRepository.values .listen((v) => emit(state.copyWith(documentTypes: v))), ); _subscriptions.add( - _storagePathRepository.labels + _storagePathRepository.values .listen((v) => emit(state.copyWith(storagePaths: v))), ); _subscriptions.add( - _tagRepository.labels.listen( + _tagRepository.values.listen( (v) => emit(state.copyWith(tags: v)), ), ); diff --git a/lib/features/edit_label/cubit/edit_label_cubit.dart b/lib/features/edit_label/cubit/edit_label_cubit.dart index cb75902..f18f029 100644 --- a/lib/features/edit_label/cubit/edit_label_cubit.dart +++ b/lib/features/edit_label/cubit/edit_label_cubit.dart @@ -8,13 +8,14 @@ import 'package:paperless_mobile/features/edit_label/cubit/edit_label_state.dart class EditLabelCubit extends Cubit> { final LabelRepository _repository; - StreamSubscription>? _subscription; + StreamSubscription?>? _subscription; EditLabelCubit(LabelRepository repository) : _repository = repository, super(const EditLabelInitial()) { - _subscription = _repository.labels - .listen((labels) => emit(EditLabelState(labels: labels))); + _subscription = repository.values.listen( + (update) => emit(EditLabelState(labels: update ?? {})), + ); } Future create(T label) => _repository.create(label); diff --git a/lib/features/edit_label/view/edit_label_page.dart b/lib/features/edit_label/view/edit_label_page.dart index c901507..ca53585 100644 --- a/lib/features/edit_label/view/edit_label_page.dart +++ b/lib/features/edit_label/view/edit_label_page.dart @@ -1,3 +1,4 @@ +import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; @@ -5,6 +6,7 @@ import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart'; import 'package:paperless_mobile/features/edit_label/view/label_form.dart'; import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:paperless_mobile/util.dart'; class EditLabelPage extends StatelessWidget { final T label; @@ -91,7 +93,8 @@ class EditLabelForm extends StatelessWidget { }, child: Text( S.of(context).genericActionDeleteLabel, - style: TextStyle(color: Theme.of(context).errorColor), + style: + TextStyle(color: Theme.of(context).colorScheme.error), ), ), ], @@ -99,7 +102,13 @@ class EditLabelForm extends StatelessWidget { ) ?? false; if (shouldDelete) { - context.read>().delete(label); + try { + context.read>().delete(label); + } on PaperlessServerException catch (error) { + showErrorMessage(context, error); + } catch (error) { + print(error); + } Navigator.pop(context); } } else { diff --git a/lib/features/edit_label/view/label_form.dart b/lib/features/edit_label/view/label_form.dart index 3bb1bb4..943c02d 100644 --- a/lib/features/edit_label/view/label_form.dart +++ b/lib/features/edit_label/view/label_form.dart @@ -123,8 +123,6 @@ class _LabelFormState extends State> { Navigator.pop(context, createdLabel); } on PaperlessServerException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); - } on DioError catch (error) { - setState(() => _errors = error.error as PaperlessValidationErrors); } } } diff --git a/lib/features/home/view/widget/info_drawer.dart b/lib/features/home/view/widget/info_drawer.dart index 2c83dd5..698c883 100644 --- a/lib/features/home/view/widget/info_drawer.dart +++ b/lib/features/home/view/widget/info_drawer.dart @@ -30,6 +30,14 @@ class InfoDrawer extends StatefulWidget { State createState() => _InfoDrawerState(); } +enum NavigationDestinations { + inbox, + settings, + reportBug, + about, + logout; +} + class _InfoDrawerState extends State { late final Future _packageInfo; @@ -41,6 +49,59 @@ class _InfoDrawerState extends State { @override Widget build(BuildContext context) { + final listtTileShape = RoundedRectangleBorder( + borderRadius: BorderRadius.circular(32), + ); + // return NavigationDrawer( + // selectedIndex: -1, + // children: [ + // Text( + // "", + // style: Theme.of(context).textTheme.titleSmall, + // ).padded(16), + // NavigationDrawerDestination( + // icon: const Icon(Icons.inbox), + // label: Text(S.of(context).bottomNavInboxPageLabel), + // ), + // NavigationDrawerDestination( + // icon: const Icon(Icons.settings), + // label: Text(S.of(context).appDrawerSettingsLabel), + // ), + // const Divider( + // indent: 16, + // ), + // NavigationDrawerDestination( + // icon: const Icon(Icons.bug_report), + // label: Text(S.of(context).appDrawerReportBugLabel), + // ), + // NavigationDrawerDestination( + // icon: const Icon(Icons.info_outline), + // label: Text(S.of(context).appDrawerAboutLabel), + // ), + // ], + // onDestinationSelected: (idx) { + // final val = NavigationDestinations.values[idx - 1]; + // switch (val) { + // case NavigationDestinations.inbox: + // _onOpenInbox(); + // break; + // case NavigationDestinations.settings: + // _onOpenSettings(); + // break; + // case NavigationDestinations.reportBug: + // launchUrlString( + // 'https://github.com/astubenbord/paperless-mobile/issues/new', + // ); + // break; + // case NavigationDestinations.about: + // _onShowAboutDialog(); + // break; + // case NavigationDestinations.logout: + // _onLogout(); + // break; + // } + // }, + // ); return ClipRRect( borderRadius: const BorderRadius.only( topRight: Radius.circular(16.0), @@ -146,10 +207,12 @@ class _InfoDrawerState extends State { ListTile( title: Text(S.of(context).bottomNavInboxPageLabel), leading: const Icon(Icons.inbox), - onTap: () => _onOpenInbox(context), + onTap: () => _onOpenInbox(), + shape: listtTileShape, ), ListTile( leading: const Icon(Icons.settings), + shape: listtTileShape, title: Text( S.of(context).appDrawerSettingsLabel, ), @@ -169,71 +232,45 @@ class _InfoDrawerState extends State { launchUrlString( 'https://github.com/astubenbord/paperless-mobile/issues/new'); }, + shape: listtTileShape, + ), + ListTile( + title: Text(S.of(context).appDrawerAboutLabel), + leading: Icon(Icons.info_outline_rounded), + onTap: _onShowAboutDialog, + shape: listtTileShape, ), - FutureBuilder( - future: _packageInfo, - builder: (context, snapshot) { - return AboutListTile( - icon: const Icon(Icons.info), - applicationIcon: const ImageIcon( - AssetImage('assets/logos/paperless_logo_green.png'), - ), - applicationName: 'Paperless Mobile', - applicationVersion: (snapshot.data?.version ?? '') + - '+' + - (snapshot.data?.buildNumber ?? ''), - aboutBoxChildren: [ - Text( - '${S.of(context).aboutDialogDevelopedByText} Anton Stubenbord'), - Link( - uri: Uri.parse( - 'https://github.com/astubenbord/paperless-mobile'), - builder: (context, followLink) => GestureDetector( - onTap: followLink, - child: Text( - 'https://github.com/astubenbord/paperless-mobile', - style: TextStyle( - color: - Theme.of(context).colorScheme.tertiary), - ), - ), - ), - const SizedBox(height: 16), - Text( - 'Credits', - style: Theme.of(context).textTheme.titleMedium, - ), - _buildOnboardingImageCredits(), - ], - child: Text(S.of(context).appDrawerAboutLabel), - ); - }), ListTile( leading: const Icon(Icons.logout), title: Text(S.of(context).appDrawerLogoutLabel), + shape: listtTileShape, onTap: () { - try { - context.read().logout(); - context.read().clear(); - context.read().clear(); - context.read>().clear(); - context.read>().clear(); - context.read>().clear(); - context.read>().clear(); - context.read().clear(); - } on PaperlessServerException catch (error, stackTrace) { - showErrorMessage(context, error, stackTrace); - } + _onLogout(); }, ) - ].expand((element) => [element, const Divider()]), + ], ], ), ), ); } - Future _onOpenInbox(BuildContext context) async { + void _onLogout() { + try { + context.read().logout(); + context.read().clear(); + context.read().clear(); + context.read>().clear(); + context.read>().clear(); + context.read>().clear(); + context.read>().clear(); + context.read().clear(); + } on PaperlessServerException catch (error, stackTrace) { + showErrorMessage(context, error, stackTrace); + } + } + + Future _onOpenInbox() async { await Navigator.of(context).push( MaterialPageRoute( builder: (_) => LabelRepositoriesProvider( @@ -251,6 +288,17 @@ class _InfoDrawerState extends State { widget.afterInboxClosed?.call(); } + void _onOpenSettings() { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => BlocProvider.value( + value: context.read(), + child: const SettingsPage(), + ), + ), + ); + } + Link _buildOnboardingImageCredits() { return Link( uri: Uri.parse( @@ -270,4 +318,35 @@ class _InfoDrawerState extends State { ), ); } + + Future _onShowAboutDialog() async { + final snapshot = await _packageInfo; + showAboutDialog( + context: context, + applicationIcon: const ImageIcon( + AssetImage('assets/logos/paperless_logo_green.png'), + ), + applicationName: 'Paperless Mobile', + applicationVersion: snapshot.version + '+' + snapshot.buildNumber, + children: [ + Text('${S.of(context).aboutDialogDevelopedByText} Anton Stubenbord'), + Link( + uri: Uri.parse('https://github.com/astubenbord/paperless-mobile'), + builder: (context, followLink) => GestureDetector( + onTap: followLink, + child: Text( + 'https://github.com/astubenbord/paperless-mobile', + style: TextStyle(color: Theme.of(context).colorScheme.tertiary), + ), + ), + ), + const SizedBox(height: 16), + Text( + 'Credits', + style: Theme.of(context).textTheme.titleMedium, + ), + _buildOnboardingImageCredits(), + ], + ); + } } diff --git a/lib/features/inbox/view/pages/inbox_page.dart b/lib/features/inbox/view/pages/inbox_page.dart index 2e818f7..1f73517 100644 --- a/lib/features/inbox/view/pages/inbox_page.dart +++ b/lib/features/inbox/view/pages/inbox_page.dart @@ -136,7 +136,7 @@ class _InboxPageState extends State { child: Text( S.of(context).inboxPageUsageHintText, textAlign: TextAlign.center, - style: Theme.of(context).textTheme.caption, + style: Theme.of(context).textTheme.bodySmall, ).padded(), ), ...slivers diff --git a/lib/features/inbox/view/widgets/inbox_item.dart b/lib/features/inbox/view/widgets/inbox_item.dart index 2759b54..0e2865d 100644 --- a/lib/features/inbox/view/widgets/inbox_item.dart +++ b/lib/features/inbox/view/widgets/inbox_item.dart @@ -7,7 +7,6 @@ import 'package:paperless_mobile/features/document_details/bloc/document_details import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart'; -import 'package:provider/provider.dart'; class InboxItem extends StatelessWidget { static const _a4AspectRatio = 1 / 1.4142; diff --git a/lib/features/labels/bloc/label_cubit.dart b/lib/features/labels/bloc/label_cubit.dart index 859d13c..069a155 100644 --- a/lib/features/labels/bloc/label_cubit.dart +++ b/lib/features/labels/bloc/label_cubit.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; @@ -13,11 +12,17 @@ class LabelCubit extends Cubit> { LabelCubit(LabelRepository repository) : _repository = repository, - super(LabelState(labels: repository.current, isLoaded: true)) { - _subscription = _repository.labels.listen( - (update) => emit( - LabelState(isLoaded: true, labels: update), - ), + super(LabelState( + isLoaded: repository.isInitialized, + labels: repository.current ?? {}, + )) { + _subscription = _repository.values.listen( + (update) { + if (update == null) { + emit(LabelState()); + } + emit(LabelState(isLoaded: true, labels: update!)); + }, ); } diff --git a/lib/features/labels/bloc/label_state.dart b/lib/features/labels/bloc/label_state.dart index 5f7c860..3cd0f63 100644 --- a/lib/features/labels/bloc/label_state.dart +++ b/lib/features/labels/bloc/label_state.dart @@ -6,8 +6,8 @@ class LabelState { final Map labels; LabelState({ - required this.isLoaded, - required this.labels, + this.isLoaded = false, + this.labels = const {}, }); T? getLabel(int? key) { diff --git a/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart b/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart index 432b322..6a04f9d 100644 --- a/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart +++ b/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart @@ -1,11 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; -import 'package:paperless_mobile/features/labels/bloc/providers/correspondent_bloc_provider.dart'; import 'package:paperless_mobile/features/labels/bloc/label_state.dart'; -import 'package:paperless_mobile/util.dart'; +import 'package:paperless_mobile/features/labels/bloc/providers/correspondent_bloc_provider.dart'; class CorrespondentWidget extends StatelessWidget { final int? correspondentId; diff --git a/lib/features/labels/document_type/view/widgets/document_type_widget.dart b/lib/features/labels/document_type/view/widgets/document_type_widget.dart index 9987b72..018622a 100644 --- a/lib/features/labels/document_type/view/widgets/document_type_widget.dart +++ b/lib/features/labels/document_type/view/widgets/document_type_widget.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; import 'package:paperless_mobile/features/labels/bloc/label_state.dart'; import 'package:paperless_mobile/features/labels/bloc/providers/document_type_bloc_provider.dart'; diff --git a/lib/features/labels/storage_path/view/widgets/storage_path_autofill_form_builder_field.dart b/lib/features/labels/storage_path/view/widgets/storage_path_autofill_form_builder_field.dart index c8653a5..6d48559 100644 --- a/lib/features/labels/storage_path/view/widgets/storage_path_autofill_form_builder_field.dart +++ b/lib/features/labels/storage_path/view/widgets/storage_path_autofill_form_builder_field.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/src/widgets/framework.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:paperless_mobile/generated/l10n.dart'; diff --git a/lib/features/labels/storage_path/view/widgets/storage_path_widget.dart b/lib/features/labels/storage_path/view/widgets/storage_path_widget.dart index cc524a4..9adfd92 100644 --- a/lib/features/labels/storage_path/view/widgets/storage_path_widget.dart +++ b/lib/features/labels/storage_path/view/widgets/storage_path_widget.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; import 'package:paperless_mobile/features/labels/bloc/label_state.dart'; import 'package:paperless_mobile/features/labels/bloc/providers/storage_path_bloc_provider.dart'; diff --git a/lib/features/labels/view/pages/labels_page.dart b/lib/features/labels/view/pages/labels_page.dart index f4f9c6e..a5cb0b3 100644 --- a/lib/features/labels/view/pages/labels_page.dart +++ b/lib/features/labels/view/pages/labels_page.dart @@ -13,7 +13,6 @@ import 'package:paperless_mobile/features/edit_label/view/impl/edit_document_typ import 'package:paperless_mobile/features/edit_label/view/impl/edit_storage_path_page.dart'; import 'package:paperless_mobile/features/edit_label/view/impl/edit_tag_page.dart'; import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart'; -import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; import 'package:paperless_mobile/features/labels/bloc/providers/correspondent_bloc_provider.dart'; import 'package:paperless_mobile/features/labels/bloc/providers/document_type_bloc_provider.dart'; import 'package:paperless_mobile/features/labels/bloc/providers/storage_path_bloc_provider.dart'; diff --git a/lib/features/labels/view/widgets/label_item.dart b/lib/features/labels/view/widgets/label_item.dart index a0b0bdf..7e74a3d 100644 --- a/lib/features/labels/view/widgets/label_item.dart +++ b/lib/features/labels/view/widgets/label_item.dart @@ -3,7 +3,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/features/linked_documents_preview/bloc/linked_documents_cubit.dart'; import 'package:paperless_mobile/features/linked_documents_preview/view/pages/linked_documents_page.dart'; -import 'package:provider/provider.dart'; class LabelItem extends StatelessWidget { final T label; diff --git a/lib/features/labels/view/widgets/label_tab_view.dart b/lib/features/labels/view/widgets/label_tab_view.dart index 035274c..f1d480c 100644 --- a/lib/features/labels/view/widgets/label_tab_view.dart +++ b/lib/features/labels/view/widgets/label_tab_view.dart @@ -2,7 +2,6 @@ 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/repository/label_repository.dart'; import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; import 'package:paperless_mobile/core/widgets/offline_widget.dart'; import 'package:paperless_mobile/features/labels/bloc/label_state.dart'; @@ -38,12 +37,12 @@ class LabelTabView extends StatelessWidget { @override Widget build(BuildContext context) { return BlocBuilder( - builder: (context, state) { - if (state == ConnectivityState.notConnected) { - return const OfflineWidget(); - } + builder: (context, connectivityState) { return BlocBuilder, LabelState>( builder: (context, state) { + if (!state.isLoaded && !connectivityState.isConnected) { + return const OfflineWidget(); + } final labels = state.labels.values.toList()..sort(); if (labels.isEmpty) { return Center( @@ -57,13 +56,15 @@ class LabelTabView extends StatelessWidget { TextButton( onPressed: onAddNew, child: Text(emptyStateActionButtonLabel), - ) + ), ].padded(), ), ); } return RefreshIndicator( onRefresh: context.read>().reload, + notificationPredicate: (notification) => + connectivityState.isConnected, child: ListView( children: labels .map( diff --git a/lib/features/linked_documents_preview/view/pages/linked_documents_page.dart b/lib/features/linked_documents_preview/view/pages/linked_documents_page.dart index 25c144c..f4ea7eb 100644 --- a/lib/features/linked_documents_preview/view/pages/linked_documents_page.dart +++ b/lib/features/linked_documents_preview/view/pages/linked_documents_page.dart @@ -9,7 +9,6 @@ import 'package:paperless_mobile/features/documents/view/widgets/list/document_l import 'package:paperless_mobile/features/linked_documents_preview/bloc/linked_documents_cubit.dart'; import 'package:paperless_mobile/features/linked_documents_preview/bloc/state/linked_documents_state.dart'; import 'package:paperless_mobile/generated/l10n.dart'; -import 'package:provider/provider.dart'; class LinkedDocumentsPage extends StatefulWidget { const LinkedDocumentsPage({super.key}); diff --git a/lib/features/login/bloc/authentication_cubit.dart b/lib/features/login/bloc/authentication_cubit.dart index d0cbee7..9508a1a 100644 --- a/lib/features/login/bloc/authentication_cubit.dart +++ b/lib/features/login/bloc/authentication_cubit.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/security/authentication_aware_dio_manager.dart'; @@ -27,45 +25,32 @@ class AuthenticationCubit extends Cubit ClientCertificate? clientCertificate, }) async { assert(credentials.username != null && credentials.password != null); - try { - print(_dioWrapper.client.hashCode); - _dioWrapper.updateSettings( - baseUrl: serverUrl, - clientCertificate: clientCertificate, - ); - final token = await _authApi.login( - username: credentials.username!, - password: credentials.password!, - ); + _dioWrapper.updateSettings( + baseUrl: serverUrl, + clientCertificate: clientCertificate, + ); + final token = await _authApi.login( + username: credentials.username!, + password: credentials.password!, + ); - _dioWrapper.updateSettings( - baseUrl: serverUrl, - clientCertificate: clientCertificate, - authToken: token, - ); + _dioWrapper.updateSettings( + baseUrl: serverUrl, + clientCertificate: clientCertificate, + authToken: token, + ); - emit( - AuthenticationState( - wasLoginStored: false, - authentication: AuthenticationInformation( - serverUrl: serverUrl, - clientCertificate: clientCertificate, - token: token, - ), + emit( + AuthenticationState( + wasLoginStored: false, + authentication: AuthenticationInformation( + serverUrl: serverUrl, + clientCertificate: clientCertificate, + token: token, ), - ); - } on TlsException catch (_) { - const error = PaperlessServerException( - ErrorCode.invalidClientCertificateConfiguration); - throw error; - } on SocketException catch (err) { - if (err.message.contains("connection timed out")) { - throw const PaperlessServerException(ErrorCode.requestTimedOut); - } else { - throw const PaperlessServerException.unknown(); - } - } + ), + ); } /// diff --git a/lib/features/login/model/reachability_status.dart b/lib/features/login/model/reachability_status.dart new file mode 100644 index 0000000..c34b803 --- /dev/null +++ b/lib/features/login/model/reachability_status.dart @@ -0,0 +1,8 @@ +enum ReachabilityStatus { + unknown, + reachable, + notReachable, + unknownHost, + missingClientCertificate, + invalidClientCertificateConfiguration, +} diff --git a/lib/features/login/view/login_page.dart b/lib/features/login/view/login_page.dart index fa4656d..0820e60 100644 --- a/lib/features/login/view/login_page.dart +++ b/lib/features/login/view/login_page.dart @@ -2,14 +2,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart'; import 'package:paperless_mobile/features/login/view/widgets/client_certificate_form_field.dart'; import 'package:paperless_mobile/features/login/view/widgets/server_address_form_field.dart'; +import 'package:paperless_mobile/features/login/view/widgets/server_connection_page.dart'; import 'package:paperless_mobile/features/login/view/widgets/user_credentials_form_field.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/util.dart'; +import 'widgets/never_scrollable_scroll_behavior.dart'; +import 'widgets/server_login_page.dart'; + class LoginPage extends StatefulWidget { const LoginPage({Key? key}) : super(key: key); @@ -20,53 +23,73 @@ class LoginPage extends StatefulWidget { class _LoginPageState extends State { final _formKey = GlobalKey(); - bool _isLoginLoading = false; + final PageController _pageController = PageController(); @override Widget build(BuildContext context) { return Scaffold( - resizeToAvoidBottomInset: true, - appBar: AppBar( - title: Text(S.of(context).loginPageTitle), - bottom: _isLoginLoading - ? const PreferredSize( - preferredSize: Size(double.infinity, 4), - child: LinearProgressIndicator(), - ) - : null, - ), - body: Padding( - padding: const EdgeInsets.all(8.0), - child: FormBuilder( - key: _formKey, - child: ListView( - children: [ - const ServerAddressFormField().padded(), - const UserCredentialsFormField(), - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.only(top: 16.0), - child: Text( - S.of(context).loginPageAdvancedLabel, - style: Theme.of(context).textTheme.bodyLarge, - ).padded(), - ), - ), - const ClientCertificateFormField(), - LayoutBuilder(builder: (context, constraints) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: SizedBox( - width: constraints.maxWidth, - child: _buildLoginButton(), - ), - ); - }), - ], - ), + resizeToAvoidBottomInset: false, // appBar: AppBar( + // title: Text(S.of(context).loginPageTitle), + // bottom: _isLoginLoading + // ? const PreferredSize( + // preferredSize: Size(double.infinity, 4), + // child: LinearProgressIndicator(), + // ) + // : null, + // ), + body: FormBuilder( + key: _formKey, + child: PageView( + controller: _pageController, + scrollBehavior: NeverScrollableScrollBehavior(), + children: [ + ServerConnectionPage( + formBuilderKey: _formKey, + onContinue: () { + _pageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut); + }, + ), + ServerLoginPage( + formBuilderKey: _formKey, + onDone: _login, + ), + ], ), ), + // Padding( + // padding: const EdgeInsets.all(8.0), + // child: FormBuilder( + // key: _formKey, + // child: ListView( + // children: [ + // const ServerAddressFormField().padded(), + // const UserCredentialsFormField(), + // Align( + // alignment: Alignment.centerLeft, + // child: Padding( + // padding: const EdgeInsets.only(top: 16.0), + // child: Text( + // S.of(context).loginPageAdvancedLabel, + // style: Theme.of(context).textTheme.bodyLarge, + // ).padded(), + // ), + // ), + // const ClientCertificateFormField(), + // LayoutBuilder(builder: (context, constraints) { + // return Padding( + // padding: const EdgeInsets.all(8.0), + // child: SizedBox( + // width: constraints.maxWidth, + // child: _buildLoginButton(), + // ), + // ); + // }), + // ], + // ), + // ), + // ), ); } @@ -89,7 +112,6 @@ class _LoginPageState extends State { void _login() async { FocusScope.of(context).unfocus(); if (_formKey.currentState?.saveAndValidate() ?? false) { - setState(() => _isLoginLoading = true); final form = _formKey.currentState!.value; try { await context.read().login( @@ -104,9 +126,7 @@ class _LoginPageState extends State { showGenericError(context, error.values.first, stackTrace); } catch (unknownError, stackTrace) { showGenericError(context, unknownError.toString(), stackTrace); - } finally { - setState(() => _isLoginLoading = false); - } + } finally {} } } } diff --git a/lib/features/login/view/widgets/client_certificate_form_field.dart b/lib/features/login/view/widgets/client_certificate_form_field.dart index 0768333..6547c03 100644 --- a/lib/features/login/view/widgets/client_certificate_form_field.dart +++ b/lib/features/login/view/widgets/client_certificate_form_field.dart @@ -10,7 +10,12 @@ import 'package:paperless_mobile/generated/l10n.dart'; class ClientCertificateFormField extends StatefulWidget { static const fkClientCertificate = 'clientCertificate'; - const ClientCertificateFormField({Key? key}) : super(key: key); + + final void Function(ClientCertificate? cert) onChanged; + const ClientCertificateFormField({ + Key? key, + required this.onChanged, + }) : super(key: key); @override State createState() => @@ -19,11 +24,13 @@ class ClientCertificateFormField extends StatefulWidget { class _ClientCertificateFormFieldState extends State { + RestorableString? _selectedFilePath; File? _selectedFile; @override Widget build(BuildContext context) { return FormBuilderField( key: const ValueKey('login-client-cert'), + onChanged: widget.onChanged, initialValue: null, validator: (value) { if (value == null) { @@ -38,54 +45,59 @@ class _ClientCertificateFormFieldState return null; }, builder: (field) { - return ExpansionTile( - title: Text(S.of(context).loginPageClientCertificateSettingLabel), - subtitle: Text( - S.of(context).loginPageClientCertificateSettingDescriptionText), - children: [ - InputDecorator( - decoration: InputDecoration( - errorText: field.errorText, - border: InputBorder.none, - ), - child: Column( - children: [ - ListTile( - leading: ElevatedButton( - onPressed: () => _onSelectFile(field), - child: Text(S.of(context).genericActionSelectText), - ), - title: _buildSelectedFileText(field), - trailing: AbsorbPointer( - absorbing: field.value == null, - child: _selectedFile != null - ? IconButton( - icon: const Icon(Icons.close), - onPressed: () => setState(() { - _selectedFile = null; - field.didChange(null); - }), - ) - : null, - ), - ), - if (_selectedFile != null) ...[ - ObscuredInputTextFormField( - key: const ValueKey('login-client-cert-passphrase'), - initialValue: field.value?.passphrase, - onChanged: (value) => field.didChange( - field.value?.copyWith(passphrase: value), + final theme = + Theme.of(context).copyWith(dividerColor: Colors.transparent); //new + return Theme( + data: theme, + child: ExpansionTile( + title: Text(S.of(context).loginPageClientCertificateSettingLabel), + subtitle: Text( + S.of(context).loginPageClientCertificateSettingDescriptionText), + children: [ + InputDecorator( + decoration: InputDecoration( + errorText: field.errorText, + border: InputBorder.none, + ), + child: Column( + children: [ + ListTile( + leading: ElevatedButton( + onPressed: () => _onSelectFile(field), + child: Text(S.of(context).genericActionSelectText), ), - label: S - .of(context) - .loginPageClientCertificatePassphraseLabel, - ).padded(), - ] else - ...[] - ], + title: _buildSelectedFileText(field), + trailing: AbsorbPointer( + absorbing: field.value == null, + child: _selectedFile != null + ? IconButton( + icon: const Icon(Icons.close), + onPressed: () => setState(() { + _selectedFile = null; + field.didChange(null); + }), + ) + : null, + ), + ), + if (_selectedFile != null) ...[ + ObscuredInputTextFormField( + key: const ValueKey('login-client-cert-passphrase'), + initialValue: field.value?.passphrase, + onChanged: (value) => field.didChange( + field.value?.copyWith(passphrase: value), + ), + label: S + .of(context) + .loginPageClientCertificatePassphraseLabel, + ).padded(), + ] else + ...[] + ], + ), ), - ), - ], + ], + ), ); }, name: ClientCertificateFormField.fkClientCertificate, diff --git a/lib/features/login/view/widgets/never_scrollable_scroll_behavior.dart b/lib/features/login/view/widgets/never_scrollable_scroll_behavior.dart new file mode 100644 index 0000000..bfdc0fd --- /dev/null +++ b/lib/features/login/view/widgets/never_scrollable_scroll_behavior.dart @@ -0,0 +1,8 @@ +import 'package:flutter/widgets.dart'; + +class NeverScrollableScrollBehavior extends ScrollBehavior { + @override + ScrollPhysics getScrollPhysics(BuildContext context) { + return const NeverScrollableScrollPhysics(); + } +} diff --git a/lib/features/login/view/widgets/server_address_form_field.dart b/lib/features/login/view/widgets/server_address_form_field.dart index 1f16812..db34df7 100644 --- a/lib/features/login/view/widgets/server_address_form_field.dart +++ b/lib/features/login/view/widgets/server_address_form_field.dart @@ -1,15 +1,18 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; -import 'package:paperless_mobile/core/service/connectivity_status.service.dart'; -import 'package:paperless_mobile/generated/l10n.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; +import 'package:paperless_mobile/core/service/connectivity_status_service.dart'; +import 'package:paperless_mobile/features/login/model/client_certificate.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; import 'package:provider/provider.dart'; class ServerAddressFormField extends StatefulWidget { static const String fkServerAddress = "serverAddress"; + + final void Function(String address) onDone; const ServerAddressFormField({ Key? key, + required this.onDone, }) : super(key: key); @override @@ -17,13 +20,7 @@ class ServerAddressFormField extends StatefulWidget { } class _ServerAddressFormFieldState extends State { - static const _ipv4Regex = r"((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}"; - static const _ipv6Regex = - r"(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))"; - static final _urlRegex = RegExp( - r"^(https?:\/\/)(([\da-z\.-]+)\.([a-z\.]{2,6})|(((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4})|((([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))))(:\d{1,5})?([\/\w \.-]*)*\/?$"); final TextEditingController _textEditingController = TextEditingController(); - ReachabilityStatus _reachabilityStatus = ReachabilityStatus.undefined; @override Widget build(BuildContext context) { @@ -31,75 +28,25 @@ class _ServerAddressFormFieldState extends State { key: const ValueKey('login-server-address'), controller: _textEditingController, name: ServerAddressFormField.fkServerAddress, - validator: FormBuilderValidators.compose( - [ - FormBuilderValidators.required( - errorText: - S.of(context).loginPageServerUrlValidatorMessageRequiredText, - ), - FormBuilderValidators.match( - _urlRegex.pattern, - errorText: S - .of(context) - .loginPageServerUrlValidatorMessageInvalidAddressText, - ), - ], + validator: FormBuilderValidators.required( + errorText: S.of(context).loginPageServerUrlValidatorMessageRequiredText, ), decoration: InputDecoration( - suffixIcon: _buildIsReachableIcon(), hintText: "http://192.168.1.50:8000", labelText: S.of(context).loginPageServerUrlFieldLabel, ), - onChanged: _updateIsAddressReachableStatus, onSubmitted: (value) { if (value == null) return; // Remove trailing slash if it is a valid address. - final address = value.trim(); + String address = value.trim(); + address = _replaceTrailingSlashes(address); _textEditingController.text = address; - if (_urlRegex.hasMatch(address) && address.endsWith("/")) { - _textEditingController.text = address.replaceAll(RegExp(r'\/$'), ''); - } + widget.onDone(address); }, ); } - Widget? _buildIsReachableIcon() { - switch (_reachabilityStatus) { - case ReachabilityStatus.reachable: - return const Icon( - Icons.done, - color: Colors.green, - ); - case ReachabilityStatus.notReachable: - return Icon( - Icons.close, - color: Theme.of(context).colorScheme.error, - ); - case ReachabilityStatus.testing: - return const RefreshProgressIndicator(); - case ReachabilityStatus.undefined: - return null; - } - } - - void _updateIsAddressReachableStatus(String? address) async { - if (address == null || !_urlRegex.hasMatch(address)) { - setState(() { - _reachabilityStatus = ReachabilityStatus.undefined; - }); - return; - } - //https://stackoverflow.com/questions/49648022/check-whether-there-is-an-internet-connection-available-on-flutter-app - setState(() => _reachabilityStatus = ReachabilityStatus.testing); - final isReachable = await context - .read() - .isServerReachable(address.trim()); - setState( - () => _reachabilityStatus = isReachable - ? ReachabilityStatus.reachable - : ReachabilityStatus.notReachable, - ); + String _replaceTrailingSlashes(String src) { + return src.replaceAll(RegExp(r'^\/+|\/+$'), ''); } } - -enum ReachabilityStatus { reachable, notReachable, testing, undefined } diff --git a/lib/features/login/view/widgets/server_connection_page.dart b/lib/features/login/view/widgets/server_connection_page.dart new file mode 100644 index 0000000..11bd994 --- /dev/null +++ b/lib/features/login/view/widgets/server_connection_page.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:paperless_mobile/core/service/connectivity_status_service.dart'; +import 'package:paperless_mobile/core/widgets/paperless_logo.dart'; +import 'package:paperless_mobile/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/features/login/model/reachability_status.dart'; +import 'package:paperless_mobile/features/login/view/widgets/client_certificate_form_field.dart'; +import 'package:paperless_mobile/features/login/view/widgets/server_address_form_field.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:provider/provider.dart'; + +class ServerConnectionPage extends StatefulWidget { + final GlobalKey formBuilderKey; + final void Function() onContinue; + + const ServerConnectionPage({ + super.key, + required this.formBuilderKey, + required this.onContinue, + }); + + @override + State createState() => _ServerConnectionPageState(); +} + +class _ServerConnectionPageState extends State { + ReachabilityStatus _reachabilityStatus = ReachabilityStatus.unknown; + + @override + Widget build(BuildContext context) { + final logoHeight = MediaQuery.of(context).size.width / 2; + return Scaffold( + appBar: AppBar( + title: Text(S.of(context).loginPageTitle), + ), + resizeToAvoidBottomInset: true, + body: Column( + children: [ + ServerAddressFormField( + onDone: (address) { + _updateReachability(); + }, + ).padded(), + ClientCertificateFormField( + onChanged: (_) => _updateReachability(), + ).padded(), + _buildStatusIndicator(), + ], + ).padded(), + bottomNavigationBar: BottomAppBar( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + FilledButton( + child: Text("Continue"), + onPressed: _reachabilityStatus == ReachabilityStatus.reachable + ? widget.onContinue + : null, + ), + ], + ), + ), + ); + } + + Future _updateReachability() async { + final status = await context + .read() + .isPaperlessServerReachable( + widget.formBuilderKey.currentState! + .getRawValue(ServerAddressFormField.fkServerAddress), + widget.formBuilderKey.currentState?.getRawValue( + ClientCertificateFormField.fkClientCertificate, + ), + ); + setState(() => _reachabilityStatus = status); + } + + Widget _buildStatusIndicator() { + Color errorColor = Theme.of(context).colorScheme.error; + switch (_reachabilityStatus) { + case ReachabilityStatus.unknown: + return Container(); + case ReachabilityStatus.reachable: + return _buildIconText( + Icons.done, + "Connection established.", + Colors.green, + ); + case ReachabilityStatus.notReachable: + return _buildIconText( + Icons.close, + "Could not establish a connection to the server.", + errorColor, + ); + case ReachabilityStatus.unknownHost: + return _buildIconText( + Icons.close, + "Host could not be resolved.", + errorColor, + ); + case ReachabilityStatus.missingClientCertificate: + return _buildIconText( + Icons.close, + "A client certificate was expected but not sent. Please provide a certificate.", + errorColor, + ); + case ReachabilityStatus.invalidClientCertificateConfiguration: + return _buildIconText( + Icons.close, + "Incorrect or missing client certificate passphrase.", + errorColor, + ); + } + } + + Widget _buildIconText( + IconData icon, + String text, [ + Color? color, + ]) { + return ListTile( + title: Text( + text, + style: Theme.of(context).textTheme.bodySmall?.copyWith(color: color), + ), + leading: Icon( + icon, + color: color, + ), + ); + } +} diff --git a/lib/features/login/view/widgets/server_login_page.dart b/lib/features/login/view/widgets/server_login_page.dart new file mode 100644 index 0000000..2459dc2 --- /dev/null +++ b/lib/features/login/view/widgets/server_login_page.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:paperless_mobile/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/features/login/view/widgets/server_address_form_field.dart'; +import 'package:paperless_mobile/features/login/view/widgets/user_credentials_form_field.dart'; + +class ServerLoginPage extends StatefulWidget { + final VoidCallback onDone; + final GlobalKey formBuilderKey; + const ServerLoginPage({ + super.key, + required this.onDone, + required this.formBuilderKey, + }); + + @override + State createState() => _ServerLoginPageState(); +} + +class _ServerLoginPageState extends State { + @override + Widget build(BuildContext context) { + final serverAddress = (widget.formBuilderKey.currentState + ?.getRawValue(ServerAddressFormField.fkServerAddress) as String?) + ?.replaceAll(RegExp(r'https?://'), ''); + return Scaffold( + appBar: AppBar( + title: Text("Sign In"), + ), + body: ListView( + children: [ + Text("Sign in to $serverAddress").padded(), + UserCredentialsFormField(), + ], + ), + bottomNavigationBar: BottomAppBar( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + FilledButton( + onPressed: widget.onDone, + child: Text("Sign In"), + ) + ], + ), + ), + ); + } +} diff --git a/lib/features/saved_view/cubit/saved_view_cubit.dart b/lib/features/saved_view/cubit/saved_view_cubit.dart index 1373f28..7f9940c 100644 --- a/lib/features/saved_view/cubit/saved_view_cubit.dart +++ b/lib/features/saved_view/cubit/saved_view_cubit.dart @@ -10,7 +10,7 @@ class SavedViewCubit extends Cubit { StreamSubscription? _subscription; SavedViewCubit(this._repository) : super(SavedViewState(value: {})) { - _subscription = _repository.savedViews.listen( + _subscription = _repository.values.listen( (savedViews) { if (savedViews == null) { emit(state.copyWith(isLoaded: false)); diff --git a/lib/features/scan/bloc/document_scanner_cubit.dart b/lib/features/scan/bloc/document_scanner_cubit.dart index 87607c4..2d162d1 100644 --- a/lib/features/scan/bloc/document_scanner_cubit.dart +++ b/lib/features/scan/bloc/document_scanner_cubit.dart @@ -4,9 +4,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:injectable/injectable.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart'; class DocumentScannerCubit extends Cubit> { DocumentScannerCubit() : super(const []); diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 028af80..7a11762 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -212,7 +212,7 @@ "@errorMessageCreateSavedViewError": {}, "errorMessageDeleteSavedViewError": "Gespeicherte Ansicht konnte nicht geklöscht werden, bitte versuche es erneut.", "@errorMessageDeleteSavedViewError": {}, - "errorMessageDeviceOffline": "Daten konnten nicht geladen werden: Eine Verbindung zum Internet konnte nicht hergestellt werden.", + "errorMessageDeviceOffline": "Du bist offline. Bitte stelle sicher, dass du mit dem Internet verbunden bist.", "@errorMessageDeviceOffline": {}, "errorMessageDocumentAsnQueryFailed": "Archiv-Seriennummer konnte nicht zugewiesen werden.", "@errorMessageDocumentAsnQueryFailed": {}, diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index b13a85d..abb28ca 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -212,7 +212,7 @@ "@errorMessageCreateSavedViewError": {}, "errorMessageDeleteSavedViewError": "Could not delete saved view, please try again", "@errorMessageDeleteSavedViewError": {}, - "errorMessageDeviceOffline": "Could not fetch data: You are not connected to the internet.", + "errorMessageDeviceOffline": "You are currently offline. Please make sure you are connected to the internet.", "@errorMessageDeviceOffline": {}, "errorMessageDocumentAsnQueryFailed": "Could not assign archive serial number.", "@errorMessageDocumentAsnQueryFailed": {}, diff --git a/lib/main.dart b/lib/main.dart index 09a8c5f..96fbd38 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,4 @@ import 'dart:io'; -import 'dart:math'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart'; @@ -11,7 +10,6 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; -import 'package:hive/hive.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/intl_standalone.dart'; @@ -23,7 +21,6 @@ import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.da import 'package:paperless_mobile/core/global/constants.dart'; import 'package:paperless_mobile/core/interceptor/dio_http_error_interceptor.dart'; import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart'; -import 'package:paperless_mobile/core/interceptor/retry_on_connection_change_interceptor.dart'; import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart'; import 'package:paperless_mobile/core/repository/impl/correspondent_repository_impl.dart'; import 'package:paperless_mobile/core/repository/impl/document_type_repository_impl.dart'; @@ -33,11 +30,10 @@ import 'package:paperless_mobile/core/repository/impl/tag_repository_impl.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; import 'package:paperless_mobile/core/security/authentication_aware_dio_manager.dart'; -import 'package:paperless_mobile/core/service/connectivity_status.service.dart'; +import 'package:paperless_mobile/core/service/connectivity_status_service.dart'; import 'package:paperless_mobile/core/service/dio_file_service.dart'; import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/core/store/local_vault.dart'; -import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/app_intro/application_intro_slideshow.dart'; import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart'; import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart'; @@ -52,9 +48,9 @@ import 'package:paperless_mobile/features/settings/model/application_settings_st import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/util.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:pretty_dio_logger/pretty_dio_logger.dart'; import 'package:provider/provider.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; -import 'package:pretty_dio_logger/pretty_dio_logger.dart'; void main() async { Bloc.observer = BlocChangesObserver(); diff --git a/packages/paperless_api/lib/src/models/paperless_server_exception.dart b/packages/paperless_api/lib/src/models/paperless_server_exception.dart index aa2b6ba..d1ff16b 100644 --- a/packages/paperless_api/lib/src/models/paperless_server_exception.dart +++ b/packages/paperless_api/lib/src/models/paperless_server_exception.dart @@ -15,7 +15,7 @@ class PaperlessServerException implements Exception { @override String toString() { - return "ErrorMessage(code: $code${stackTrace != null ? ', stackTrace: ${stackTrace.toString()}' : ''}${httpStatusCode != null ? ', httpStatusCode: $httpStatusCode' : ''})"; + return "PaperlessServerException(code: $code${stackTrace != null ? ', stackTrace: ${stackTrace.toString()}' : ''}${httpStatusCode != null ? ', httpStatusCode: $httpStatusCode' : ''})"; } } 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 064decd..5533ae0 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 @@ -24,11 +24,8 @@ class PaperlessAuthenticationApiImpl implements PaperlessAuthenticationApi { }, ); } on DioError catch (error) { - if (error.error is ErrorCode) { - throw PaperlessServerException( - error.error, - httpStatusCode: error.response?.statusCode, - ); + if (error.error is PaperlessServerException) { + throw error.error; } else { log(error.message); throw PaperlessServerException( 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 0cde468..6043b4e 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 @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:developer'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; @@ -42,58 +43,74 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { for (final tag in tags) { formData.fields.add(MapEntry('tags', tag.toString())); } + try { + final response = + await client.post('/api/documents/post_document/', data: formData); - final response = - await client.post('/api/documents/post_document/', data: formData); - if (response.statusCode != 200) { - throw PaperlessServerException( - ErrorCode.documentUploadFailed, - httpStatusCode: response.statusCode, - ); + if (response.statusCode != 200) { + throw PaperlessServerException( + ErrorCode.documentUploadFailed, + httpStatusCode: response.statusCode, + ); + } + } on DioError catch (err) { + throw err.error; } } @override Future update(DocumentModel doc) async { - final response = await client.put( - "/api/documents/${doc.id}/", - data: doc.toJson(), - ); - if (response.statusCode == 200) { - return DocumentModel.fromJson(response.data); - } else { - throw const PaperlessServerException(ErrorCode.documentUpdateFailed); + try { + final response = await client.put( + "/api/documents/${doc.id}/", + data: doc.toJson(), + ); + if (response.statusCode == 200) { + return DocumentModel.fromJson(response.data); + } else { + throw const PaperlessServerException(ErrorCode.documentUpdateFailed); + } + } on DioError catch (err) { + throw err.error; } } @override Future> find(DocumentFilter filter) async { final filterParams = filter.toQueryParameters(); - final response = await client.get( - "/api/documents/", - queryParameters: filterParams, - ); - if (response.statusCode == 200) { - return compute( - PagedSearchResult.fromJson, - PagedSearchResultJsonSerializer( - response.data, - DocumentModel.fromJson, - ), + try { + final response = await client.get( + "/api/documents/", + queryParameters: filterParams, ); - } else { - throw const PaperlessServerException(ErrorCode.documentLoadFailed); + if (response.statusCode == 200) { + return compute( + PagedSearchResult.fromJson, + PagedSearchResultJsonSerializer( + response.data, + DocumentModel.fromJson, + ), + ); + } else { + throw const PaperlessServerException(ErrorCode.documentLoadFailed); + } + } on DioError catch (err) { + throw err.error; } } @override Future delete(DocumentModel doc) async { - final response = await client.delete("/api/documents/${doc.id}/"); + try { + final response = await client.delete("/api/documents/${doc.id}/"); - if (response.statusCode == 204) { - return Future.value(doc.id); + if (response.statusCode == 204) { + return Future.value(doc.id); + } + throw const PaperlessServerException(ErrorCode.documentDeleteFailed); + } on DioError catch (err) { + throw err.error; } - throw const PaperlessServerException(ErrorCode.documentDeleteFailed); } @override @@ -107,16 +124,20 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { @override Future getPreview(int documentId) async { - final response = await client.get( - getPreviewUrl(documentId), - options: Options( - responseType: - ResponseType.bytes), //TODO: Check if bytes or stream is required - ); - if (response.statusCode == 200) { - return response.data; + try { + final response = await client.get( + getPreviewUrl(documentId), + options: Options( + responseType: ResponseType + .bytes), //TODO: Check if bytes or stream is required + ); + if (response.statusCode == 200) { + return response.data; + } + throw const PaperlessServerException(ErrorCode.documentPreviewFailed); + } on DioError catch (err) { + throw err.error; } - throw const PaperlessServerException(ErrorCode.documentPreviewFailed); } @override @@ -134,21 +155,29 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { .map((e) => e.archiveSerialNumber) .firstWhere((asn) => asn != null, orElse: () => 0)! + 1; - } on PaperlessServerException catch (_) { + } on PaperlessServerException { throw const PaperlessServerException(ErrorCode.documentAsnQueryFailed); + } on DioError catch (err) { + throw err.error; } } @override Future> bulkAction(BulkAction action) async { - final response = await client.post( - "/api/documents/bulk_edit/", - data: action.toJson(), - ); - if (response.statusCode == 200) { - return action.documentIds; - } else { - throw const PaperlessServerException(ErrorCode.documentBulkActionFailed); + try { + final response = await client.post( + "/api/documents/bulk_edit/", + data: action.toJson(), + ); + if (response.statusCode == 200) { + return action.documentIds; + } else { + throw const PaperlessServerException( + ErrorCode.documentBulkActionFailed, + ); + } + } on DioError catch (err) { + throw err.error; } } @@ -174,53 +203,68 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { @override Future download(DocumentModel document) async { - //TODO: Add missing error handling - final response = await client.get( - "/api/documents/${document.id}/download/", - options: Options(responseType: ResponseType.bytes), - ); - return response.data; + try { + final response = await client.get( + "/api/documents/${document.id}/download/", + options: Options(responseType: ResponseType.bytes), + ); + return response.data; + } on DioError catch (err) { + throw err.error; + } } @override Future getMetaData(DocumentModel document) async { - final response = - await client.get("/api/documents/${document.id}/metadata/"); - return compute( - DocumentMetaData.fromJson, - response.data as Map, - ); + try { + 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; + } } @override Future> autocomplete(String query, [int limit = 10]) async { - final response = await client.get( - '/api/search/autocomplete/', - queryParameters: { - 'query': query, - 'limit': limit, - }, - ); - if (response.statusCode == 200) { - return response.data as List; + try { + final response = await client.get( + '/api/search/autocomplete/', + queryParameters: { + 'query': query, + 'limit': limit, + }, + ); + if (response.statusCode == 200) { + return response.data as List; + } + throw const PaperlessServerException(ErrorCode.autocompleteQueryError); + } on DioError catch (err) { + throw err.error; } - throw const PaperlessServerException(ErrorCode.autocompleteQueryError); } @override Future> findSimilar(int docId) async { - final response = - await client.get("/api/documents/?more_like=$docId&pageSize=10"); - if (response.statusCode == 200) { - return (await compute( - PagedSearchResult.fromJson, - PagedSearchResultJsonSerializer( - response.data, - SimilarDocumentModel.fromJson, - ), - )) - .results; + try { + final response = + await client.get("/api/documents/?more_like=$docId&pageSize=10"); + if (response.statusCode == 200) { + return (await compute( + PagedSearchResult.fromJson, + PagedSearchResultJsonSerializer( + response.data, + SimilarDocumentModel.fromJson, + ), + )) + .results; + } + throw const PaperlessServerException(ErrorCode.similarQueryError); + } on DioError catch (err) { + throw err.error; } - throw const PaperlessServerException(ErrorCode.similarQueryError); } } 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 097e9af..1bff1ca 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 @@ -13,16 +13,16 @@ import 'package:paperless_api/src/request_utils.dart'; //Notes: // Removed content type json header class PaperlessLabelApiImpl implements PaperlessLabelsApi { - final Dio client; + final Dio _client; - PaperlessLabelApiImpl(this.client); + PaperlessLabelApiImpl(this._client); @override Future getCorrespondent(int id) async { return getSingleResult( "/api/correspondents/$id/", Correspondent.fromJson, ErrorCode.correspondentLoadFailed, - client: client, + client: _client, ); } @@ -32,7 +32,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { "/api/tags/$id/", Tag.fromJson, ErrorCode.tagLoadFailed, - client: client, + client: _client, ); } @@ -42,7 +42,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { "/api/tags/?page=1&page_size=100000", Tag.fromJson, ErrorCode.tagLoadFailed, - client: client, + client: _client, minRequiredApiVersion: 2, ); return results @@ -56,7 +56,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { "/api/document_types/$id/", DocumentType.fromJson, ErrorCode.documentTypeLoadFailed, - client: client, + client: _client, ); } @@ -66,7 +66,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { "/api/correspondents/?page=1&page_size=100000", Correspondent.fromJson, ErrorCode.correspondentLoadFailed, - client: client, + client: _client, ); return results @@ -80,7 +80,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { "/api/document_types/?page=1&page_size=100000", DocumentType.fromJson, ErrorCode.documentTypeLoadFailed, - client: client, + client: _client, ); return results @@ -90,151 +90,191 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { @override Future saveCorrespondent(Correspondent correspondent) async { - final response = await client.post( - '/api/correspondents/', - data: correspondent.toJson(), - ); - if (response.statusCode == HttpStatus.created) { - return Correspondent.fromJson(response.data); + try { + final response = await _client.post( + '/api/correspondents/', + data: correspondent.toJson(), + ); + if (response.statusCode == HttpStatus.created) { + return Correspondent.fromJson(response.data); + } + throw PaperlessServerException( + ErrorCode.correspondentCreateFailed, + httpStatusCode: response.statusCode, + ); + } on DioError catch (err) { + throw err.error; } - throw PaperlessServerException( - ErrorCode.correspondentCreateFailed, - httpStatusCode: response.statusCode, - ); } @override Future saveDocumentType(DocumentType type) async { - final response = await client.post( - '/api/document_types/', - data: type.toJson(), - ); - if (response.statusCode == HttpStatus.created) { - return DocumentType.fromJson(response.data); + try { + final response = await _client.post( + '/api/document_types/', + data: type.toJson(), + ); + if (response.statusCode == HttpStatus.created) { + return DocumentType.fromJson(response.data); + } + throw PaperlessServerException( + ErrorCode.documentTypeCreateFailed, + httpStatusCode: response.statusCode, + ); + } on DioError catch (err) { + throw err.error; } - throw PaperlessServerException( - ErrorCode.documentTypeCreateFailed, - httpStatusCode: response.statusCode, - ); } @override Future saveTag(Tag tag) async { - final response = await client.post( - '/api/tags/', - data: tag.toJson(), - options: Options(headers: {"Accept": "application/json; version=2"}), - ); - if (response.statusCode == HttpStatus.created) { - return Tag.fromJson(response.data); + try { + final response = await _client.post( + '/api/tags/', + data: tag.toJson(), + options: Options(headers: {"Accept": "application/json; version=2"}), + ); + if (response.statusCode == HttpStatus.created) { + return Tag.fromJson(response.data); + } + throw PaperlessServerException( + ErrorCode.tagCreateFailed, + httpStatusCode: response.statusCode, + ); + } on DioError catch (err) { + throw err.error; } - throw PaperlessServerException( - ErrorCode.tagCreateFailed, - httpStatusCode: response.statusCode, - ); } @override Future deleteCorrespondent(Correspondent correspondent) async { assert(correspondent.id != null); - final response = - await client.delete('/api/correspondents/${correspondent.id}/'); - if (response.statusCode == HttpStatus.noContent) { - return correspondent.id!; + 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, + ); + } on DioError catch (err) { + throw err.error; } - throw PaperlessServerException( - ErrorCode.unknown, - httpStatusCode: response.statusCode, - ); } @override Future deleteDocumentType(DocumentType documentType) async { assert(documentType.id != null); - final response = - await client.delete('/api/document_types/${documentType.id}/'); - if (response.statusCode == HttpStatus.noContent) { - return documentType.id!; + 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, + ); + } on DioError catch (err) { + throw err.error; } - throw PaperlessServerException( - ErrorCode.unknown, - httpStatusCode: response.statusCode, - ); } @override Future deleteTag(Tag tag) async { assert(tag.id != null); - final response = await client.delete('/api/tags/${tag.id}/'); - if (response.statusCode == HttpStatus.noContent) { - return tag.id!; + 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, + ); + } on DioError catch (err) { + throw err.error; } - throw PaperlessServerException( - ErrorCode.unknown, - httpStatusCode: response.statusCode, - ); } @override Future updateCorrespondent(Correspondent correspondent) async { assert(correspondent.id != null); - final response = await client.put( - '/api/correspondents/${correspondent.id}/', - data: json.encode(correspondent.toJson()), - ); - if (response.statusCode == HttpStatus.ok) { - return Correspondent.fromJson(response.data); + try { + final response = await _client.put( + '/api/correspondents/${correspondent.id}/', + data: json.encode(correspondent.toJson()), + ); + if (response.statusCode == HttpStatus.ok) { + return Correspondent.fromJson(response.data); + } + throw PaperlessServerException( + ErrorCode.unknown, //TODO: Add correct error code mapping. + httpStatusCode: response.statusCode, + ); + } on DioError catch (err) { + throw err.error; } - throw PaperlessServerException( - ErrorCode.unknown, //TODO: Add correct error code mapping. - httpStatusCode: response.statusCode, - ); } @override Future updateDocumentType(DocumentType documentType) async { assert(documentType.id != null); - final response = await client.put( - '/api/document_types/${documentType.id}/', - data: documentType.toJson(), - ); - if (response.statusCode == HttpStatus.ok) { - return DocumentType.fromJson(response.data); + try { + final response = await _client.put( + '/api/document_types/${documentType.id}/', + data: documentType.toJson(), + ); + if (response.statusCode == HttpStatus.ok) { + return DocumentType.fromJson(response.data); + } + throw PaperlessServerException( + ErrorCode.unknown, + httpStatusCode: response.statusCode, + ); + } on DioError catch (err) { + throw err.error; } - throw PaperlessServerException( - ErrorCode.unknown, - httpStatusCode: response.statusCode, - ); } @override Future updateTag(Tag tag) async { assert(tag.id != null); - final response = await client.put( - '/api/tags/${tag.id}/', - options: Options(headers: {"Accept": "application/json; version=2"}), - data: tag.toJson(), - ); - if (response.statusCode == HttpStatus.ok) { - return Tag.fromJson(response.data); + try { + final response = await _client.put( + '/api/tags/${tag.id}/', + options: Options(headers: {"Accept": "application/json; version=2"}), + data: tag.toJson(), + ); + if (response.statusCode == HttpStatus.ok) { + return Tag.fromJson(response.data); + } + throw PaperlessServerException( + ErrorCode.unknown, + httpStatusCode: response.statusCode, + ); + } on DioError catch (err) { + throw err.error; } - throw PaperlessServerException( - ErrorCode.unknown, - httpStatusCode: response.statusCode, - ); } @override Future deleteStoragePath(StoragePath path) async { assert(path.id != null); - final response = await client.delete('/api/storage_paths/${path.id}/'); - if (response.statusCode == HttpStatus.noContent) { - return path.id!; + 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, + ); + } on DioError catch (err) { + throw err.error; } - throw PaperlessServerException( - ErrorCode.unknown, - httpStatusCode: response.statusCode, - ); } @override @@ -243,7 +283,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { "/api/storage_paths/$id/", StoragePath.fromJson, ErrorCode.storagePathLoadFailed, - client: client, + client: _client, ); } @@ -253,7 +293,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { "/api/storage_paths/?page=1&page_size=100000", StoragePath.fromJson, ErrorCode.storagePathLoadFailed, - client: client, + client: _client, ); return results @@ -263,27 +303,37 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { @override Future saveStoragePath(StoragePath path) async { - final response = await client.post( - '/api/storage_paths/', - data: path.toJson(), - ); - if (response.statusCode == HttpStatus.created) { - return StoragePath.fromJson(response.data); + try { + final response = await _client.post( + '/api/storage_paths/', + data: path.toJson(), + ); + if (response.statusCode == HttpStatus.created) { + return StoragePath.fromJson(response.data); + } + throw PaperlessServerException( + ErrorCode.storagePathCreateFailed, + httpStatusCode: response.statusCode, + ); + } on DioError catch (err) { + throw err.error; } - throw PaperlessServerException(ErrorCode.storagePathCreateFailed, - httpStatusCode: response.statusCode); } @override Future updateStoragePath(StoragePath path) async { assert(path.id != null); - final response = await client.put( - '/api/storage_paths/${path.id}/', - data: path.toJson(), - ); - if (response.statusCode == HttpStatus.ok) { - return StoragePath.fromJson(response.data); + try { + final response = await _client.put( + '/api/storage_paths/${path.id}/', + data: path.toJson(), + ); + if (response.statusCode == HttpStatus.ok) { + return StoragePath.fromJson(response.data); + } + throw const PaperlessServerException(ErrorCode.unknown); + } on DioError catch (err) { + throw err.error; } - throw const PaperlessServerException(ErrorCode.unknown); } } 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 66cc30e..21a5693 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,4 +1,3 @@ -import 'dart:convert'; import 'dart:io'; import 'package:dio/dio.dart'; @@ -9,9 +8,9 @@ import 'package:paperless_api/src/request_utils.dart'; import 'paperless_saved_views_api.dart'; class PaperlessSavedViewsApiImpl implements PaperlessSavedViewsApi { - final Dio client; + final Dio _client; - PaperlessSavedViewsApiImpl(this.client); + PaperlessSavedViewsApiImpl(this._client); @override Future> findAll([Iterable? ids]) async { @@ -19,7 +18,7 @@ class PaperlessSavedViewsApiImpl implements PaperlessSavedViewsApi { "/api/saved_views/", SavedView.fromJson, ErrorCode.loadSavedViewsError, - client: client, + client: _client, ); return result.where((view) => ids?.contains(view.id!) ?? true); @@ -27,29 +26,37 @@ class PaperlessSavedViewsApiImpl implements PaperlessSavedViewsApi { @override Future save(SavedView view) async { - final response = await client.post( - "/api/saved_views/", - data: view.toJson(), - ); - if (response.statusCode == HttpStatus.created) { - return SavedView.fromJson(response.data); + try { + final response = await _client.post( + "/api/saved_views/", + data: view.toJson(), + ); + if (response.statusCode == HttpStatus.created) { + return SavedView.fromJson(response.data); + } + throw PaperlessServerException( + ErrorCode.createSavedViewError, + httpStatusCode: response.statusCode, + ); + } on DioError catch (err) { + throw err.error; } - throw PaperlessServerException( - ErrorCode.createSavedViewError, - httpStatusCode: response.statusCode, - ); } @override Future delete(SavedView view) async { - final response = await client.delete("/api/saved_views/${view.id}/"); - if (response.statusCode == HttpStatus.noContent) { - return view.id!; + 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, + ); + } on DioError catch (err) { + throw err.error; } - throw PaperlessServerException( - ErrorCode.deleteSavedViewError, - httpStatusCode: response.statusCode, - ); } @override @@ -58,7 +65,7 @@ class PaperlessSavedViewsApiImpl implements PaperlessSavedViewsApi { "/api/saved_views/$id/", SavedView.fromJson, ErrorCode.loadSavedViewsError, - client: client, + client: _client, ); } } diff --git a/packages/paperless_api/lib/src/request_utils.dart b/packages/paperless_api/lib/src/request_utils.dart index b8f808a..5ff0578 100644 --- a/packages/paperless_api/lib/src/request_utils.dart +++ b/packages/paperless_api/lib/src/request_utils.dart @@ -11,22 +11,26 @@ Future getSingleResult( required Dio client, int minRequiredApiVersion = 1, }) async { - final response = await client.get( - url, - options: Options( - headers: {'accept': 'application/json; version=$minRequiredApiVersion'}, - ), - ); - if (response.statusCode == HttpStatus.ok) { - return compute( - fromJson, - response.data as Map, + try { + final response = await client.get( + url, + options: Options( + headers: {'accept': 'application/json; version=$minRequiredApiVersion'}, + ), ); + if (response.statusCode == HttpStatus.ok) { + return compute( + fromJson, + response.data as Map, + ); + } + throw PaperlessServerException( + errorCode, + httpStatusCode: response.statusCode, + ); + } on DioError catch (err) { + throw err.error; } - throw PaperlessServerException( - errorCode, - httpStatusCode: response.statusCode, - ); } Future> getCollection( @@ -36,30 +40,34 @@ Future> getCollection( required Dio client, int minRequiredApiVersion = 1, }) async { - final response = await client.get( - url, - options: Options(headers: { - 'accept': 'application/json; version=$minRequiredApiVersion' - }), - ); - 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>()), - ); + try { + final response = await client.get( + url, + options: Options(headers: { + 'accept': 'application/json; version=$minRequiredApiVersion' + }), + ); + 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>()), + ); + } } } + throw PaperlessServerException( + errorCode, + httpStatusCode: response.statusCode, + ); + } on DioError catch (err) { + throw err.error; } - throw PaperlessServerException( - errorCode, - httpStatusCode: response.statusCode, - ); } List _collectionFromJson(