From 60aecb549ddbfaeeb22a11b9267fb03b46e8b649 Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Wed, 21 Dec 2022 01:14:06 +0100 Subject: [PATCH] WIP - Replaced get_it + injectable with Provider --- integration_test/login_integration_test.dart | 367 +++++++++--------- integration_test/src/framework.dart | 2 - ...http_self_signed_certificate_override.dart | 11 - .../authentication.interceptor.dart | 3 - .../interceptor/base_url_interceptor.dart | 3 - .../language_header.interceptor.dart | 2 - .../response_conversion.interceptor.dart | 3 - lib/core/logic/timeout_client.dart | 195 ---------- .../provider/label_repositories_provider.dart | 20 +- .../security_context_aware_dio_manager.dart | 100 +++++ .../security/security_context_subject.dart | 26 ++ lib/core/security/security_context_utils.dart | 0 .../service/connectivity_status.service.dart | 3 - lib/core/service/file_service.dart | 4 +- lib/core/service/status.service.dart | 132 +++---- lib/core/store/local_vault.dart | 3 - lib/di_initializer.dart | 33 -- lib/di_modules.dart | 70 ---- lib/di_paperless_api.dart | 47 --- .../application_intro_slideshow.dart | 142 ++++--- .../biometric_authentication_intro_slide.dart | 87 ----- .../view/pages/document_details_page.dart | 42 +- .../widgets/document_download_button.dart | 5 +- .../cubit/document_upload_cubit.dart | 2 - .../document_upload_preparation_page.dart | 15 +- .../view/pages/document_edit_page.dart | 22 +- .../documents/view/pages/documents_page.dart | 13 +- .../view/widgets/document_preview.dart | 7 +- .../view/widgets/sort_documents_button.dart | 2 +- lib/features/home/view/home_page.dart | 24 +- .../home/view/widget/info_drawer.dart | 97 +++-- .../inbox/view/widgets/inbox_item.dart | 8 +- .../labels/view/widgets/label_item.dart | 8 +- .../view/pages/linked_documents_page.dart | 10 +- .../login/bloc/authentication_cubit.dart | 77 ++-- .../login/bloc/authentication_state.dart | 44 +++ .../login/bloc/authentication_state.g.dart | 29 ++ .../model/authentication_information.dart | 29 +- .../model/authentication_information.g.dart | 26 ++ .../widgets/server_address_form_field.dart | 5 +- .../scan/bloc/document_scanner_cubit.dart | 1 - lib/features/scan/view/scanner_page.dart | 10 +- .../bloc/application_settings_cubit.dart | 33 +- .../model/application_settings_state.dart | 41 +- .../model/application_settings_state.g.dart | 39 ++ .../biometric_authentication_setting.dart | 7 +- .../view/widgets/clear_storage_setting.dart | 12 +- lib/main.dart | 181 ++++++--- lib/util.dart | 1 - .../authentication_api_impl.dart | 21 +- .../paperless_documents_api.dart | 4 +- .../paperless_documents_api_impl.dart | 171 +++----- .../labels_api/paperless_labels_api_impl.dart | 99 ++--- .../paperless_saved_views_api_impl.dart | 16 +- .../paperless_server_stats_api_impl.dart | 32 +- .../src/{utils.dart => request_utils.dart} | 24 +- packages/paperless_api/pubspec.yaml | 3 +- pubspec.lock | 40 +- pubspec.yaml | 8 +- 59 files changed, 1099 insertions(+), 1362 deletions(-) delete mode 100644 lib/core/global/http_self_signed_certificate_override.dart delete mode 100644 lib/core/logic/timeout_client.dart create mode 100644 lib/core/security/security_context_aware_dio_manager.dart create mode 100644 lib/core/security/security_context_subject.dart create mode 100644 lib/core/security/security_context_utils.dart delete mode 100644 lib/di_initializer.dart delete mode 100644 lib/di_modules.dart delete mode 100644 lib/di_paperless_api.dart delete mode 100644 lib/features/app_intro/widgets/biometric_authentication_intro_slide.dart create mode 100644 lib/features/login/bloc/authentication_state.dart create mode 100644 lib/features/login/bloc/authentication_state.g.dart create mode 100644 lib/features/login/model/authentication_information.g.dart create mode 100644 lib/features/settings/model/application_settings_state.g.dart rename packages/paperless_api/lib/src/{utils.dart => request_utils.dart} (76%) diff --git a/integration_test/login_integration_test.dart b/integration_test/login_integration_test.dart index cc28581..aac8a56 100644 --- a/integration_test/login_integration_test.dart +++ b/integration_test/login_integration_test.dart @@ -1,225 +1,224 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; -import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; -import 'package:paperless_mobile/core/service/connectivity_status.service.dart'; -import 'package:paperless_mobile/core/store/local_vault.dart'; -import 'package:paperless_mobile/di_initializer.dart'; -import 'package:paperless_mobile/di_test_mocks.mocks.dart'; -import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; -import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; -import 'package:paperless_mobile/features/settings/model/view_type.dart'; +// import 'package:flutter/material.dart'; +// import 'package:flutter_test/flutter_test.dart'; +// import 'package:mockito/mockito.dart'; +// import 'package:paperless_api/paperless_api.dart'; +// import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; +// import 'package:paperless_mobile/core/service/connectivity_status.service.dart'; +// import 'package:paperless_mobile/core/store/local_vault.dart'; +// import 'package:paperless_mobile/di_test_mocks.mocks.dart'; +// import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; +// import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; +// import 'package:paperless_mobile/features/settings/model/view_type.dart'; -import 'src/framework.dart'; +// import 'src/framework.dart'; -void main() async { - final t = await initializeTestingFramework(languageCode: 'de'); +// void main() async { +// final t = await initializeTestingFramework(languageCode: 'de'); - const testServerUrl = 'https://example.com'; - const testUsername = 'user'; - const testPassword = 'pass'; +// const testServerUrl = 'https://example.com'; +// const testUsername = 'user'; +// const testPassword = 'pass'; - final serverAddressField = find.byKey(const ValueKey('login-server-address')); - final usernameField = find.byKey(const ValueKey('login-username')); - final passwordField = find.byKey(const ValueKey('login-password')); - final loginBtn = find.byKey(const ValueKey('login-login-button')); +// final serverAddressField = find.byKey(const ValueKey('login-server-address')); +// final usernameField = find.byKey(const ValueKey('login-username')); +// final passwordField = find.byKey(const ValueKey('login-password')); +// final loginBtn = find.byKey(const ValueKey('login-login-button')); - testWidgets('Test successful login flow', (WidgetTester tester) async { - await initAndLaunchTestApp(tester, () async { - // Initialize dat for mocked classes - when((getIt()).connectivityChanges()) - .thenAnswer((i) => Stream.value(true)); - when((getIt() as MockLocalVault) - .loadAuthenticationInformation()) - .thenAnswer((realInvocation) async => null); - when((getIt() as MockLocalVault).loadApplicationSettings()) - .thenAnswer((realInvocation) async => ApplicationSettingsState( - preferredLocaleSubtag: 'en', - preferredThemeMode: ThemeMode.light, - isLocalAuthenticationEnabled: false, - preferredViewType: ViewType.list, - showInboxOnStartup: false, - )); - when(getIt().login( - username: testUsername, - password: testPassword, - )).thenAnswer((i) => Future.value("eyTestToken")); +// testWidgets('Test successful login flow', (WidgetTester tester) async { +// await initAndLaunchTestApp(tester, () async { +// // Initialize dat for mocked classes +// when((getIt()).connectivityChanges()) +// .thenAnswer((i) => Stream.value(true)); +// when((getIt() as MockLocalVault) +// .loadAuthenticationInformation()) +// .thenAnswer((realInvocation) async => null); +// when((getIt() as MockLocalVault).loadApplicationSettings()) +// .thenAnswer((realInvocation) async => ApplicationSettingsState( +// preferredLocaleSubtag: 'en', +// preferredThemeMode: ThemeMode.light, +// isLocalAuthenticationEnabled: false, +// preferredViewType: ViewType.list, +// showInboxOnStartup: false, +// )); +// when(getIt().login( +// username: testUsername, +// password: testPassword, +// )).thenAnswer((i) => Future.value("eyTestToken")); - await getIt().initialize(); - await getIt().initialize(); - }); +// await getIt().initialize(); +// await getIt().initialize(); +// }); - // Mocked classes +// // Mocked classes - await t.binding.waitUntilFirstFrameRasterized; - await tester.pumpAndSettle(); +// await t.binding.waitUntilFirstFrameRasterized; +// await tester.pumpAndSettle(); - await tester.enterText(serverAddressField, testServerUrl); - await tester.pumpAndSettle(); +// await tester.enterText(serverAddressField, testServerUrl); +// await tester.pumpAndSettle(); - await tester.enterText(usernameField, testUsername); - await tester.pumpAndSettle(); +// await tester.enterText(usernameField, testUsername); +// await tester.pumpAndSettle(); - await tester.enterText(passwordField, testPassword); +// await tester.enterText(passwordField, testPassword); - FocusManager.instance.primaryFocus?.unfocus(); - await tester.pumpAndSettle(); +// FocusManager.instance.primaryFocus?.unfocus(); +// await tester.pumpAndSettle(); - await tester.tap(loginBtn); +// await tester.tap(loginBtn); - verify(getIt().login( - username: testUsername, - password: testPassword, - )).called(1); - }); +// verify(getIt().login( +// username: testUsername, +// password: testPassword, +// )).called(1); +// }); - testWidgets('Test login validation missing password', - (WidgetTester tester) async { - await initAndLaunchTestApp(tester, () async { - when((getIt() as MockConnectivityStatusService) - .connectivityChanges()) - .thenAnswer((i) => Stream.value(true)); - when((getIt() as MockLocalVault) - .loadAuthenticationInformation()) - .thenAnswer((realInvocation) async => null); +// testWidgets('Test login validation missing password', +// (WidgetTester tester) async { +// await initAndLaunchTestApp(tester, () async { +// when((getIt() as MockConnectivityStatusService) +// .connectivityChanges()) +// .thenAnswer((i) => Stream.value(true)); +// when((getIt() as MockLocalVault) +// .loadAuthenticationInformation()) +// .thenAnswer((realInvocation) async => null); - when((getIt() as MockLocalVault).loadApplicationSettings()) - .thenAnswer((realInvocation) async => ApplicationSettingsState( - preferredLocaleSubtag: 'en', - preferredThemeMode: ThemeMode.light, - isLocalAuthenticationEnabled: false, - preferredViewType: ViewType.list, - showInboxOnStartup: false, - )); +// when((getIt() as MockLocalVault).loadApplicationSettings()) +// .thenAnswer((realInvocation) async => ApplicationSettingsState( +// preferredLocaleSubtag: 'en', +// preferredThemeMode: ThemeMode.light, +// isLocalAuthenticationEnabled: false, +// preferredViewType: ViewType.list, +// showInboxOnStartup: false, +// )); - await getIt().initialize(); - await getIt().initialize(); - }); - // Mocked classes +// await getIt().initialize(); +// await getIt().initialize(); +// }); +// // Mocked classes - // Initialize dat for mocked classes +// // Initialize dat for mocked classes - await t.binding.waitUntilFirstFrameRasterized; - await tester.pumpAndSettle(); +// await t.binding.waitUntilFirstFrameRasterized; +// await tester.pumpAndSettle(); - await tester.enterText(serverAddressField, testServerUrl); - await tester.pumpAndSettle(); +// await tester.enterText(serverAddressField, testServerUrl); +// await tester.pumpAndSettle(); - await tester.enterText(usernameField, testUsername); - await tester.pumpAndSettle(); +// await tester.enterText(usernameField, testUsername); +// await tester.pumpAndSettle(); - FocusManager.instance.primaryFocus?.unfocus(); - await tester.pumpAndSettle(); +// FocusManager.instance.primaryFocus?.unfocus(); +// await tester.pumpAndSettle(); - await tester.tap(loginBtn); - await tester.pumpAndSettle(); +// await tester.tap(loginBtn); +// await tester.pumpAndSettle(); - verifyNever( - (getIt() as MockPaperlessAuthenticationApi) - .login( - username: testUsername, - password: testPassword, - )); - expect( - find.textContaining(t.translations.loginPagePasswordValidatorMessageText), - findsOneWidget, - ); - }); +// verifyNever( +// (getIt() as MockPaperlessAuthenticationApi) +// .login( +// username: testUsername, +// password: testPassword, +// )); +// expect( +// find.textContaining(t.translations.loginPagePasswordValidatorMessageText), +// findsOneWidget, +// ); +// }); - testWidgets('Test login validation missing username', - (WidgetTester tester) async { - await initAndLaunchTestApp(tester, () async { - when((getIt() as MockConnectivityStatusService) - .connectivityChanges()) - .thenAnswer((i) => Stream.value(true)); - when((getIt() as MockLocalVault) - .loadAuthenticationInformation()) - .thenAnswer((realInvocation) async => null); - when((getIt() as MockLocalVault).loadApplicationSettings()) - .thenAnswer((realInvocation) async => ApplicationSettingsState( - preferredLocaleSubtag: 'en', - preferredThemeMode: ThemeMode.light, - isLocalAuthenticationEnabled: false, - preferredViewType: ViewType.list, - showInboxOnStartup: false, - )); - await getIt().initialize(); - await getIt().initialize(); - }); +// testWidgets('Test login validation missing username', +// (WidgetTester tester) async { +// await initAndLaunchTestApp(tester, () async { +// when((getIt() as MockConnectivityStatusService) +// .connectivityChanges()) +// .thenAnswer((i) => Stream.value(true)); +// when((getIt() as MockLocalVault) +// .loadAuthenticationInformation()) +// .thenAnswer((realInvocation) async => null); +// when((getIt() as MockLocalVault).loadApplicationSettings()) +// .thenAnswer((realInvocation) async => ApplicationSettingsState( +// preferredLocaleSubtag: 'en', +// preferredThemeMode: ThemeMode.light, +// isLocalAuthenticationEnabled: false, +// preferredViewType: ViewType.list, +// showInboxOnStartup: false, +// )); +// await getIt().initialize(); +// await getIt().initialize(); +// }); - await t.binding.waitUntilFirstFrameRasterized; - await tester.pumpAndSettle(); +// await t.binding.waitUntilFirstFrameRasterized; +// await tester.pumpAndSettle(); - await tester.enterText(serverAddressField, testServerUrl); - await tester.pumpAndSettle(); +// await tester.enterText(serverAddressField, testServerUrl); +// await tester.pumpAndSettle(); - await tester.enterText(passwordField, testPassword); - await tester.pumpAndSettle(); +// await tester.enterText(passwordField, testPassword); +// await tester.pumpAndSettle(); - FocusManager.instance.primaryFocus?.unfocus(); - await tester.pumpAndSettle(); +// FocusManager.instance.primaryFocus?.unfocus(); +// await tester.pumpAndSettle(); - await tester.tap(loginBtn); - await tester.pumpAndSettle(); +// await tester.tap(loginBtn); +// await tester.pumpAndSettle(); - verifyNever( - (getIt() as MockPaperlessAuthenticationApi) - .login( - username: testUsername, - password: testPassword, - )); - expect( - find.textContaining(t.translations.loginPageUsernameValidatorMessageText), - findsOneWidget, - ); - }); +// verifyNever( +// (getIt() as MockPaperlessAuthenticationApi) +// .login( +// username: testUsername, +// password: testPassword, +// )); +// expect( +// find.textContaining(t.translations.loginPageUsernameValidatorMessageText), +// findsOneWidget, +// ); +// }); - testWidgets('Test login validation missing server address', - (WidgetTester tester) async { - initAndLaunchTestApp(tester, () async { - when((getIt()).connectivityChanges()) - .thenAnswer((i) => Stream.value(true)); +// testWidgets('Test login validation missing server address', +// (WidgetTester tester) async { +// initAndLaunchTestApp(tester, () async { +// when((getIt()).connectivityChanges()) +// .thenAnswer((i) => Stream.value(true)); - when((getIt()).loadAuthenticationInformation()) - .thenAnswer((realInvocation) async => null); +// when((getIt()).loadAuthenticationInformation()) +// .thenAnswer((realInvocation) async => null); - when((getIt()).loadApplicationSettings()) - .thenAnswer((realInvocation) async => ApplicationSettingsState( - preferredLocaleSubtag: 'en', - preferredThemeMode: ThemeMode.light, - isLocalAuthenticationEnabled: false, - preferredViewType: ViewType.list, - showInboxOnStartup: false, - )); +// when((getIt()).loadApplicationSettings()) +// .thenAnswer((realInvocation) async => ApplicationSettingsState( +// preferredLocaleSubtag: 'en', +// preferredThemeMode: ThemeMode.light, +// isLocalAuthenticationEnabled: false, +// preferredViewType: ViewType.list, +// showInboxOnStartup: false, +// )); - await getIt().initialize(); - await getIt().initialize(); - }); +// await getIt().initialize(); +// await getIt().initialize(); +// }); - await t.binding.waitUntilFirstFrameRasterized; - await tester.pumpAndSettle(); +// await t.binding.waitUntilFirstFrameRasterized; +// await tester.pumpAndSettle(); - await tester.enterText(usernameField, testUsername); - await tester.pumpAndSettle(); +// await tester.enterText(usernameField, testUsername); +// await tester.pumpAndSettle(); - await tester.enterText(passwordField, testPassword); - await tester.pumpAndSettle(); +// await tester.enterText(passwordField, testPassword); +// await tester.pumpAndSettle(); - FocusManager.instance.primaryFocus?.unfocus(); - await tester.pumpAndSettle(); +// FocusManager.instance.primaryFocus?.unfocus(); +// await tester.pumpAndSettle(); - await tester.tap(loginBtn); - await tester.pumpAndSettle(); +// await tester.tap(loginBtn); +// await tester.pumpAndSettle(); - verifyNever(getIt().login( - username: testUsername, - password: testPassword, - )); - expect( - find.textContaining( - t.translations.loginPageServerUrlValidatorMessageText), - findsOneWidget, - ); - }); -} +// verifyNever(getIt().login( +// username: testUsername, +// password: testPassword, +// )); +// expect( +// find.textContaining( +// t.translations.loginPageServerUrlValidatorMessageText), +// findsOneWidget, +// ); +// }); +// } diff --git a/integration_test/src/framework.dart b/integration_test/src/framework.dart index d4e0113..4bbafc9 100644 --- a/integration_test/src/framework.dart +++ b/integration_test/src/framework.dart @@ -1,13 +1,11 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/generated/l10n.dart'; Future initializeTestingFramework( {String languageCode = 'en'}) async { final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - configureDependencies('test'); final translations = await S.load( Locale.fromSubtags( languageCode: languageCode, diff --git a/lib/core/global/http_self_signed_certificate_override.dart b/lib/core/global/http_self_signed_certificate_override.dart deleted file mode 100644 index b8b9ed0..0000000 --- a/lib/core/global/http_self_signed_certificate_override.dart +++ /dev/null @@ -1,11 +0,0 @@ -// Fix for accepting self signed certificates. -import 'dart:io'; - -class X509HttpOverrides extends HttpOverrides { - @override - HttpClient createHttpClient(SecurityContext? context) { - return super.createHttpClient(context) - ..badCertificateCallback = - (X509Certificate cert, String host, int port) => true; - } -} diff --git a/lib/core/interceptor/authentication.interceptor.dart b/lib/core/interceptor/authentication.interceptor.dart index 9c0a29d..4552006 100644 --- a/lib/core/interceptor/authentication.interceptor.dart +++ b/lib/core/interceptor/authentication.interceptor.dart @@ -2,11 +2,8 @@ import 'dart:developer'; import 'package:flutter/foundation.dart'; import 'package:http_interceptor/http_interceptor.dart'; -import 'package:injectable/injectable.dart'; import 'package:paperless_mobile/core/store/local_vault.dart'; -@prod -@injectable class AuthenticationInterceptor implements InterceptorContract { final LocalVault _localVault; AuthenticationInterceptor(this._localVault); diff --git a/lib/core/interceptor/base_url_interceptor.dart b/lib/core/interceptor/base_url_interceptor.dart index 68424de..065a8eb 100644 --- a/lib/core/interceptor/base_url_interceptor.dart +++ b/lib/core/interceptor/base_url_interceptor.dart @@ -1,9 +1,6 @@ import 'package:http_interceptor/http_interceptor.dart'; -import 'package:injectable/injectable.dart'; import 'package:paperless_mobile/core/store/local_vault.dart'; -@prod -@injectable class BaseUrlInterceptor implements InterceptorContract { final LocalVault _localVault; diff --git a/lib/core/interceptor/language_header.interceptor.dart b/lib/core/interceptor/language_header.interceptor.dart index f50e704..cce34b0 100644 --- a/lib/core/interceptor/language_header.interceptor.dart +++ b/lib/core/interceptor/language_header.interceptor.dart @@ -1,8 +1,6 @@ import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; import 'package:http_interceptor/http_interceptor.dart'; -import 'package:injectable/injectable.dart'; -@injectable class LanguageHeaderInterceptor implements InterceptorContract { final ApplicationSettingsCubit appSettingsCubit; diff --git a/lib/core/interceptor/response_conversion.interceptor.dart b/lib/core/interceptor/response_conversion.interceptor.dart index a25e3c6..844321e 100644 --- a/lib/core/interceptor/response_conversion.interceptor.dart +++ b/lib/core/interceptor/response_conversion.interceptor.dart @@ -1,10 +1,7 @@ import 'package:http_interceptor/http_interceptor.dart'; -import 'package:injectable/injectable.dart'; const interceptedRoutes = ['thumb/']; -@injectable -@prod class ResponseConversionInterceptor implements InterceptorContract { @override Future interceptRequest({required BaseRequest request}) async => diff --git a/lib/core/logic/timeout_client.dart b/lib/core/logic/timeout_client.dart deleted file mode 100644 index 5d9cc2d..0000000 --- a/lib/core/logic/timeout_client.dart +++ /dev/null @@ -1,195 +0,0 @@ -import 'dart:typed_data'; - -import 'dart:convert'; - -import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/service/connectivity_status.service.dart'; -import 'package:paperless_mobile/core/type/types.dart'; -import 'package:paperless_mobile/di_initializer.dart'; -import 'package:http/http.dart'; -import 'package:injectable/injectable.dart'; - -/// -/// Convenience class which handles timeout errors. -/// -@prod -@Named("timeoutClient") -@Injectable(as: BaseClient) -class TimeoutClient implements BaseClient { - final ConnectivityStatusService connectivityStatusService; - static const Duration requestTimeout = Duration(seconds: 25); - - TimeoutClient(this.connectivityStatusService); - - @override - Future send(BaseRequest request) async { - return getIt().send(request).timeout( - requestTimeout, - onTimeout: () => Future.error( - const PaperlessServerException(ErrorCode.requestTimedOut)), - ); - } - - @override - void close() { - getIt().close(); - } - - @override - Future delete( - Uri url, { - Map? headers, - Object? body, - Encoding? encoding, - }) async { - await _handleOfflineState(); - return _handle400Error( - await getIt() - .delete(url, headers: headers, body: body, encoding: encoding) - .timeout( - requestTimeout, - onTimeout: () => Future.error( - const PaperlessServerException(ErrorCode.requestTimedOut)), - ), - ); - } - - @override - Future get( - Uri url, { - Map? headers, - }) async { - await _handleOfflineState(); - return _handle400Error( - await getIt().get(url, headers: headers).timeout( - requestTimeout, - onTimeout: () => Future.error( - const PaperlessServerException(ErrorCode.requestTimedOut)), - ), - ); - } - - @override - Future head( - Uri url, { - Map? headers, - }) async { - await _handleOfflineState(); - return _handle400Error( - await getIt().head(url, headers: headers).timeout( - requestTimeout, - onTimeout: () => Future.error( - const PaperlessServerException(ErrorCode.requestTimedOut)), - ), - ); - } - - @override - Future patch( - Uri url, { - Map? headers, - Object? body, - Encoding? encoding, - }) async { - await _handleOfflineState(); - return _handle400Error( - await getIt() - .patch(url, headers: headers, body: body, encoding: encoding) - .timeout( - requestTimeout, - onTimeout: () => Future.error( - const PaperlessServerException(ErrorCode.requestTimedOut)), - ), - ); - } - - @override - Future post( - Uri url, { - Map? headers, - Object? body, - Encoding? encoding, - }) async { - await _handleOfflineState(); - return _handle400Error( - await getIt() - .post(url, headers: headers, body: body, encoding: encoding) - .timeout( - requestTimeout, - onTimeout: () => Future.error( - const PaperlessServerException(ErrorCode.requestTimedOut)), - ), - ); - } - - @override - Future put( - Uri url, { - Map? headers, - Object? body, - Encoding? encoding, - }) async { - await _handleOfflineState(); - return _handle400Error( - await getIt() - .put(url, headers: headers, body: body, encoding: encoding) - .timeout( - requestTimeout, - onTimeout: () => Future.error( - const PaperlessServerException(ErrorCode.requestTimedOut)), - ), - ); - } - - @override - Future read( - Uri url, { - Map? headers, - }) async { - await _handleOfflineState(); - return getIt().read(url, headers: headers).timeout( - requestTimeout, - onTimeout: () => Future.error( - const PaperlessServerException(ErrorCode.requestTimedOut)), - ); - } - - @override - Future readBytes( - Uri url, { - Map? headers, - }) async { - await _handleOfflineState(); - return getIt().readBytes(url, headers: headers).timeout( - requestTimeout, - onTimeout: () => Future.error( - const PaperlessServerException(ErrorCode.requestTimedOut)), - ); - } - - Response _handle400Error(Response response) { - if (response.statusCode == 400) { - // try to parse contained error message, otherwise return response - final JSON json = jsonDecode(utf8.decode(response.bodyBytes)); - final PaperlessValidationErrors errorMessages = {}; - for (final entry in json.entries) { - if (entry.value is List) { - errorMessages.putIfAbsent( - entry.key, () => (entry.value as List).cast().first); - } else if (entry.value is String) { - errorMessages.putIfAbsent(entry.key, () => entry.value); - } else { - errorMessages.putIfAbsent(entry.key, () => entry.value.toString()); - } - } - throw errorMessages; - } - return response; - } - - Future _handleOfflineState() async { - if (!(await connectivityStatusService.isConnectedToInternet())) { - throw const PaperlessServerException(ErrorCode.deviceOffline); - } - } -} diff --git a/lib/core/repository/provider/label_repositories_provider.dart b/lib/core/repository/provider/label_repositories_provider.dart index 8bfb691..c1c83f0 100644 --- a/lib/core/repository/provider/label_repositories_provider.dart +++ b/lib/core/repository/provider/label_repositories_provider.dart @@ -11,17 +11,21 @@ class LabelRepositoriesProvider extends StatelessWidget { Widget build(BuildContext context) { return MultiRepositoryProvider( providers: [ - RepositoryProvider.value( - value: RepositoryProvider.of>(context), + RepositoryProvider( + create: (context) => + RepositoryProvider.of>(context), ), - RepositoryProvider.value( - value: RepositoryProvider.of>(context), + RepositoryProvider( + create: (context) => + RepositoryProvider.of>(context), ), - RepositoryProvider.value( - value: RepositoryProvider.of>(context), + RepositoryProvider( + create: (context) => + RepositoryProvider.of>(context), ), - RepositoryProvider.value( - value: RepositoryProvider.of>(context), + RepositoryProvider( + create: (context) => + RepositoryProvider.of>(context), ), ], child: child, diff --git a/lib/core/security/security_context_aware_dio_manager.dart b/lib/core/security/security_context_aware_dio_manager.dart new file mode 100644 index 0000000..5d891ac --- /dev/null +++ b/lib/core/security/security_context_aware_dio_manager.dart @@ -0,0 +1,100 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:dio/adapter.dart'; +import 'package:dio/dio.dart'; +import 'package:paperless_mobile/core/type/types.dart'; +import 'package:paperless_mobile/features/login/model/client_certificate.dart'; + +/// +/// Convenience http client handling timeouts. +/// +class SecurityContextAwareDioManager { + final Dio _dio; + // Some dependencies require an [HttpClient], therefore this is also maintained here. + final HttpClient _httpClient; + SecurityContextAwareDioManager() + : _dio = _initDio(), + _httpClient = HttpClient(); + + Dio get client => _dio; + + Stream get securityContextChanges => + _securityContextStreamController.stream.asBroadcastStream(); + + final StreamController _securityContextStreamController = + StreamController.broadcast(); + + static Dio _initDio() { + //en- and decoded by utf8 by default + final Dio dio = Dio(BaseOptions()); + dio.options.receiveTimeout = const Duration(seconds: 25).inMilliseconds; + dio.options.responseType = ResponseType.json; + (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = + (client) => client + ..badCertificateCallback = + (X509Certificate cert, String host, int port) => true; + dio.interceptors.add( + //TODO: Refactor, create own class... + InterceptorsWrapper( + onError: (e, handler) { + //TODO: Implement and debug how error handling works, or if request has to be resolved. + if (e.response?.statusCode == 400) { + // try to parse contained error message, otherwise return response + final JSON json = jsonDecode(e.response?.data); + final PaperlessValidationErrors errorMessages = {}; + for (final entry in json.entries) { + if (entry.value is List) { + errorMessages.putIfAbsent(entry.key, + () => (entry.value as List).cast().first); + } else if (entry.value is String) { + errorMessages.putIfAbsent(entry.key, () => entry.value); + } else { + errorMessages.putIfAbsent( + entry.key, () => entry.value.toString()); + } + } + throw errorMessages; + } + handler.next(e); + }, + ), + ); + return dio; + } + + void updateSettings({ + String? baseUrl, + String? authToken, + ClientCertificate? clientCertificate, + }) { + if (clientCertificate != null) { + final sc = SecurityContext() + ..usePrivateKeyBytes( + clientCertificate.bytes, + password: clientCertificate.passphrase, + ) + ..useCertificateChainBytes( + clientCertificate.bytes, + password: clientCertificate.passphrase, + ) + ..setTrustedCertificatesBytes( + clientCertificate.bytes, + password: clientCertificate.passphrase, + ); + (_dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = + (client) => HttpClient( + context: sc, + )..badCertificateCallback = + (X509Certificate cert, String host, int port) => true; + _securityContextStreamController.add(sc); + } + if (baseUrl != null) { + _dio.options.baseUrl = baseUrl; + } + if (authToken != null) { + _dio.options.headers.addAll({'Authorization': 'Token $authToken'}); + } + } +} diff --git a/lib/core/security/security_context_subject.dart b/lib/core/security/security_context_subject.dart new file mode 100644 index 0000000..036b720 --- /dev/null +++ b/lib/core/security/security_context_subject.dart @@ -0,0 +1,26 @@ +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/security/security_context_utils.dart b/lib/core/security/security_context_utils.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/core/service/connectivity_status.service.dart b/lib/core/service/connectivity_status.service.dart index a4fbbd1..9b81267 100644 --- a/lib/core/service/connectivity_status.service.dart +++ b/lib/core/service/connectivity_status.service.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:connectivity_plus/connectivity_plus.dart'; -import 'package:injectable/injectable.dart'; abstract class ConnectivityStatusService { Future isConnectedToInternet(); @@ -9,8 +8,6 @@ abstract class ConnectivityStatusService { Stream connectivityChanges(); } -@prod -@Injectable(as: ConnectivityStatusService) class ConnectivityStatusServiceImpl implements ConnectivityStatusService { final Connectivity connectivity; diff --git a/lib/core/service/file_service.dart b/lib/core/service/file_service.dart index d9e9b2c..a8c0011 100644 --- a/lib/core/service/file_service.dart +++ b/lib/core/service/file_service.dart @@ -82,8 +82,8 @@ class FileService { static Future clearUserData() async { final scanDir = await scanDirectory; final tempDir = await temporaryDirectory; - scanDir?.delete(recursive: true); - tempDir.delete(recursive: true); + await scanDir?.delete(recursive: true); + await tempDir.delete(recursive: true); } } diff --git a/lib/core/service/status.service.dart b/lib/core/service/status.service.dart index 1362b39..0a5bc96 100644 --- a/lib/core/service/status.service.dart +++ b/lib/core/service/status.service.dart @@ -3,11 +3,9 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart'; -import 'package:injectable/injectable.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/document_status_cubit.dart'; import 'package:paperless_mobile/core/model/document_processing_status.dart'; -import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/features/login/model/authentication_information.dart'; import 'package:paperless_mobile/util.dart'; import 'package:web_socket_channel/io.dart'; @@ -17,8 +15,6 @@ abstract class StatusService { AuthenticationInformation credentials, String documentFileName); } -@Singleton(as: StatusService) -@Named("webSocketStatusService") class WebSocketStatusService implements StatusService { late WebSocket? socket; late IOWebSocketChannel? _channel; @@ -31,35 +27,33 @@ class WebSocketStatusService implements StatusService { AuthenticationInformation credentials, String documentFileName, ) async { - socket = await WebSocket.connect( - httpUrl.replaceFirst("http", "ws") + "/ws/status/", - customClient: getIt(), - headers: { - 'Authorization': 'Token ${credentials.token}', - }, - ).catchError((_) { - // Use long polling if connection could not be established - }); + // socket = await WebSocket.connect( + // httpUrl.replaceFirst("http", "ws") + "/ws/status/", + // customClient: getIt(), + // headers: { + // 'Authorization': 'Token ${credentials.token}', + // }, + // ).catchError((_) { + // // Use long polling if connection could not be established + // }); - if (socket != null) { - socket!.where(isNotNull).listen((event) { - final status = DocumentProcessingStatus.fromJson(event); - getIt().updateStatus(status); - if (status.currentProgress == 100) { - socket!.close(); - } - }); - } + // if (socket != null) { + // socket!.where(isNotNull).listen((event) { + // final status = DocumentProcessingStatus.fromJson(event); + // getIt().updateStatus(status); + // if (status.currentProgress == 100) { + // socket!.close(); + // } + // }); + // } } } -@Injectable(as: StatusService) -@Named("longPollingStatusService") class LongPollingStatusService implements StatusService { static const maxRetries = 60; final BaseClient httpClient; - LongPollingStatusService(@Named("timeoutClient") this.httpClient); + LongPollingStatusService(this.httpClient); @override Future startListeningBeforeDocumentUpload( @@ -67,51 +61,51 @@ class LongPollingStatusService implements StatusService { AuthenticationInformation credentials, String documentFileName, ) async { - final today = DateTime.now(); - bool consumptionFinished = false; - int retryCount = 0; + // final today = DateTime.now(); + // bool consumptionFinished = false; + // int retryCount = 0; - getIt().updateStatus( - DocumentProcessingStatus( - currentProgress: 0, - filename: documentFileName, - maxProgress: 100, - message: ProcessingMessage.new_file, - status: ProcessingStatus.working, - taskId: DocumentProcessingStatus.unknownTaskId, - documentId: null, - isApproximated: true, - ), - ); + // getIt().updateStatus( + // DocumentProcessingStatus( + // currentProgress: 0, + // filename: documentFileName, + // maxProgress: 100, + // message: ProcessingMessage.new_file, + // status: ProcessingStatus.working, + // taskId: DocumentProcessingStatus.unknownTaskId, + // documentId: null, + // isApproximated: true, + // ), + // ); - do { - final response = await httpClient.get( - Uri.parse( - '$httpUrl/api/documents/?query=$documentFileName added:${formatDate(today)}'), - ); - final data = await compute( - PagedSearchResult.fromJson, - PagedSearchResultJsonSerializer( - jsonDecode(response.body), DocumentModel.fromJson), - ); - if (data.count > 0) { - consumptionFinished = true; - final docId = data.results[0].id; - getIt().updateStatus( - DocumentProcessingStatus( - currentProgress: 100, - filename: documentFileName, - maxProgress: 100, - message: ProcessingMessage.finished, - status: ProcessingStatus.success, - taskId: DocumentProcessingStatus.unknownTaskId, - documentId: docId, - isApproximated: true, - ), - ); - return; - } - sleep(const Duration(seconds: 1)); - } while (!consumptionFinished && retryCount < maxRetries); + // do { + // final response = await httpClient.get( + // Uri.parse( + // '$httpUrl/api/documents/?query=$documentFileName added:${formatDate(today)}'), + // ); + // final data = await compute( + // PagedSearchResult.fromJson, + // PagedSearchResultJsonSerializer( + // jsonDecode(response.body), DocumentModel.fromJson), + // ); + // if (data.count > 0) { + // consumptionFinished = true; + // final docId = data.results[0].id; + // getIt().updateStatus( + // DocumentProcessingStatus( + // currentProgress: 100, + // filename: documentFileName, + // maxProgress: 100, + // message: ProcessingMessage.finished, + // status: ProcessingStatus.success, + // taskId: DocumentProcessingStatus.unknownTaskId, + // documentId: docId, + // isApproximated: true, + // ), + // ); + // return; + // } + // sleep(const Duration(seconds: 1)); + // } while (!consumptionFinished && retryCount < maxRetries); } } diff --git a/lib/core/store/local_vault.dart b/lib/core/store/local_vault.dart index 9a8d929..9859e8f 100644 --- a/lib/core/store/local_vault.dart +++ b/lib/core/store/local_vault.dart @@ -6,7 +6,6 @@ import 'package:paperless_mobile/core/type/types.dart'; import 'package:paperless_mobile/features/login/model/authentication_information.dart'; import 'package:paperless_mobile/features/login/model/client_certificate.dart'; import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; -import 'package:injectable/injectable.dart'; abstract class LocalVault { Future storeAuthenticationInformation(AuthenticationInformation auth); @@ -17,8 +16,6 @@ abstract class LocalVault { Future clear(); } -@prod -@Injectable(as: LocalVault) class LocalVaultImpl implements LocalVault { static const applicationSettingsKey = "applicationSettings"; static const authenticationKey = "authentication"; diff --git a/lib/di_initializer.dart b/lib/di_initializer.dart deleted file mode 100644 index 08bcb22..0000000 --- a/lib/di_initializer.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'dart:io'; - -import 'package:paperless_mobile/di_initializer.config.dart'; -import 'package:paperless_mobile/di_modules.dart'; -import 'package:paperless_mobile/features/login/model/client_certificate.dart'; -import 'package:get_it/get_it.dart'; -import 'package:injectable/injectable.dart'; - -final getIt = GetIt.instance..allowReassignment; - -@InjectableInit( - initializerName: 'init', // default - preferRelativeImports: true, // default - asExtension: false, // default - includeMicroPackages: false, -) -void configureDependencies(String environment) => - init(getIt, environment: environment); - -/// -/// Registers new security context, which will be used by the HttpClient, see [RegisterModule]. -/// -Future registerSecurityContext(ClientCertificate? cert) async { - var context = SecurityContext(); - if (cert != null) { - context = context - ..usePrivateKeyBytes(cert.bytes, password: cert.passphrase) - ..useCertificateChainBytes(cert.bytes, password: cert.passphrase) - ..setTrustedCertificatesBytes(cert.bytes, password: cert.passphrase); - } - await getIt.unregister(); - getIt.registerSingleton(context); -} diff --git a/lib/di_modules.dart b/lib/di_modules.dart deleted file mode 100644 index b4e5b5e..0000000 --- a/lib/di_modules.dart +++ /dev/null @@ -1,70 +0,0 @@ -import 'dart:io'; - -import 'package:connectivity_plus/connectivity_plus.dart'; -import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart'; -import 'package:flutter_cache_manager/flutter_cache_manager.dart'; -import 'package:http_interceptor/http_interceptor.dart'; -import 'package:paperless_mobile/core/interceptor/authentication.interceptor.dart'; -import 'package:paperless_mobile/core/interceptor/base_url_interceptor.dart'; -import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart'; -import 'package:paperless_mobile/core/interceptor/response_conversion.interceptor.dart'; -import 'package:http/io_client.dart'; -import 'package:injectable/injectable.dart'; -import 'package:local_auth/local_auth.dart'; - -@module -abstract class RegisterModule { - @prod - @singleton - LocalAuthentication get localAuthentication => LocalAuthentication(); - - @prod - @singleton - EncryptedSharedPreferences get encryptedSharedPreferences => - EncryptedSharedPreferences(); - - @prod - @test - @singleton - @Order(-1) - SecurityContext get securityContext => SecurityContext(); - - @prod - @singleton - Connectivity get connectivity => Connectivity(); - - /// - /// Factory method creating an [HttpClient] with the currently registered [SecurityContext]. - /// - @prod - @Order(-1) - HttpClient getHttpClient(SecurityContext securityContext) => - HttpClient(context: securityContext) - ..connectionTimeout = const Duration(seconds: 10); - - /// - /// Factory method creating a [InterceptedClient] on top of the currently registered [HttpClient]. - /// - @prod - @Order(-1) - BaseClient getBaseClient( - AuthenticationInterceptor authInterceptor, - ResponseConversionInterceptor responseConversionInterceptor, - LanguageHeaderInterceptor languageHeaderInterceptor, - BaseUrlInterceptor baseUrlInterceptor, - HttpClient client, - ) => - InterceptedClient.build( - interceptors: [ - baseUrlInterceptor, - authInterceptor, - responseConversionInterceptor, - languageHeaderInterceptor, - ], - client: IOClient(client), - ); - - @prod - CacheManager getCacheManager(BaseClient client) => CacheManager( - Config('cacheKey', fileService: HttpFileService(httpClient: client))); -} diff --git a/lib/di_paperless_api.dart b/lib/di_paperless_api.dart deleted file mode 100644 index 10b3e3b..0000000 --- a/lib/di_paperless_api.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'dart:io'; - -import 'package:http/http.dart'; -import 'package:injectable/injectable.dart'; -import 'package:paperless_api/paperless_api.dart'; - -@module -abstract class PaperlessApiModule { - @prod - @Order(-1) - @injectable - PaperlessAuthenticationApi authenticationModule(BaseClient client) => - PaperlessAuthenticationApiImpl(client); - - @prod - @Order(-1) - @injectable - PaperlessLabelsApi labelsModule( - @Named('timeoutClient') BaseClient timeoutClient, - ) => - PaperlessLabelApiImpl(timeoutClient); - - @prod - @Order(-1) - @injectable - PaperlessDocumentsApi documentsModule( - @Named('timeoutClient') BaseClient timeoutClient, - HttpClient httpClient, - ) => - PaperlessDocumentsApiImpl(timeoutClient, httpClient); - - @prod - @Order(-1) - @injectable - PaperlessSavedViewsApi savedViewsModule( - @Named('timeoutClient') BaseClient timeoutClient, - ) => - PaperlessSavedViewsApiImpl(timeoutClient); - - @prod - @Order(-1) - @injectable - PaperlessServerStatsApi serverStatsModule( - @Named('timeoutClient') BaseClient timeoutClient, - ) => - PaperlessServerStatsApiImpl(timeoutClient); -} diff --git a/lib/features/app_intro/application_intro_slideshow.dart b/lib/features/app_intro/application_intro_slideshow.dart index b91d571..02a3231 100644 --- a/lib/features/app_intro/application_intro_slideshow.dart +++ b/lib/features/app_intro/application_intro_slideshow.dart @@ -2,7 +2,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/di_initializer.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'; @@ -26,84 +25,81 @@ class _ApplicationIntroSlideshowState extends State { Widget build(BuildContext context) { return WillPopScope( onWillPop: () async => false, - child: BlocProvider.value( - value: getIt(), - child: IntroductionScreen( - globalBackgroundColor: Theme.of(context).canvasColor, - showDoneButton: true, - next: Text(S.of(context).onboardingNextButtonLabel), - done: Text(S.of(context).onboardingDoneButtonLabel), - onDone: () => Navigator.pop(context), - dotsDecorator: DotsDecorator( - color: Theme.of(context).colorScheme.onBackground, - activeColor: Theme.of(context).colorScheme.primary, - activeSize: const Size(16.0, 8.0), - activeShape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(25.0)), + child: IntroductionScreen( + globalBackgroundColor: Theme.of(context).canvasColor, + showDoneButton: true, + next: Text(S.of(context).onboardingNextButtonLabel), + done: Text(S.of(context).onboardingDoneButtonLabel), + onDone: () => Navigator.pop(context), + dotsDecorator: DotsDecorator( + color: Theme.of(context).colorScheme.onBackground, + activeColor: Theme.of(context).colorScheme.primary, + activeSize: const Size(16.0, 8.0), + activeShape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(25.0)), + ), + ), + pages: [ + PageViewModel( + titleWidget: Text( + "Always right at your fingertip", + style: Theme.of(context).textTheme.titleLarge, + ), + image: Padding( + padding: const EdgeInsets.all(8.0), + child: Image( + image: AssetImages.organizeDocuments.image, + ), + ), + bodyWidget: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Text( + "Organizing documents was never this easy", + textAlign: TextAlign.center, + ), + ], ), ), - pages: [ - PageViewModel( - titleWidget: Text( - "Always right at your fingertip", - style: Theme.of(context).textTheme.titleLarge, - ), - image: Padding( - padding: const EdgeInsets.all(8.0), - child: Image( - image: AssetImages.organizeDocuments.image, + PageViewModel( + titleWidget: Text( + "Accessible only by you", + style: Theme.of(context).textTheme.titleLarge, + ), + image: Padding( + padding: const EdgeInsets.all(8.0), + child: Image(image: AssetImages.secureDocuments.image), + ), + bodyWidget: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Text( + "Secure your documents with biometric authentication and client certificates", + textAlign: TextAlign.center, ), - ), - bodyWidget: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Text( - "Organizing documents was never this easy", - textAlign: TextAlign.center, - ), - ], - ), + ], ), - PageViewModel( - titleWidget: Text( - "Accessible only by you", - style: Theme.of(context).textTheme.titleLarge, - ), - image: Padding( - padding: const EdgeInsets.all(8.0), - child: Image(image: AssetImages.secureDocuments.image), - ), - bodyWidget: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Text( - "Secure your documents with biometric authentication and client certificates", - textAlign: TextAlign.center, - ), - ], - ), + ), + PageViewModel( + titleWidget: Text( + "You're almost done", + style: Theme.of(context).textTheme.titleLarge, ), - PageViewModel( - titleWidget: Text( - "You're almost done", - style: Theme.of(context).textTheme.titleLarge, - ), - image: Padding( - padding: const EdgeInsets.all(8.0), - child: Image(image: AssetImages.success.image), - ), - bodyWidget: Column( - children: const [ - BiometricAuthenticationSetting(), - LanguageSelectionSetting(), - ThemeModeSetting(), - ], - ), + image: Padding( + padding: const EdgeInsets.all(8.0), + child: Image(image: AssetImages.success.image), ), - ], - ), + bodyWidget: Column( + children: const [ + BiometricAuthenticationSetting(), + LanguageSelectionSetting(), + ThemeModeSetting(), + ], + ), + ), + ], ), ); } diff --git a/lib/features/app_intro/widgets/biometric_authentication_intro_slide.dart b/lib/features/app_intro/widgets/biometric_authentication_intro_slide.dart deleted file mode 100644 index afafc46..0000000 --- a/lib/features/app_intro/widgets/biometric_authentication_intro_slide.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_mobile/di_initializer.dart'; -import 'package:paperless_mobile/features/login/services/authentication_service.dart'; -import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; -import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; -import 'package:paperless_mobile/util.dart'; - -class BiometricAuthenticationIntroSlide extends StatefulWidget { - const BiometricAuthenticationIntroSlide({ - Key? key, - }) : super(key: key); - - @override - State createState() => - _BiometricAuthenticationIntroSlideState(); -} - -class _BiometricAuthenticationIntroSlideState - extends State { - @override - Widget build(BuildContext context) { - //TODO: INTL - return BlocBuilder( - builder: (context, settings) { - return Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - "Configure Biometric Authentication", - style: Theme.of(context).textTheme.titleLarge, - textAlign: TextAlign.center, - ), - Text( - "It is highly recommended to additionally secure your local data. Do you want to enable biometric authentication?", - textAlign: TextAlign.center, - ), - Column( - children: [ - const Icon( - Icons.fingerprint, - size: 48, - ), - const SizedBox( - height: 32, - ), - Builder(builder: (context) { - if (settings.isLocalAuthenticationEnabled) { - return ElevatedButton.icon( - icon: Icon( - Icons.done, - color: Colors.green, - ), - label: Text("Enabled"), - onPressed: null, - ); - } - return ElevatedButton( - child: Text("Enable"), - onPressed: () { - final settings = - BlocProvider.of(context) - .state; - getIt() - .authenticateLocalUser( - "Please authenticate to secure Paperless Mobile") - .then((isEnabled) { - if (!isEnabled) { - showSnackBar(context, - "Could not set up biometric authentication. Please try again or skip for now."); - return; - } - BlocProvider.of(context) - .setIsBiometricAuthenticationEnabled(true); - }); - }, - ); - }), - ], - ), - ], - ); - }, - ); - } -} 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 b7e693e..1e8cf0e 100644 --- a/lib/features/document_details/view/pages/document_details_page.dart +++ b/lib/features/document_details/view/pages/document_details_page.dart @@ -10,7 +10,6 @@ import 'package:intl/intl.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/widgets/highlighted_text.dart'; -import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/document_download_button.dart'; @@ -26,6 +25,7 @@ import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.d import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/util.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:provider/provider.dart'; import 'package:share_plus/share_plus.dart'; class DocumentDetailsPage extends StatefulWidget { @@ -50,7 +50,7 @@ class _DocumentDetailsPageState extends State { return WillPopScope( onWillPop: () async { Navigator.of(context) - .pop(BlocProvider.of(context).state.document); + .pop(context.read().state.document); return false; }, child: DefaultTabController( @@ -106,9 +106,7 @@ class _DocumentDetailsPageState extends State { .black, //TODO: check if there is a way to dynamically determine color... ), onPressed: () => Navigator.of(context).pop( - BlocProvider.of(context) - .state - .document, + context.read().state.document, ), ), floating: true, @@ -185,29 +183,18 @@ class _DocumentDetailsPageState extends State { Future _onEdit(DocumentModel document) async { { - final cubit = BlocProvider.of(context); + final cubit = context.read(); Navigator.push( context, MaterialPageRoute( builder: (context) => BlocProvider( create: (context) => EditDocumentCubit( document, - documentsApi: getIt(), - correspondentRepository: - RepositoryProvider.of>( - context, - ), - documentTypeRepository: - RepositoryProvider.of>( - context, - ), - storagePathRepository: - RepositoryProvider.of>( - context, - ), - tagRepository: RepositoryProvider.of>( - context, - ), + documentsApi: context.watch(), + correspondentRepository: context.watch(), + documentTypeRepository: context.watch(), + storagePathRepository: context.watch(), + tagRepository: context.watch(), ), child: BlocListener( listenWhen: (previous, current) => @@ -226,7 +213,7 @@ class _DocumentDetailsPageState extends State { Widget _buildDocumentMetaDataView(DocumentModel document) { return FutureBuilder( - future: getIt().getMetaData(document), + future: context.read().getMetaData(document), builder: (context, snapshot) { if (!snapshot.hasData) { return const Center(child: CircularProgressIndicator()); @@ -281,7 +268,7 @@ class _DocumentDetailsPageState extends State { Future _assignAsn(DocumentModel document) async { try { - await BlocProvider.of(context).assignAsn(document); + await context.read().assignAsn(document); } on PaperlessServerException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } @@ -393,7 +380,7 @@ class _DocumentDetailsPageState extends State { /// Future _onShare(DocumentModel document) async { Uint8List documentBytes = - await getIt().download(document); + await context.read().download(document); final dir = await getTemporaryDirectory(); final String path = "${dir.path}/${document.originalFileName}"; await File(path).writeAsBytes(documentBytes); @@ -419,7 +406,7 @@ class _DocumentDetailsPageState extends State { false; if (delete) { try { - await BlocProvider.of(context).delete(document); + await context.read().delete(document); showSnackBar(context, S.of(context).documentDeleteSuccessMessage); } on PaperlessServerException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); @@ -434,7 +421,8 @@ class _DocumentDetailsPageState extends State { Navigator.of(context).push( MaterialPageRoute( builder: (context) => DocumentView( - documentBytes: getIt().getPreview(document.id), + documentBytes: + context.read().getPreview(document.id), ), ), ); diff --git a/lib/features/document_details/view/widgets/document_download_button.dart b/lib/features/document_details/view/widgets/document_download_button.dart index 614d825..235c3c8 100644 --- a/lib/features/document_details/view/widgets/document_download_button.dart +++ b/lib/features/document_details/view/widgets/document_download_button.dart @@ -3,10 +3,10 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/service/file_service.dart'; -import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/util.dart'; +import 'package:provider/provider.dart'; class DocumentDownloadButton extends StatefulWidget { final DocumentModel? document; @@ -43,7 +43,8 @@ class _DocumentDownloadButtonState extends State { } setState(() => _isDownloadPending = true); try { - final bytes = await getIt().download(document); + final bytes = + await context.read().download(document); final Directory dir = await FileService.downloadsDirectory; String filePath = "${dir.path}/${document.originalFileName}"; //TODO: Add replacement mechanism here (ask user if file should be replaced if exists) diff --git a/lib/features/document_upload/cubit/document_upload_cubit.dart b/lib/features/document_upload/cubit/document_upload_cubit.dart index 8ea94a4..3622f5e 100644 --- a/lib/features/document_upload/cubit/document_upload_cubit.dart +++ b/lib/features/document_upload/cubit/document_upload_cubit.dart @@ -70,8 +70,6 @@ class DocumentUploadCubit extends Cubit { documentType: documentType, tags: tags, createdAt: createdAt, - authToken: auth.token!, - serverUrl: auth.serverUrl, ); if (onConsumptionFinished != null) { _documentApi diff --git a/lib/features/document_upload/view/document_upload_preparation_page.dart b/lib/features/document_upload/view/document_upload_preparation_page.dart index cf01b3f..5faa053 100644 --- a/lib/features/document_upload/view/document_upload_preparation_page.dart +++ b/lib/features/document_upload/view/document_upload_preparation_page.dart @@ -166,10 +166,8 @@ class _DocumentUploadPreparationPageState notAssignedSelectable: false, formBuilderState: _formKey.currentState, labelCreationWidgetBuilder: (initialName) => - RepositoryProvider.value( - value: RepositoryProvider.of>( - context, - ), + RepositoryProvider>( + create: (context) => context.watch(), child: AddDocumentTypePage(initialName: initialName), ), textFieldLabel: @@ -182,11 +180,8 @@ class _DocumentUploadPreparationPageState notAssignedSelectable: false, formBuilderState: _formKey.currentState, labelCreationWidgetBuilder: (initialName) => - RepositoryProvider.value( - value: - RepositoryProvider.of>( - context, - ), + RepositoryProvider>( + create: (context) => context.watch(), child: AddCorrespondentPage(initialName: initialName), ), textFieldLabel: @@ -220,7 +215,7 @@ class _DocumentUploadPreparationPageState void _onSubmit() async { if (_formKey.currentState?.saveAndValidate() ?? false) { - final cubit = BlocProvider.of(context); + final cubit = context.read(); try { setState(() => _isUploadLoading = true); diff --git a/lib/features/documents/view/pages/document_edit_page.dart b/lib/features/documents/view/pages/document_edit_page.dart index 954b2ea..af971bf 100644 --- a/lib/features/documents/view/pages/document_edit_page.dart +++ b/lib/features/documents/view/pages/document_edit_page.dart @@ -99,8 +99,9 @@ class _DocumentEditPageState extends State { return LabelFormField( notAssignedSelectable: false, formBuilderState: _formKey.currentState, - labelCreationWidgetBuilder: (initialValue) => RepositoryProvider.value( - value: RepositoryProvider.of>(context), + labelCreationWidgetBuilder: (initialValue) => + RepositoryProvider>( + create: (context) => context.watch(), child: AddStoragePathPage(initalValue: initialValue), ), textFieldLabel: S.of(context).documentStoragePathPropertyLabel, @@ -116,10 +117,9 @@ class _DocumentEditPageState extends State { return LabelFormField( notAssignedSelectable: false, formBuilderState: _formKey.currentState, - labelCreationWidgetBuilder: (initialValue) => RepositoryProvider.value( - value: RepositoryProvider.of>( - context, - ), + labelCreationWidgetBuilder: (initialValue) => + RepositoryProvider>( + create: context.watch(), child: AddCorrespondentPage(initialName: initialValue), ), textFieldLabel: S.of(context).documentCorrespondentPropertyLabel, @@ -135,10 +135,9 @@ class _DocumentEditPageState extends State { return LabelFormField( notAssignedSelectable: false, formBuilderState: _formKey.currentState, - labelCreationWidgetBuilder: (currentInput) => RepositoryProvider.value( - value: RepositoryProvider.of>( - context, - ), + labelCreationWidgetBuilder: (currentInput) => + RepositoryProvider>( + create: (context) => context.watch(), child: AddDocumentTypePage( initialName: currentInput, ), @@ -170,8 +169,7 @@ class _DocumentEditPageState extends State { _isSubmitLoading = true; }); try { - await BlocProvider.of(context) - .updateDocument(mergedDocument); + await context.read().updateDocument(mergedDocument); showSnackBar(context, S.of(context).documentUpdateSuccessMessage); } on PaperlessServerException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index dbd1724..c1edfa3 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -4,7 +4,6 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; -import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; @@ -23,6 +22,7 @@ import 'package:paperless_mobile/features/settings/bloc/application_settings_cub import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; import 'package:paperless_mobile/features/settings/model/view_type.dart'; import 'package:paperless_mobile/util.dart'; +import 'package:provider/provider.dart'; class DocumentsPage extends StatefulWidget { const DocumentsPage({Key? key}) : super(key: key); @@ -42,8 +42,8 @@ class _DocumentsPageState extends State { @override void initState() { super.initState(); - _documentsCubit = BlocProvider.of(context); - _savedViewCubit = BlocProvider.of(context); + _documentsCubit = context.watch(); + _savedViewCubit = context.watch(); try { _documentsCubit.load(); } on PaperlessServerException catch (error, stackTrace) { @@ -249,8 +249,11 @@ class _DocumentsPageState extends State { MaterialPageRoute _buildDetailsPageRoute( DocumentModel document) { return MaterialPageRoute( - builder: (_) => BlocProvider.value( - value: DocumentDetailsCubit(getIt(), document), + builder: (_) => BlocProvider( + create: (context) => DocumentDetailsCubit( + context.watch(), + document, + ), child: const LabelRepositoriesProvider( child: DocumentDetailsPage(), ), diff --git a/lib/features/documents/view/widgets/document_preview.dart b/lib/features/documents/view/widgets/document_preview.dart index 482a61b..cdad066 100644 --- a/lib/features/documents/view/widgets/document_preview.dart +++ b/lib/features/documents/view/widgets/document_preview.dart @@ -2,7 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/di_initializer.dart'; +import 'package:provider/provider.dart'; import 'package:shimmer/shimmer.dart'; class DocumentPreview extends StatelessWidget { @@ -30,14 +30,15 @@ class DocumentPreview extends StatelessWidget { fit: fit, alignment: Alignment.topCenter, cacheKey: "thumb_$id", - imageUrl: getIt().getThumbnailUrl(id), + imageUrl: + Provider.of(context).getThumbnailUrl(id), errorWidget: (ctxt, msg, __) => Text(msg), placeholder: (context, value) => Shimmer.fromColors( baseColor: Colors.grey[300]!, highlightColor: Colors.grey[100]!, child: const SizedBox(height: 100, width: 100), ), - cacheManager: getIt(), + cacheManager: context.watch(), ), // ), ); diff --git a/lib/features/documents/view/widgets/sort_documents_button.dart b/lib/features/documents/view/widgets/sort_documents_button.dart index 04d9734..3f06569 100644 --- a/lib/features/documents/view/widgets/sort_documents_button.dart +++ b/lib/features/documents/view/widgets/sort_documents_button.dart @@ -28,7 +28,7 @@ class SortDocumentsButton extends StatelessWidget { topRight: Radius.circular(16), ), ), - builder: (_) => BlocProvider.value( + builder: (_) => BlocProvider.value( value: BlocProvider.of(context), child: FractionallySizedBox( heightFactor: .6, diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index cdbdf01..0e75904 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -5,7 +5,6 @@ import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; -import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/view/pages/documents_page.dart'; import 'package:paperless_mobile/features/home/view/widget/bottom_navigation_bar.dart'; @@ -15,6 +14,7 @@ import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart'; import 'package:paperless_mobile/features/scan/view/scanner_page.dart'; import 'package:paperless_mobile/util.dart'; +import 'package:provider/provider.dart'; class HomePage extends StatefulWidget { const HomePage({Key? key}) : super(key: key); @@ -56,7 +56,8 @@ class _HomePageState extends State { MultiBlocProvider( providers: [ BlocProvider.value( - value: DocumentsCubit(getIt()), + value: + DocumentsCubit(Provider.of(context)), ), BlocProvider( create: (context) => SavedViewCubit( @@ -70,8 +71,10 @@ class _HomePageState extends State { value: _scannerCubit, child: const ScannerPage(), ), - BlocProvider.value( - value: DocumentsCubit(getIt()), + BlocProvider( + create: (context) => DocumentsCubit( + Provider.of(context), + ), child: const LabelsPage(), ), ][_currentIndex], @@ -81,13 +84,12 @@ class _HomePageState extends State { void _initializeData(BuildContext context) { try { - RepositoryProvider.of>(context).findAll(); - RepositoryProvider.of>(context).findAll(); - RepositoryProvider.of>(context).findAll(); - RepositoryProvider.of>(context).findAll(); - RepositoryProvider.of(context).findAll(); - BlocProvider.of(context) - .updateInformtion(); + context.read>().findAll(); + context.read>().findAll(); + context.read>().findAll(); + context.read>().findAll(); + context.read().findAll(); + context.read().updateInformtion(); } on PaperlessServerException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } diff --git a/lib/features/home/view/widget/info_drawer.dart b/lib/features/home/view/widget/info_drawer.dart index 7e167a6..f367755 100644 --- a/lib/features/home/view/widget/info_drawer.dart +++ b/lib/features/home/view/widget/info_drawer.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:package_info_plus/package_info_plus.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart'; import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart'; @@ -7,7 +8,6 @@ import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; import 'package:paperless_mobile/core/store/local_vault.dart'; -import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart'; import 'package:paperless_mobile/features/inbox/view/pages/inbox_page.dart'; @@ -16,14 +16,29 @@ import 'package:paperless_mobile/features/settings/bloc/application_settings_cub import 'package:paperless_mobile/features/settings/view/settings_page.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/util.dart'; +import 'package:provider/provider.dart'; import 'package:url_launcher/link.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:collection/collection.dart'; -class InfoDrawer extends StatelessWidget { +class InfoDrawer extends StatefulWidget { final VoidCallback? afterInboxClosed; const InfoDrawer({Key? key, this.afterInboxClosed}) : super(key: key); + + @override + State createState() => _InfoDrawerState(); +} + +class _InfoDrawerState extends State { + late final Future _packageInfo; + + @override + void initState() { + super.initState(); + _packageInfo = PackageInfo.fromPlatform(); + } + @override Widget build(BuildContext context) { return ClipRRect( @@ -140,8 +155,9 @@ class InfoDrawer extends StatelessWidget { ), onTap: () => Navigator.of(context).push( MaterialPageRoute( - builder: (context) => BlocProvider.value( - value: getIt(), + builder: (context) => BlocProvider( + create: (context) => + Provider.of(context), child: const SettingsPage(), ), ), @@ -155,44 +171,51 @@ class InfoDrawer extends StatelessWidget { 'https://github.com/astubenbord/paperless-mobile/issues/new'); }, ), - AboutListTile( - icon: const Icon(Icons.info), - applicationIcon: const ImageIcon( - AssetImage('assets/logos/paperless_logo_green.png')), - applicationName: 'Paperless Mobile', - applicationVersion: - kPackageInfo.version + '+' + kPackageInfo.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), + FutureBuilder( + future: _packageInfo, + builder: (context, snapshot) { + return AboutListTile( + icon: const Icon(Icons.info), + applicationIcon: const ImageIcon( + AssetImage('assets/logos/paperless_logo_green.png'), ), - ), - ), - const SizedBox(height: 16), - Text( - 'Credits', - style: Theme.of(context).textTheme.titleMedium, - ), - _buildOnboardingImageCredits(), - ], - child: Text(S.of(context).appDrawerAboutLabel), - ), + 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), onTap: () { try { BlocProvider.of(context).logout(); - getIt().clear(); + Provider.of(context).clear(); BlocProvider.of(context).clear(); RepositoryProvider.of>(context) .clear(); @@ -224,7 +247,7 @@ class InfoDrawer extends StatelessWidget { child: BlocProvider( create: (context) => InboxCubit( RepositoryProvider.of>(context), - getIt(), + Provider.of(context), )..loadInbox(), child: const InboxPage(), ), @@ -232,7 +255,7 @@ class InfoDrawer extends StatelessWidget { maintainState: false, ), ); - afterInboxClosed?.call(); + widget.afterInboxClosed?.call(); } Link _buildOnboardingImageCredits() { diff --git a/lib/features/inbox/view/widgets/inbox_item.dart b/lib/features/inbox/view/widgets/inbox_item.dart index 9413a18..5b57538 100644 --- a/lib/features/inbox/view/widgets/inbox_item.dart +++ b/lib/features/inbox/view/widgets/inbox_item.dart @@ -3,11 +3,11 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; -import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; import 'package:paperless_mobile/features/documents/view/widgets/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; @@ -48,9 +48,9 @@ class InboxItem extends StatelessWidget { onTap: () => Navigator.push( context, MaterialPageRoute( - builder: (_) => BlocProvider.value( - value: DocumentDetailsCubit( - getIt(), + builder: (_) => BlocProvider( + create: (context) => DocumentDetailsCubit( + Provider.of(context), document, ), child: const LabelRepositoriesProvider( diff --git a/lib/features/labels/view/widgets/label_item.dart b/lib/features/labels/view/widgets/label_item.dart index 222b0e2..482b5de 100644 --- a/lib/features/labels/view/widgets/label_item.dart +++ b/lib/features/labels/view/widgets/label_item.dart @@ -1,9 +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/di_initializer.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; @@ -45,9 +45,9 @@ class LabelItem extends StatelessWidget { Navigator.push( context, MaterialPageRoute( - builder: (context) => BlocProvider.value( - value: LinkedDocumentsCubit( - getIt(), + builder: (context) => BlocProvider( + create: (context) => LinkedDocumentsCubit( + Provider.of(context), filter, ), child: const LinkedDocumentsPage(), 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 f24b303..183c878 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 @@ -3,13 +3,13 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart'; -import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart'; import 'package:paperless_mobile/features/linked_documents_preview/bloc/linked_documents_cubit.dart'; import 'package:paperless_mobile/features/linked_documents_preview/bloc/state/linked_documents_state.dart'; import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:provider/provider.dart'; class LinkedDocumentsPage extends StatefulWidget { const LinkedDocumentsPage({super.key}); @@ -63,10 +63,10 @@ class _LinkedDocumentsPageState extends State { Navigator.push( context, MaterialPageRoute( - builder: (context) => - BlocProvider.value( - value: DocumentDetailsCubit( - getIt(), + builder: (context) => BlocProvider( + create: (context) => DocumentDetailsCubit( + Provider.of( + context), document, ), child: const DocumentDetailsPage( diff --git a/lib/features/login/bloc/authentication_cubit.dart b/lib/features/login/bloc/authentication_cubit.dart index e9e1657..fbb6f9d 100644 --- a/lib/features/login/bloc/authentication_cubit.dart +++ b/lib/features/login/bloc/authentication_cubit.dart @@ -1,24 +1,28 @@ import 'dart:io'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/security/security_context_aware_dio_manager.dart'; import 'package:paperless_mobile/core/store/local_vault.dart'; -import 'package:paperless_mobile/di_initializer.dart'; +import 'package:paperless_mobile/features/login/bloc/authentication_state.dart'; import 'package:paperless_mobile/features/login/model/authentication_information.dart'; import 'package:paperless_mobile/features/login/model/client_certificate.dart'; import 'package:paperless_mobile/features/login/model/user_credentials.model.dart'; import 'package:paperless_mobile/features/login/services/authentication_service.dart'; import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; -class AuthenticationCubit extends Cubit { +class AuthenticationCubit extends HydratedCubit { final LocalAuthenticationService _localAuthService; - PaperlessAuthenticationApi _authApi; + final PaperlessAuthenticationApi _authApi; final LocalVault _localVault; + final SecurityContextAwareDioManager _dioWrapper; AuthenticationCubit( this._localVault, this._localAuthService, this._authApi, + this._dioWrapper, ) : super(AuthenticationState.initial); Future login({ @@ -28,22 +32,21 @@ class AuthenticationCubit extends Cubit { }) async { assert(credentials.username != null && credentials.password != null); try { - await registerSecurityContext(clientCertificate); - //TODO: Workaround for new architecture, listen for security context changes in timeout_client, possibly persisted in hive. - _authApi = getIt(); - // Store information required to make requests - final currentAuth = AuthenticationInformation( - serverUrl: serverUrl, + _dioWrapper.updateSettings( + baseUrl: serverUrl, clientCertificate: clientCertificate, ); - await _localVault.storeAuthenticationInformation(currentAuth); final token = await _authApi.login( username: credentials.username!, password: credentials.password!, ); - final auth = currentAuth.copyWith(token: token); + final auth = AuthenticationInformation( + serverUrl: serverUrl, + clientCertificate: clientCertificate, + token: token, + ); await _localVault.storeAuthenticationInformation(auth); @@ -83,9 +86,9 @@ class AuthenticationCubit extends Cubit { final localAuthSuccess = await _localAuthService .authenticateLocalUser("Authenticate to log back in"); if (localAuthSuccess) { - await registerSecurityContext(storedAuth.clientCertificate); - //TODO: Workaround for new architecture, listen for security context changes in timeout_client, possibly persisted in hive. - _authApi = getIt(); + _dioWrapper.updateSettings( + clientCertificate: storedAuth.clientCertificate, + ); return emit( AuthenticationState( isAuthenticated: true, @@ -102,7 +105,9 @@ class AuthenticationCubit extends Cubit { )); } } else { - await registerSecurityContext(storedAuth.clientCertificate); + _dioWrapper.updateSettings( + clientCertificate: storedAuth.clientCertificate, + ); final authState = AuthenticationState( isAuthenticated: true, authentication: storedAuth, @@ -115,40 +120,14 @@ class AuthenticationCubit extends Cubit { Future logout() async { await _localVault.clear(); + await super.clear(); emit(AuthenticationState.initial); } -} - -class AuthenticationState { - final bool wasLoginStored; - final bool? wasLocalAuthenticationSuccessful; - final bool isAuthenticated; - final AuthenticationInformation? authentication; - - static final AuthenticationState initial = AuthenticationState( - wasLoginStored: false, - isAuthenticated: false, - ); - - AuthenticationState({ - required this.isAuthenticated, - required this.wasLoginStored, - this.wasLocalAuthenticationSuccessful, - this.authentication, - }); - - AuthenticationState copyWith({ - bool? wasLoginStored, - bool? isAuthenticated, - AuthenticationInformation? authentication, - bool? wasLocalAuthenticationSuccessful, - }) { - return AuthenticationState( - isAuthenticated: isAuthenticated ?? this.isAuthenticated, - wasLoginStored: wasLoginStored ?? this.wasLoginStored, - authentication: authentication ?? this.authentication, - wasLocalAuthenticationSuccessful: wasLocalAuthenticationSuccessful ?? - this.wasLocalAuthenticationSuccessful, - ); - } + + @override + AuthenticationState? fromJson(Map json) => + AuthenticationState.fromJson(json); + + @override + Map? toJson(AuthenticationState state) => state.toJson(); } diff --git a/lib/features/login/bloc/authentication_state.dart b/lib/features/login/bloc/authentication_state.dart new file mode 100644 index 0000000..ef244f4 --- /dev/null +++ b/lib/features/login/bloc/authentication_state.dart @@ -0,0 +1,44 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:paperless_mobile/features/login/model/authentication_information.dart'; + +part 'authentication_state.g.dart'; + +@JsonSerializable() +class AuthenticationState { + final bool wasLoginStored; + final bool? wasLocalAuthenticationSuccessful; + final bool isAuthenticated; + final AuthenticationInformation? authentication; + + static final AuthenticationState initial = AuthenticationState( + wasLoginStored: false, + isAuthenticated: false, + ); + + AuthenticationState({ + required this.isAuthenticated, + required this.wasLoginStored, + this.wasLocalAuthenticationSuccessful, + this.authentication, + }); + + AuthenticationState copyWith({ + bool? wasLoginStored, + bool? isAuthenticated, + AuthenticationInformation? authentication, + bool? wasLocalAuthenticationSuccessful, + }) { + return AuthenticationState( + isAuthenticated: isAuthenticated ?? this.isAuthenticated, + wasLoginStored: wasLoginStored ?? this.wasLoginStored, + authentication: authentication ?? this.authentication, + wasLocalAuthenticationSuccessful: wasLocalAuthenticationSuccessful ?? + this.wasLocalAuthenticationSuccessful, + ); + } + + factory AuthenticationState.fromJson(Map json) => + _$AuthenticationStateFromJson(json); + + Map toJson() => _$AuthenticationStateToJson(this); +} diff --git a/lib/features/login/bloc/authentication_state.g.dart b/lib/features/login/bloc/authentication_state.g.dart new file mode 100644 index 0000000..79c879d --- /dev/null +++ b/lib/features/login/bloc/authentication_state.g.dart @@ -0,0 +1,29 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'authentication_state.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AuthenticationState _$AuthenticationStateFromJson(Map json) => + AuthenticationState( + isAuthenticated: json['isAuthenticated'] as bool, + wasLoginStored: json['wasLoginStored'] as bool, + wasLocalAuthenticationSuccessful: + json['wasLocalAuthenticationSuccessful'] as bool?, + authentication: json['authentication'] == null + ? null + : AuthenticationInformation.fromJson( + json['authentication'] as Map), + ); + +Map _$AuthenticationStateToJson( + AuthenticationState instance) => + { + 'wasLoginStored': instance.wasLoginStored, + 'wasLocalAuthenticationSuccessful': + instance.wasLocalAuthenticationSuccessful, + 'isAuthenticated': instance.isAuthenticated, + 'authentication': instance.authentication, + }; diff --git a/lib/features/login/model/authentication_information.dart b/lib/features/login/model/authentication_information.dart index be838c3..d8b1efc 100644 --- a/lib/features/login/model/authentication_information.dart +++ b/lib/features/login/model/authentication_information.dart @@ -1,11 +1,10 @@ -import 'package:paperless_mobile/core/type/types.dart'; +import 'package:json_annotation/json_annotation.dart'; import 'package:paperless_mobile/features/login/model/client_certificate.dart'; -class AuthenticationInformation { - static const tokenKey = 'token'; - static const serverUrlKey = 'serverUrl'; - static const clientCertificateKey = 'clientCertificate'; +part 'authentication_information.g.dart'; +@JsonSerializable() +class AuthenticationInformation { final String? token; final String serverUrl; final ClientCertificate? clientCertificate; @@ -16,21 +15,6 @@ class AuthenticationInformation { this.clientCertificate, }); - AuthenticationInformation.fromJson(JSON json) - : token = json[tokenKey], - serverUrl = json[serverUrlKey], - clientCertificate = json[clientCertificateKey] != null - ? ClientCertificate.fromJson(json[clientCertificateKey]) - : null; - - JSON toJson() { - return { - tokenKey: token, - serverUrlKey: serverUrl, - clientCertificateKey: clientCertificate?.toJson(), - }; - } - bool get isValid { return serverUrl.isNotEmpty && (token?.isNotEmpty ?? false); } @@ -48,4 +32,9 @@ class AuthenticationInformation { (removeClientCertificate ? null : this.clientCertificate), ); } + + factory AuthenticationInformation.fromJson(Map json) => + _$AuthenticationInformationFromJson(json); + + Map toJson() => _$AuthenticationInformationToJson(this); } diff --git a/lib/features/login/model/authentication_information.g.dart b/lib/features/login/model/authentication_information.g.dart new file mode 100644 index 0000000..5538e32 --- /dev/null +++ b/lib/features/login/model/authentication_information.g.dart @@ -0,0 +1,26 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'authentication_information.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AuthenticationInformation _$AuthenticationInformationFromJson( + Map json) => + AuthenticationInformation( + token: json['token'] as String?, + serverUrl: json['serverUrl'] as String, + clientCertificate: json['clientCertificate'] == null + ? null + : ClientCertificate.fromJson( + json['clientCertificate'] as Map), + ); + +Map _$AuthenticationInformationToJson( + AuthenticationInformation instance) => + { + 'token': instance.token, + 'serverUrl': instance.serverUrl, + 'clientCertificate': instance.clientCertificate, + }; 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 1b5488d..176b380 100644 --- a/lib/features/login/view/widgets/server_address_form_field.dart +++ b/lib/features/login/view/widgets/server_address_form_field.dart @@ -1,9 +1,9 @@ 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/di_initializer.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; +import 'package:provider/provider.dart'; class ServerAddressFormField extends StatefulWidget { static const String fkServerAddress = "serverAddress"; @@ -64,7 +64,8 @@ class _ServerAddressFormFieldState extends State { //https://stackoverflow.com/questions/49648022/check-whether-there-is-an-internet-connection-available-on-flutter-app setState(() => _reachabilityStatus = ReachabilityStatus.testing); final isReachable = - await getIt().isServerReachable(address); + await Provider.of(context, listen: false) + .isServerReachable(address); if (isReachable) { setState(() => _reachabilityStatus = ReachabilityStatus.reachable); } else { diff --git a/lib/features/scan/bloc/document_scanner_cubit.dart b/lib/features/scan/bloc/document_scanner_cubit.dart index 1423dcc..87607c4 100644 --- a/lib/features/scan/bloc/document_scanner_cubit.dart +++ b/lib/features/scan/bloc/document_scanner_cubit.dart @@ -6,7 +6,6 @@ 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/di_initializer.dart'; import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart'; class DocumentScannerCubit extends Cubit> { diff --git a/lib/features/scan/view/scanner_page.dart b/lib/features/scan/view/scanner_page.dart index bf2ea54..8b88e97 100644 --- a/lib/features/scan/view/scanner_page.dart +++ b/lib/features/scan/view/scanner_page.dart @@ -16,7 +16,6 @@ import 'package:paperless_mobile/core/repository/provider/label_repositories_pro import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/core/store/local_vault.dart'; import 'package:paperless_mobile/core/widgets/offline_banner.dart'; -import 'package:paperless_mobile/di_initializer.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'; import 'package:paperless_mobile/features/documents/view/pages/document_view.dart'; @@ -28,6 +27,7 @@ import 'package:paperless_mobile/util.dart'; import 'package:pdf/pdf.dart'; import 'package:pdf/widgets.dart' as pw; import 'package:permission_handler/permission_handler.dart'; +import 'package:provider/provider.dart'; class ScannerPage extends StatefulWidget { const ScannerPage({Key? key}) : super(key: key); @@ -139,8 +139,8 @@ class _ScannerPageState extends State builder: (_) => LabelRepositoriesProvider( child: BlocProvider( create: (context) => DocumentUploadCubit( - localVault: getIt(), - documentApi: getIt(), + localVault: Provider.of(context), + documentApi: Provider.of(context), correspondentRepository: RepositoryProvider.of>( context, @@ -274,8 +274,8 @@ class _ScannerPageState extends State builder: (_) => LabelRepositoriesProvider( child: BlocProvider( create: (context) => DocumentUploadCubit( - localVault: getIt(), - documentApi: getIt(), + localVault: Provider.of(context), + documentApi: Provider.of(context), correspondentRepository: RepositoryProvider.of>( context, diff --git a/lib/features/settings/bloc/application_settings_cubit.dart b/lib/features/settings/bloc/application_settings_cubit.dart index 0b1021b..e62699d 100644 --- a/lib/features/settings/bloc/application_settings_cubit.dart +++ b/lib/features/settings/bloc/application_settings_cubit.dart @@ -1,24 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_mobile/core/store/local_vault.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; -import 'package:injectable/injectable.dart'; import 'package:paperless_mobile/features/settings/model/view_type.dart'; -@prod -@test -@lazySingleton -class ApplicationSettingsCubit extends Cubit { - final LocalVault localVault; - - ApplicationSettingsCubit(this.localVault) - : super(ApplicationSettingsState.defaultSettings); - - Future initialize() async { - final settings = (await localVault.loadApplicationSettings()) ?? - ApplicationSettingsState.defaultSettings; - emit(settings); - } +class ApplicationSettingsCubit extends HydratedCubit { + ApplicationSettingsCubit() : super(ApplicationSettingsState.defaultSettings); Future setLocale(String? localeSubtag) async { final updatedSettings = state.copyWith(preferredLocaleSubtag: localeSubtag); @@ -42,11 +28,20 @@ class ApplicationSettingsCubit extends Cubit { } Future _updateSettings(ApplicationSettingsState settings) async { - await localVault.storeApplicationSettings(settings); emit(settings); } - void clear() { + @override + Future clear() async { + await super.clear(); emit(ApplicationSettingsState.defaultSettings); } + + @override + ApplicationSettingsState? fromJson(Map json) => + ApplicationSettingsState.fromJson(json); + + @override + Map? toJson(ApplicationSettingsState state) => + state.toJson(); } diff --git a/lib/features/settings/model/application_settings_state.dart b/lib/features/settings/model/application_settings_state.dart index c3a963b..05c284d 100644 --- a/lib/features/settings/model/application_settings_state.dart +++ b/lib/features/settings/model/application_settings_state.dart @@ -1,71 +1,45 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:json_annotation/json_annotation.dart'; import 'package:paperless_mobile/core/type/types.dart'; import 'package:paperless_mobile/features/settings/model/view_type.dart'; +part 'application_settings_state.g.dart'; + /// /// State holding the current application settings such as selected language, theme mode and more. /// -/// +@JsonSerializable() class ApplicationSettingsState { static final defaultSettings = ApplicationSettingsState( isLocalAuthenticationEnabled: false, preferredLocaleSubtag: Platform.localeName.split('_').first, preferredThemeMode: ThemeMode.system, preferredViewType: ViewType.list, - showInboxOnStartup: true, ); - static const isLocalAuthenticationEnabledKey = "isLocalAuthenticationEnabled"; - static const preferredLocaleSubtagKey = "localeSubtag"; - static const preferredThemeModeKey = "preferredThemeModeKey"; - static const preferredViewTypeKey = 'preferredViewType'; - static const showInboxOnStartupKey = 'showinboxOnStartup'; - final bool isLocalAuthenticationEnabled; final String preferredLocaleSubtag; final ThemeMode preferredThemeMode; final ViewType preferredViewType; - final bool showInboxOnStartup; ApplicationSettingsState({ required this.preferredLocaleSubtag, required this.preferredThemeMode, required this.isLocalAuthenticationEnabled, required this.preferredViewType, - required this.showInboxOnStartup, }); - JSON toJson() { - return { - isLocalAuthenticationEnabledKey: isLocalAuthenticationEnabled, - preferredLocaleSubtagKey: preferredLocaleSubtag, - preferredThemeModeKey: preferredThemeMode.name, - preferredViewTypeKey: preferredViewType.name, - }; - } - - ApplicationSettingsState.fromJson(JSON json) - : isLocalAuthenticationEnabled = json[isLocalAuthenticationEnabledKey] ?? - defaultSettings.isLocalAuthenticationEnabled, - preferredLocaleSubtag = json[preferredLocaleSubtagKey] ?? - defaultSettings.preferredLocaleSubtag, - preferredThemeMode = json.containsKey(preferredThemeModeKey) - ? ThemeMode.values.byName(json[preferredThemeModeKey]) - : defaultSettings.preferredThemeMode, - preferredViewType = json.containsKey(preferredViewTypeKey) - ? ViewType.values.byName(json[preferredViewTypeKey]) - : defaultSettings.preferredViewType, - showInboxOnStartup = - json[showInboxOnStartupKey] ?? defaultSettings.showInboxOnStartup; + Map toJson() => _$ApplicationSettingsStateToJson(this); + factory ApplicationSettingsState.fromJson(Map json) => + _$ApplicationSettingsStateFromJson(json); ApplicationSettingsState copyWith({ bool? isLocalAuthenticationEnabled, String? preferredLocaleSubtag, ThemeMode? preferredThemeMode, ViewType? preferredViewType, - bool? showInboxOnStartup, }) { return ApplicationSettingsState( isLocalAuthenticationEnabled: @@ -74,7 +48,6 @@ class ApplicationSettingsState { preferredLocaleSubtag ?? this.preferredLocaleSubtag, preferredThemeMode: preferredThemeMode ?? this.preferredThemeMode, preferredViewType: preferredViewType ?? this.preferredViewType, - showInboxOnStartup: showInboxOnStartup ?? this.showInboxOnStartup, ); } } diff --git a/lib/features/settings/model/application_settings_state.g.dart b/lib/features/settings/model/application_settings_state.g.dart new file mode 100644 index 0000000..6166a38 --- /dev/null +++ b/lib/features/settings/model/application_settings_state.g.dart @@ -0,0 +1,39 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'application_settings_state.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ApplicationSettingsState _$ApplicationSettingsStateFromJson( + Map json) => + ApplicationSettingsState( + preferredLocaleSubtag: json['preferredLocaleSubtag'] as String, + preferredThemeMode: + $enumDecode(_$ThemeModeEnumMap, json['preferredThemeMode']), + isLocalAuthenticationEnabled: + json['isLocalAuthenticationEnabled'] as bool, + preferredViewType: + $enumDecode(_$ViewTypeEnumMap, json['preferredViewType']), + ); + +Map _$ApplicationSettingsStateToJson( + ApplicationSettingsState instance) => + { + 'isLocalAuthenticationEnabled': instance.isLocalAuthenticationEnabled, + 'preferredLocaleSubtag': instance.preferredLocaleSubtag, + 'preferredThemeMode': _$ThemeModeEnumMap[instance.preferredThemeMode]!, + 'preferredViewType': _$ViewTypeEnumMap[instance.preferredViewType]!, + }; + +const _$ThemeModeEnumMap = { + ThemeMode.system: 'system', + ThemeMode.light: 'light', + ThemeMode.dark: 'dark', +}; + +const _$ViewTypeEnumMap = { + ViewType.grid: 'grid', + ViewType.list: 'list', +}; diff --git a/lib/features/settings/view/widgets/biometric_authentication_setting.dart b/lib/features/settings/view/widgets/biometric_authentication_setting.dart index be6e63a..f6ab030 100644 --- a/lib/features/settings/view/widgets/biometric_authentication_setting.dart +++ b/lib/features/settings/view/widgets/biometric_authentication_setting.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/features/login/services/authentication_service.dart'; import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:provider/provider.dart'; class BiometricAuthenticationSetting extends StatelessWidget { const BiometricAuthenticationSetting({super.key}); @@ -28,8 +28,9 @@ class BiometricAuthenticationSetting extends StatelessWidget { : S .of(context) .appSettingsDisableBiometricAuthenticationReasonText; - final changeValue = await getIt() - .authenticateLocalUser(localizedReason); + final changeValue = + await Provider.of(context) + .authenticateLocalUser(localizedReason); if (changeValue) { settingsBloc.setIsBiometricAuthenticationEnabled(val); } diff --git a/lib/features/settings/view/widgets/clear_storage_setting.dart b/lib/features/settings/view/widgets/clear_storage_setting.dart index e4cc2bf..c6c6279 100644 --- a/lib/features/settings/view/widgets/clear_storage_setting.dart +++ b/lib/features/settings/view/widgets/clear_storage_setting.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart' as cm; import 'package:paperless_mobile/core/service/file_service.dart'; -import 'package:paperless_mobile/di_initializer.dart'; +import 'package:provider/provider.dart'; class ClearStorageSetting extends StatelessWidget { const ClearStorageSetting({super.key}); @@ -12,12 +12,10 @@ class ClearStorageSetting extends StatelessWidget { title: Text("Clear data"), subtitle: Text("Remove downloaded files, scans and clear the cache's content"), - onTap: _clearCache, + onTap: () { + Provider.of(context).emptyCache(); + FileService.clearUserData(); + }, ); } - - void _clearCache() async { - getIt().emptyCache(); - FileService.clearUserData(); - } } diff --git a/lib/main.dart b/lib/main.dart index 385ae38..4604708 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,25 +1,28 @@ -import 'dart:developer'; -import 'dart:async'; import 'dart:io'; +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart' as cm; 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:http/io_client.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:intl/date_symbol_data_local.dart'; -import 'package:intl/intl.dart'; import 'package:intl/intl_standalone.dart'; +import 'package:local_auth/local_auth.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/bloc_changes_observer.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart'; import 'package:paperless_mobile/core/global/constants.dart'; -import 'package:paperless_mobile/core/global/http_self_signed_certificate_override.dart'; import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart'; +import 'package:paperless_mobile/core/model/paperless_statistics_state.dart'; import 'package:paperless_mobile/core/repository/impl/correspondent_repository_impl.dart'; import 'package:paperless_mobile/core/repository/impl/document_type_repository_impl.dart'; import 'package:paperless_mobile/core/repository/impl/saved_view_repository_impl.dart'; @@ -27,78 +30,146 @@ import 'package:paperless_mobile/core/repository/impl/storage_path_repository_im 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/security_context_aware_dio_manager.dart'; +import 'package:paperless_mobile/core/service/connectivity_status.service.dart'; import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/core/store/local_vault.dart'; -import 'package:paperless_mobile/di_initializer.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'; import 'package:paperless_mobile/features/home/view/home_page.dart'; import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart'; +import 'package:paperless_mobile/features/login/bloc/authentication_state.dart'; import 'package:paperless_mobile/features/login/services/authentication_service.dart'; import 'package:paperless_mobile/features/login/view/login_page.dart'; import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/util.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:provider/provider.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; void main() async { Bloc.observer = BlocChangesObserver(); final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); + + HydratedBloc.storage = await HydratedStorage.build( + storageDirectory: await getApplicationDocumentsDirectory(), + ); FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); - Intl.systemLocale = await findSystemLocale(); + await findSystemLocale(); // Required for self signed client certificates - HttpOverrides.global = X509HttpOverrides(); + final dioWrapper = SecurityContextAwareDioManager(); + IOClient httpClient = IOClient(); - configureDependencies('prod'); + dioWrapper.securityContextChanges.listen( + (context) => httpClient = IOClient(HttpClient(context: context)), + ); + // Initialize External dependencies + final connectivity = Connectivity(); + final encryptedSharedPreferences = EncryptedSharedPreferences(); + final localAuthentication = LocalAuthentication(); + final cacheManager = cm.CacheManager(cm.Config('cacheKey', + fileService: cm.HttpFileService(httpClient: httpClient))); + + // Initialize Paperless APIs + final authApi = PaperlessAuthenticationApiImpl(dioWrapper.client); + final documentsApi = PaperlessDocumentsApiImpl(dioWrapper.client); + final labelsApi = PaperlessLabelApiImpl(dioWrapper.client); + final statsApi = PaperlessServerStatsApiImpl(dioWrapper.client); + final savedViewsApi = PaperlessSavedViewsApiImpl(dioWrapper.client); + + // Initialize other utility classes + final connectivityStatusService = ConnectivityStatusServiceImpl(connectivity); + final localVault = LocalVaultImpl(encryptedSharedPreferences); + final localAuthService = + LocalAuthenticationService(localVault, localAuthentication); + + // Initialize Repositories + + // Initialize Blocs/Cubits + final connectivityCubit = ConnectivityCubit(connectivityStatusService); // Remove temporarily downloaded files. + (await FileService.temporaryDirectory).deleteSync(recursive: true); - kPackageInfo = await PackageInfo.fromPlatform(); // Load application settings and stored authentication data - await getIt().initialize(); - await getIt().initialize(); + await connectivityCubit.initialize(); final authCubit = AuthenticationCubit( - getIt(), - getIt(), - getIt(), + localVault, + localAuthService, + authApi, + dioWrapper, ); - await authCubit.restoreSessionState(); + //TODO: Check if hydrated cubit restores state. + //await authCubit.restoreSessionState(); // Create repositories - final LabelRepository tagRepository = - TagRepositoryImpl(getIt()); - final LabelRepository correspondentRepository = - CorrespondentRepositoryImpl(getIt()); - final LabelRepository documentTypeRepository = - DocumentTypeRepositoryImpl(getIt()); - final LabelRepository storagePathRepository = - StoragePathRepositoryImpl(getIt()); - final SavedViewRepository savedViewRepository = - SavedViewRepositoryImpl(getIt()); + final tagRepository = TagRepositoryImpl(labelsApi); + final correspondentRepository = CorrespondentRepositoryImpl(labelsApi); + final documentTypeRepository = DocumentTypeRepositoryImpl(labelsApi); + final storagePathRepository = StoragePathRepositoryImpl(labelsApi); + final savedViewRepository = SavedViewRepositoryImpl(savedViewsApi); runApp( - MultiRepositoryProvider( + MultiProvider( providers: [ - RepositoryProvider.value(value: tagRepository), - RepositoryProvider.value(value: correspondentRepository), - RepositoryProvider.value(value: documentTypeRepository), - RepositoryProvider.value(value: storagePathRepository), - RepositoryProvider.value(value: savedViewRepository), + Provider.value(value: authApi), + Provider.value(value: documentsApi), + Provider.value(value: labelsApi), + Provider.value(value: statsApi), + Provider.value(value: savedViewsApi), + Provider.value(value: cacheManager), + Provider.value(value: localVault), + Provider.value( + value: connectivityStatusService, + ), ], - child: PaperlessMobileEntrypoint(authenticationCubit: authCubit), + child: MultiRepositoryProvider( + providers: [ + RepositoryProvider>.value( + value: tagRepository, + ), + RepositoryProvider>.value( + value: correspondentRepository, + ), + RepositoryProvider>.value( + value: documentTypeRepository, + ), + RepositoryProvider>.value( + value: storagePathRepository, + ), + RepositoryProvider.value( + value: savedViewRepository, + ), + ], + child: MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => AuthenticationCubit( + localVault, + localAuthService, + authApi, + dioWrapper, + ), + ), + BlocProvider.value( + value: connectivityCubit, + ), + ], + child: const PaperlessMobileEntrypoint(), + ), + ), ), ); } class PaperlessMobileEntrypoint extends StatefulWidget { - final AuthenticationCubit authenticationCubit; const PaperlessMobileEntrypoint({ Key? key, - required this.authenticationCubit, }) : super(key: key); @override @@ -153,14 +224,14 @@ class _PaperlessMobileEntrypointState extends State { Widget build(BuildContext context) { return MultiBlocProvider( providers: [ - BlocProvider.value( - value: getIt(), + BlocProvider( + create: (context) => ConnectivityCubit(context.watch()), ), - BlocProvider.value( - value: getIt(), + BlocProvider( + create: (context) => PaperlessServerInformationCubit(context.watch()), ), - BlocProvider.value( - value: getIt(), + BlocProvider( + create: (context) => ApplicationSettingsCubit(), ), ], child: BlocBuilder( @@ -188,10 +259,7 @@ class _PaperlessMobileEntrypointState extends State { GlobalWidgetsLocalizations.delegate, FormBuilderLocalizations.delegate, ], - home: BlocProvider.value( - value: widget.authenticationCubit, - child: const AuthenticationWrapper(), - ), + home: const AuthenticationWrapper(), ); }, ), @@ -247,19 +315,11 @@ class _AuthenticationWrapperState extends State { MaterialPageRoute( builder: (context) => BlocProvider( create: (BuildContext context) => DocumentUploadCubit( - localVault: getIt(), - documentApi: getIt(), - tagRepository: RepositoryProvider.of>( - context, - ), - correspondentRepository: - RepositoryProvider.of>( - context, - ), - documentTypeRepository: - RepositoryProvider.of>( - context, - ), + localVault: context.watch(), + documentApi: context.watch(), + tagRepository: context.watch(), + correspondentRepository: context.watch(), + documentTypeRepository: context.watch(), ), child: DocumentUploadPreparationPage( fileBytes: bytes, @@ -349,13 +409,12 @@ class BiometricAuthenticationPage extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ ElevatedButton( - onPressed: () => - BlocProvider.of(context).logout(), + onPressed: () => context.read().logout(), child: const Text("Log out"), ), ElevatedButton( - onPressed: () => BlocProvider.of(context) - .restoreSessionState(), + onPressed: () => + context.read().restoreSessionState(), child: const Text("Authenticate"), ), ], diff --git a/lib/util.dart b/lib/util.dart index f46b36c..131d049 100644 --- a/lib/util.dart +++ b/lib/util.dart @@ -14,7 +14,6 @@ import 'package:paperless_mobile/generated/l10n.dart'; final dateFormat = DateFormat("yyyy-MM-dd"); final GlobalKey rootScaffoldKey = GlobalKey(); -late PackageInfo kPackageInfo; class SnackBarActionConfig { final String label; 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 21ab4a0..9138256 100644 --- a/packages/paperless_api/lib/src/modules/authentication_api/authentication_api_impl.dart +++ b/packages/paperless_api/lib/src/modules/authentication_api/authentication_api_impl.dart @@ -1,12 +1,9 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:http/http.dart'; +import 'package:dio/dio.dart'; import 'package:paperless_api/src/models/paperless_server_exception.dart'; import 'package:paperless_api/src/modules/authentication_api/authentication_api.dart'; class PaperlessAuthenticationApiImpl implements PaperlessAuthenticationApi { - final BaseClient client; + final Dio client; PaperlessAuthenticationApiImpl(this.client); @@ -18,8 +15,8 @@ class PaperlessAuthenticationApiImpl implements PaperlessAuthenticationApi { late Response response; try { response = await client.post( - Uri.parse("/api/token/"), - body: { + "/api/token/", + data: { "username": username, "password": password, }, @@ -34,11 +31,11 @@ class PaperlessAuthenticationApiImpl implements PaperlessAuthenticationApi { ); } } - if (response.statusCode == HttpStatus.ok) { - final data = jsonDecode(utf8.decode(response.bodyBytes)); - return data['token']; - } else if (response.statusCode == HttpStatus.badRequest && - response.body + if (response.statusCode == 200) { + return response.data['token']; + } else if (response.statusCode == 400 && + response + .data //TODO: Check if text is included in statusMessage instead of body .toLowerCase() .contains("no required certificate was sent")) { throw PaperlessServerException( diff --git a/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api.dart b/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api.dart index cb3d15e..829e58b 100644 --- a/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api.dart +++ b/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api.dart @@ -12,12 +12,10 @@ abstract class PaperlessDocumentsApi { Uint8List documentBytes, { required String filename, required String title, - required String authToken, - required String serverUrl, + DateTime? createdAt, int? documentType, int? correspondent, Iterable tags = const [], - DateTime? createdAt, }); Future update(DocumentModel doc); Future findNextAsn(); 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 a18ef92..df2b1a4 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,94 +1,42 @@ import 'dart:convert'; -import 'dart:io'; -import 'dart:math'; -import 'package:http/src/boundary_characters.dart'; + +import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; -import 'package:http/http.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/src/constants.dart'; -import 'package:paperless_api/src/models/bulk_edit_model.dart'; -import 'package:paperless_api/src/models/document_filter.dart'; -import 'package:paperless_api/src/models/document_meta_data_model.dart'; -import 'package:paperless_api/src/models/document_model.dart'; -import 'package:paperless_api/src/models/paged_search_result.dart'; -import 'package:paperless_api/src/models/paperless_server_exception.dart'; -import 'package:paperless_api/src/models/query_parameters/sort_field.dart'; -import 'package:paperless_api/src/models/query_parameters/sort_order.dart'; -import 'package:paperless_api/src/models/similar_document_model.dart'; -import 'paperless_documents_api.dart'; class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { - final BaseClient baseClient; - final HttpClient httpClient; + final Dio client; - PaperlessDocumentsApiImpl(this.baseClient, this.httpClient); + PaperlessDocumentsApiImpl(this.client); @override Future create( Uint8List documentBytes, { required String filename, required String title, - required String authToken, - required String serverUrl, + DateTime? createdAt, int? documentType, int? correspondent, Iterable tags = const [], - DateTime? createdAt, }) async { - // The multipart request has to be generated from scratch as the http library does - // not allow the same key (tags) to be added multiple times. However, this is what the - // paperless api expects, i.e. one block for each tag. - final request = await httpClient.postUrl( - Uri.parse("$serverUrl/api/documents/post_document/"), - ); + final formData = FormData(); - final boundary = _boundaryString(); - - StringBuffer bodyBuffer = StringBuffer(); - - var fields = {}; - fields.putIfAbsent('title', () => title); + formData.fields.add(MapEntry('title', title)); if (createdAt != null) { - fields.putIfAbsent('created', () => apiDateFormat.format(createdAt)); + formData.fields.add(MapEntry('created', apiDateFormat.format(createdAt))); } if (correspondent != null) { - fields.putIfAbsent('correspondent', () => jsonEncode(correspondent)); + formData.fields.add(MapEntry('correspondent', jsonEncode(correspondent))); } if (documentType != null) { - fields.putIfAbsent('document_type', () => jsonEncode(documentType)); + formData.fields.add(MapEntry('document_type', jsonEncode(documentType))); } - - for (final key in fields.keys) { - bodyBuffer.write(_buildMultipartField(key, fields[key]!, boundary)); - } - for (final tag in tags) { - bodyBuffer.write(_buildMultipartField('tags', tag.toString(), boundary)); + formData.fields.add(MapEntry('tags', tag.toString())); } - - bodyBuffer.write("--$boundary" - '\r\nContent-Disposition: form-data; name="document"; filename="$filename"' - "\r\nContent-type: application/octet-stream" - "\r\n\r\n"); - - final closing = "\r\n--$boundary--\r\n"; - - // Set headers - request.headers.set(HttpHeaders.contentTypeHeader, - "multipart/form-data; boundary=$boundary"); - request.headers.set(HttpHeaders.contentLengthHeader, - "${bodyBuffer.length + closing.length + documentBytes.lengthInBytes}"); - request.headers.set(HttpHeaders.authorizationHeader, "Token $authToken"); - - //Write fields to request - request.write(bodyBuffer.toString()); - //Stream file - await request.addStream(Stream.fromIterable(documentBytes.map((e) => [e]))); - // Write closing boundary to request - request.write(closing); - - final response = await request.close(); - + final response = + await client.post('/api/documents/post_document/', data: formData); if (response.statusCode != 200) { throw PaperlessServerException( ErrorCode.documentUploadFailed, @@ -97,38 +45,14 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { } } - String _buildMultipartField(String fieldName, String value, String boundary) { - // ignore: prefer_interpolation_to_compose_strings - return '--$boundary' - '\r\nContent-Disposition: form-data; name="$fieldName"' - '\r\nContent-type: text/plain' - '\r\n\r\n' + - value + - '\r\n'; - } - - String _boundaryString() { - Random _random = Random(); - var prefix = 'dart-http-boundary-'; - var list = List.generate( - 70 - prefix.length, - (index) => boundaryCharacters[_random.nextInt(boundaryCharacters.length)], - growable: false, - ); - return '$prefix${String.fromCharCodes(list)}'; - } - @override Future update(DocumentModel doc) async { - final response = await baseClient.put( - Uri.parse("/api/documents/${doc.id}/"), - body: json.encode(doc.toJson()), - headers: {"Content-Type": "application/json"}, + final response = await client.put( + "/api/documents/${doc.id}/", + data: doc.toJson(), ); if (response.statusCode == 200) { - return DocumentModel.fromJson( - jsonDecode(utf8.decode(response.bodyBytes)) as Map, - ); + return DocumentModel.fromJson(response.data); } else { throw const PaperlessServerException(ErrorCode.documentUpdateFailed); } @@ -137,17 +61,15 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { @override Future> find(DocumentFilter filter) async { final filterParams = filter.toQueryParameters(); - final response = await baseClient.get( - Uri( - path: "/api/documents/", - queryParameters: filterParams, - ), + final response = await client.get( + "/api/documents/", + queryParameters: filterParams, ); if (response.statusCode == 200) { return compute( PagedSearchResult.fromJson, PagedSearchResultJsonSerializer( - jsonDecode(utf8.decode(response.bodyBytes)), + response.data, DocumentModel.fromJson, ), ); @@ -158,8 +80,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { @override Future delete(DocumentModel doc) async { - final response = - await baseClient.delete(Uri.parse("/api/documents/${doc.id}/")); + final response = await client.delete("/api/documents/${doc.id}/"); if (response.statusCode == 204) { return Future.value(doc.id); @@ -178,9 +99,14 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { @override Future getPreview(int documentId) async { - final response = await baseClient.get(Uri.parse(getPreviewUrl(documentId))); + 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.bodyBytes; + return response.data; } throw const PaperlessServerException(ErrorCode.documentPreviewFailed); } @@ -207,10 +133,9 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { @override Future> bulkAction(BulkAction action) async { - final response = await baseClient.post( - Uri.parse("/api/documents/bulk_edit/"), - body: json.encode(action.toJson()), - headers: {'Content-Type': 'application/json'}, + final response = await client.post( + "/api/documents/bulk_edit/", + data: action.toJson(), ); if (response.statusCode == 200) { return action.documentIds; @@ -241,40 +166,48 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { @override Future download(DocumentModel document) async { - final response = await baseClient - .get(Uri.parse("/api/documents/${document.id}/download/")); - return response.bodyBytes; + //TODO: Add missing error handling + final response = await client.get( + "/api/documents/${document.id}/download/", + options: Options(responseType: ResponseType.bytes), + ); + return response.data; } @override Future getMetaData(DocumentModel document) async { - final response = await baseClient - .get(Uri.parse("/api/documents/${document.id}/metadata/")); + final response = + await client.get("/api/documents/${document.id}/metadata/"); return compute( DocumentMetaData.fromJson, - jsonDecode(utf8.decode(response.bodyBytes)) as Map, + response.data as Map, ); } @override Future> autocomplete(String query, [int limit = 10]) async { - final response = await baseClient - .get(Uri.parse("/api/search/autocomplete/?query=$query&limit=$limit}")); + final response = await client.get( + '/api/search/autocomplete/', + queryParameters: { + 'query': query, + 'limit': limit, + }, + ); if (response.statusCode == 200) { - return jsonDecode(utf8.decode(response.bodyBytes)) as List; + return response.data as List; } throw const PaperlessServerException(ErrorCode.autocompleteQueryError); } @override Future> findSimilar(int docId) async { - final response = await baseClient - .get(Uri.parse("/api/documents/?more_like=$docId&pageSize=10")); + final response = + await client.get("/api/documents/?more_like=$docId&pageSize=10"); if (response.statusCode == 200) { return (await compute( PagedSearchResult.fromJson, PagedSearchResultJsonSerializer( - jsonDecode(utf8.decode(response.bodyBytes)), + response.data, SimilarDocumentModel.fromJson, ), )) 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 7ab8548..7c0fab8 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 @@ -1,17 +1,19 @@ import 'dart:convert'; import 'dart:io'; -import 'package:http/http.dart'; +import 'package:dio/dio.dart'; import 'package:paperless_api/src/models/labels/correspondent_model.dart'; import 'package:paperless_api/src/models/labels/document_type_model.dart'; import 'package:paperless_api/src/models/labels/storage_path_model.dart'; import 'package:paperless_api/src/models/labels/tag_model.dart'; import 'package:paperless_api/src/models/paperless_server_exception.dart'; import 'package:paperless_api/src/modules/labels_api/paperless_labels_api.dart'; -import 'package:paperless_api/src/utils.dart'; +import 'package:paperless_api/src/request_utils.dart'; +//Notes: +// Removed content type json header class PaperlessLabelApiImpl implements PaperlessLabelsApi { - final BaseClient client; + final Dio client; PaperlessLabelApiImpl(this.client); @override @@ -89,15 +91,11 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { @override Future saveCorrespondent(Correspondent correspondent) async { final response = await client.post( - Uri.parse('/api/correspondents/'), - body: jsonEncode(correspondent.toJson()), - headers: {"Content-Type": "application/json"}, - encoding: Encoding.getByName("utf-8"), + '/api/correspondents/', + data: correspondent.toJson(), ); if (response.statusCode == HttpStatus.created) { - return Correspondent.fromJson( - jsonDecode(utf8.decode(response.bodyBytes)), - ); + return Correspondent.fromJson(response.data); } throw PaperlessServerException( ErrorCode.correspondentCreateFailed, @@ -108,15 +106,11 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { @override Future saveDocumentType(DocumentType type) async { final response = await client.post( - Uri.parse('/api/document_types/'), - body: json.encode(type.toJson()), - headers: {"Content-Type": "application/json"}, - encoding: Encoding.getByName("utf-8"), + '/api/document_types/', + data: type.toJson(), ); if (response.statusCode == HttpStatus.created) { - return DocumentType.fromJson( - jsonDecode(utf8.decode(response.bodyBytes)), - ); + return DocumentType.fromJson(response.data); } throw PaperlessServerException( ErrorCode.documentTypeCreateFailed, @@ -126,18 +120,13 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { @override Future saveTag(Tag tag) async { - final body = json.encode(tag.toJson()); final response = await client.post( - Uri.parse('/api/tags/'), - body: body, - headers: { - "Content-Type": "application/json", - "Accept": "application/json; version=2", - }, - encoding: Encoding.getByName("utf-8"), + '/api/tags/', + data: tag.toJson(), + options: Options(headers: {"Accept": "application/json; version=2"}), ); if (response.statusCode == HttpStatus.created) { - return Tag.fromJson(jsonDecode(utf8.decode(response.bodyBytes))); + return Tag.fromJson(response.data); } throw PaperlessServerException( ErrorCode.tagCreateFailed, @@ -148,8 +137,8 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { @override Future deleteCorrespondent(Correspondent correspondent) async { assert(correspondent.id != null); - final response = await client - .delete(Uri.parse('/api/correspondents/${correspondent.id}/')); + final response = + await client.delete('/api/correspondents/${correspondent.id}/'); if (response.statusCode == HttpStatus.noContent) { return correspondent.id!; } @@ -162,8 +151,8 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { @override Future deleteDocumentType(DocumentType documentType) async { assert(documentType.id != null); - final response = await client - .delete(Uri.parse('/api/document_types/${documentType.id}/')); + final response = + await client.delete('/api/document_types/${documentType.id}/'); if (response.statusCode == HttpStatus.noContent) { return documentType.id!; } @@ -176,7 +165,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { @override Future deleteTag(Tag tag) async { assert(tag.id != null); - final response = await client.delete(Uri.parse('/api/tags/${tag.id}/')); + final response = await client.delete('/api/tags/${tag.id}/'); if (response.statusCode == HttpStatus.noContent) { return tag.id!; } @@ -190,17 +179,14 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { Future updateCorrespondent(Correspondent correspondent) async { assert(correspondent.id != null); final response = await client.put( - Uri.parse('/api/correspondents/${correspondent.id}/'), - headers: {"Content-Type": "application/json"}, - body: json.encode(correspondent.toJson()), - encoding: Encoding.getByName("utf-8"), + '/api/correspondents/${correspondent.id}/', + data: json.encode(correspondent.toJson()), ); if (response.statusCode == HttpStatus.ok) { - return Correspondent.fromJson( - jsonDecode(utf8.decode(response.bodyBytes))); + return Correspondent.fromJson(response.data); } throw PaperlessServerException( - ErrorCode.unknown, + ErrorCode.unknown, //TODO: Add correct error code mapping. httpStatusCode: response.statusCode, ); } @@ -209,13 +195,11 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { Future updateDocumentType(DocumentType documentType) async { assert(documentType.id != null); final response = await client.put( - Uri.parse('/api/document_types/${documentType.id}/'), - headers: {"Content-Type": "application/json"}, - body: json.encode(documentType.toJson()), - encoding: Encoding.getByName("utf-8"), + '/api/document_types/${documentType.id}/', + data: documentType.toJson(), ); if (response.statusCode == HttpStatus.ok) { - return DocumentType.fromJson(jsonDecode(utf8.decode(response.bodyBytes))); + return DocumentType.fromJson(response.data); } throw PaperlessServerException( ErrorCode.unknown, @@ -227,16 +211,12 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { Future updateTag(Tag tag) async { assert(tag.id != null); final response = await client.put( - Uri.parse('/api/tags/${tag.id}/'), - headers: { - "Accept": "application/json; version=2", - "Content-Type": "application/json", - }, - body: json.encode(tag.toJson()), - encoding: Encoding.getByName("utf-8"), + '/api/tags/${tag.id}/', + options: Options(headers: {"Accept": "application/json; version=2"}), + data: tag.toJson(), ); if (response.statusCode == HttpStatus.ok) { - return Tag.fromJson(jsonDecode(utf8.decode(response.bodyBytes))); + return Tag.fromJson(response.data); } throw PaperlessServerException( ErrorCode.unknown, @@ -247,8 +227,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { @override Future deleteStoragePath(StoragePath path) async { assert(path.id != null); - final response = - await client.delete(Uri.parse('/api/storage_paths/${path.id}/')); + final response = await client.delete('/api/storage_paths/${path.id}/'); if (response.statusCode == HttpStatus.noContent) { return path.id!; } @@ -285,12 +264,11 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { @override Future saveStoragePath(StoragePath path) async { final response = await client.post( - Uri.parse('/api/storage_paths/'), - body: json.encode(path.toJson()), - headers: {"Content-Type": "application/json"}, + '/api/storage_paths/', + data: path.toJson(), ); if (response.statusCode == HttpStatus.created) { - return StoragePath.fromJson(jsonDecode(utf8.decode(response.bodyBytes))); + return StoragePath.fromJson(jsonDecode(response.data)); } throw PaperlessServerException(ErrorCode.storagePathCreateFailed, httpStatusCode: response.statusCode); @@ -300,12 +278,11 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { Future updateStoragePath(StoragePath path) async { assert(path.id != null); final response = await client.put( - Uri.parse('/api/storage_paths/${path.id}/'), - headers: {"Content-Type": "application/json"}, - body: json.encode(path.toJson()), + '/api/storage_paths/${path.id}/', + data: path.toJson(), ); if (response.statusCode == HttpStatus.ok) { - return StoragePath.fromJson(jsonDecode(utf8.decode(response.bodyBytes))); + return StoragePath.fromJson(jsonDecode(response.data)); } 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 b014552..66cc30e 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,15 +1,15 @@ import 'dart:convert'; import 'dart:io'; -import 'package:http/http.dart'; +import 'package:dio/dio.dart'; import 'package:paperless_api/src/models/paperless_server_exception.dart'; import 'package:paperless_api/src/models/saved_view_model.dart'; -import 'package:paperless_api/src/utils.dart'; +import 'package:paperless_api/src/request_utils.dart'; import 'paperless_saved_views_api.dart'; class PaperlessSavedViewsApiImpl implements PaperlessSavedViewsApi { - final BaseClient client; + final Dio client; PaperlessSavedViewsApiImpl(this.client); @@ -28,12 +28,11 @@ class PaperlessSavedViewsApiImpl implements PaperlessSavedViewsApi { @override Future save(SavedView view) async { final response = await client.post( - Uri.parse("/api/saved_views/"), - body: jsonEncode(view.toJson()), - headers: {'Content-Type': 'application/json'}, + "/api/saved_views/", + data: view.toJson(), ); if (response.statusCode == HttpStatus.created) { - return SavedView.fromJson(jsonDecode(utf8.decode(response.bodyBytes))); + return SavedView.fromJson(response.data); } throw PaperlessServerException( ErrorCode.createSavedViewError, @@ -43,8 +42,7 @@ class PaperlessSavedViewsApiImpl implements PaperlessSavedViewsApi { @override Future delete(SavedView view) async { - final response = - await client.delete(Uri.parse("/api/saved_views/${view.id}/")); + final response = await client.delete("/api/saved_views/${view.id}/"); if (response.statusCode == HttpStatus.noContent) { return view.id!; } diff --git a/packages/paperless_api/lib/src/modules/server_stats_api/paperless_server_stats_api_impl.dart b/packages/paperless_api/lib/src/modules/server_stats_api/paperless_server_stats_api_impl.dart index 5ff7529..0d44ad6 100644 --- a/packages/paperless_api/lib/src/modules/server_stats_api/paperless_server_stats_api_impl.dart +++ b/packages/paperless_api/lib/src/modules/server_stats_api/paperless_server_stats_api_impl.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:dio/dio.dart'; import 'package:http/http.dart'; import 'package:paperless_api/src/models/paperless_server_exception.dart'; import 'package:paperless_api/src/models/paperless_server_information_model.dart'; @@ -13,25 +14,24 @@ import 'paperless_server_stats_api.dart'; /// inbox and total number of documents. /// class PaperlessServerStatsApiImpl implements PaperlessServerStatsApi { - final BaseClient client; + final Dio client; PaperlessServerStatsApiImpl(this.client); @override Future getServerInformation() async { - final response = await client.get(Uri.parse("/api/ui_settings/")); - final version = - response.headers[PaperlessServerInformationModel.versionHeader] ?? - 'unknown'; - final apiVersion = int.tryParse( - response.headers[PaperlessServerInformationModel.apiVersionHeader] ?? - '1'); - final String username = - jsonDecode(utf8.decode(response.bodyBytes))['username']; + final response = await client.get("/api/ui_settings/"); + final version = response + .headers[PaperlessServerInformationModel.versionHeader]?.first ?? + 'unknown'; + final apiVersion = int.tryParse(response + .headers[PaperlessServerInformationModel.apiVersionHeader]?.first ?? + '1'); + final String username = response.data['username']; final String host = response - .headers[PaperlessServerInformationModel.hostHeader] ?? - response.request?.headers[PaperlessServerInformationModel.hostHeader] ?? - ('${response.request?.url.host}:${response.request?.url.port}'); + .headers[PaperlessServerInformationModel.hostHeader]?.first ?? + response.headers[PaperlessServerInformationModel.hostHeader]?.first ?? + ('${response.requestOptions.uri.host}:${response.requestOptions.uri.port}'); return PaperlessServerInformationModel( username: username, version: version, @@ -42,11 +42,9 @@ class PaperlessServerStatsApiImpl implements PaperlessServerStatsApi { @override Future getServerStatistics() async { - final response = await client.get(Uri.parse('/api/statistics/')); + final response = await client.get('/api/statistics/'); if (response.statusCode == 200) { - return PaperlessServerStatisticsModel.fromJson( - jsonDecode(utf8.decode(response.bodyBytes)) as Map, - ); + return PaperlessServerStatisticsModel.fromJson(response.data); } throw const PaperlessServerException.unknown(); } diff --git a/packages/paperless_api/lib/src/utils.dart b/packages/paperless_api/lib/src/request_utils.dart similarity index 76% rename from packages/paperless_api/lib/src/utils.dart rename to packages/paperless_api/lib/src/request_utils.dart index b03e97b..b8f808a 100644 --- a/packages/paperless_api/lib/src/utils.dart +++ b/packages/paperless_api/lib/src/request_utils.dart @@ -1,25 +1,26 @@ -import 'dart:convert'; import 'dart:io'; +import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; -import 'package:http/http.dart'; import 'package:paperless_api/src/models/paperless_server_exception.dart'; Future getSingleResult( String url, T Function(Map) fromJson, ErrorCode errorCode, { - required BaseClient client, + required Dio client, int minRequiredApiVersion = 1, }) async { final response = await client.get( - Uri.parse(url), - headers: {'accept': 'application/json; version=$minRequiredApiVersion'}, + url, + options: Options( + headers: {'accept': 'application/json; version=$minRequiredApiVersion'}, + ), ); if (response.statusCode == HttpStatus.ok) { return compute( fromJson, - jsonDecode(utf8.decode(response.bodyBytes)) as Map, + response.data as Map, ); } throw PaperlessServerException( @@ -32,16 +33,17 @@ Future> getCollection( String url, T Function(Map) fromJson, ErrorCode errorCode, { - required BaseClient client, + required Dio client, int minRequiredApiVersion = 1, }) async { final response = await client.get( - Uri.parse(url), - headers: {'accept': 'application/json; version=$minRequiredApiVersion'}, + url, + options: Options(headers: { + 'accept': 'application/json; version=$minRequiredApiVersion' + }), ); if (response.statusCode == HttpStatus.ok) { - final Map body = - jsonDecode(utf8.decode(response.bodyBytes)); + final Map body = response.data; if (body.containsKey('count')) { if (body['count'] == 0) { return []; diff --git a/packages/paperless_api/pubspec.yaml b/packages/paperless_api/pubspec.yaml index 2d70e17..0084c4c 100644 --- a/packages/paperless_api/pubspec.yaml +++ b/packages/paperless_api/pubspec.yaml @@ -17,7 +17,8 @@ dependencies: http: ^0.13.5 json_annotation: ^4.7.0 intl: ^0.17.0 - + dio: ^4.0.6 + dev_dependencies: flutter_test: sdk: flutter diff --git a/pubspec.lock b/pubspec.lock index ad1aedb..7286c18 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -385,6 +385,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.1" + dio: + dependency: "direct main" + description: + name: dio + sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8" + url: "https://pub.dev" + source: hosted + version: "4.0.6" dots_indicator: dependency: transitive description: @@ -665,7 +673,7 @@ packages: source: sdk version: "0.0.0" get_it: - dependency: "direct main" + dependency: transitive description: name: get_it sha256: "290fde3a86072e4b37dbb03c07bec6126f0ecc28dad403c12ffe2e5a2d751ab7" @@ -736,6 +744,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + hydrated_bloc: + dependency: "direct main" + description: + name: hydrated_bloc + sha256: "5871204f14b24638dc9d18d5b94cf22a66fc4be40756925cafff3a7553c7d7b7" + url: "https://pub.dev" + source: hosted + version: "9.0.0" image: dependency: "direct main" description: @@ -753,7 +769,7 @@ packages: source: hosted version: "3.2.0" injectable: - dependency: "direct main" + dependency: transitive description: name: injectable sha256: "7dab7d341feb40a0590d9ff6261aea9495522005e2c6763f9161a4face916f7b" @@ -814,13 +830,21 @@ packages: source: hosted version: "0.6.5" json_annotation: - dependency: transitive + dependency: "direct main" description: name: json_annotation sha256: "3520fa844009431b5d4491a5a778603520cdc399ab3406332dcc50f93547258c" url: "https://pub.dev" source: hosted version: "4.7.0" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: f3c2c18a7889580f71926f30c1937727c8c7d4f3a435f8f5e8b0ddd25253ef5d + url: "https://pub.dev" + source: hosted + version: "6.5.4" lints: dependency: transitive description: @@ -1221,7 +1245,7 @@ packages: source: hosted version: "4.2.4" provider: - dependency: transitive + dependency: "direct main" description: name: provider sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f @@ -1417,6 +1441,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.6" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" + url: "https://pub.dev" + source: hosted + version: "1.3.3" source_map_stack_trace: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 4d0e67e..b860c71 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,8 +32,6 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter - get_it: ^7.2.0 - injectable: ^2.1.0 encrypted_shared_preferences: ^3.0.0 permission_handler: ^9.2.0 pdf: ^3.8.1 @@ -79,6 +77,11 @@ dependencies: rxdart: ^0.27.7 badges: ^2.0.3 flutter_colorpicker: ^1.0.3 + provider: ^6.0.5 + dio: ^4.0.6 + hydrated_bloc: ^9.0.0 + json_annotation: ^4.7.0 + dev_dependencies: integration_test: @@ -92,6 +95,7 @@ dev_dependencies: dependency_validator: ^3.0.0 intl_utils: ^2.7.0 flutter_lints: ^1.0.0 + json_serializable: ^6.5.4 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec