diff --git a/integration_test/login_integration_test.dart b/integration_test/login_integration_test.dart index a69f092..e5fdfc7 100644 --- a/integration_test/login_integration_test.dart +++ b/integration_test/login_integration_test.dart @@ -45,7 +45,6 @@ void main() async { when(getIt().login( username: testUsername, password: testPassword, - serverUrl: testServerUrl, )).thenAnswer((i) => Future.value("eyTestToken")); await getIt().initialize(); @@ -74,7 +73,6 @@ void main() async { verify(getIt().login( username: testUsername, password: testPassword, - serverUrl: testServerUrl, )).called(1); }); @@ -125,7 +123,6 @@ void main() async { .login( username: testUsername, password: testPassword, - serverUrl: testServerUrl, )); expect( find.textContaining(t.translations.loginPagePasswordValidatorMessageText), @@ -175,7 +172,6 @@ void main() async { .login( username: testUsername, password: testPassword, - serverUrl: testServerUrl, )); expect( find.textContaining(t.translations.loginPageUsernameValidatorMessageText), @@ -224,7 +220,6 @@ void main() async { verifyNever(getIt().login( username: testUsername, password: testPassword, - serverUrl: testServerUrl, )); expect( find.textContaining( diff --git a/lib/core/bloc/connectivity_cubit.dart b/lib/core/bloc/connectivity_cubit.dart index b437d90..0e21828 100644 --- a/lib/core/bloc/connectivity_cubit.dart +++ b/lib/core/bloc/connectivity_cubit.dart @@ -4,7 +4,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_mobile/core/service/connectivity_status.service.dart'; import 'package:injectable/injectable.dart'; -@singleton +@prod +@test +@lazySingleton class ConnectivityCubit extends Cubit { final ConnectivityStatusService connectivityStatusService; StreamSubscription? _sub; diff --git a/lib/core/bloc/document_status_cubit.dart b/lib/core/bloc/document_status_cubit.dart index 25ae3bb..893d091 100644 --- a/lib/core/bloc/document_status_cubit.dart +++ b/lib/core/bloc/document_status_cubit.dart @@ -2,7 +2,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_mobile/core/model/document_processing_status.dart'; import 'package:injectable/injectable.dart'; -@singleton +@prod +@test +@lazySingleton class DocumentStatusCubit extends Cubit { DocumentStatusCubit() : super(null); diff --git a/lib/core/bloc/paperless_server_information_cubit.dart b/lib/core/bloc/paperless_server_information_cubit.dart index 1e3ec45..d5f45ac 100644 --- a/lib/core/bloc/paperless_server_information_cubit.dart +++ b/lib/core/bloc/paperless_server_information_cubit.dart @@ -3,7 +3,9 @@ import 'package:injectable/injectable.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart'; -@singleton +@prod +@test +@lazySingleton class PaperlessServerInformationCubit extends Cubit { final PaperlessServerStatsApi service; diff --git a/lib/core/interceptor/authentication.interceptor.dart b/lib/core/interceptor/authentication.interceptor.dart index c9d0b59..43e2252 100644 --- a/lib/core/interceptor/authentication.interceptor.dart +++ b/lib/core/interceptor/authentication.interceptor.dart @@ -1,14 +1,12 @@ import 'dart:developer'; import 'package:flutter/foundation.dart'; -import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/store/local_vault.dart'; import 'package:http_interceptor/http_interceptor.dart'; import 'package:injectable/injectable.dart'; +import 'package:paperless_mobile/core/store/local_vault.dart'; -@injectable -@dev @prod +@injectable class AuthenticationInterceptor implements InterceptorContract { final LocalVault _localVault; AuthenticationInterceptor(this._localVault); @@ -20,15 +18,15 @@ class AuthenticationInterceptor implements InterceptorContract { if (kDebugMode) { log("Intercepted ${request.method} request to ${request.url.toString()}"); } - if (auth == null) { - throw const PaperlessServerException(ErrorCode.notAuthenticated); - } + return request.copyWith( //Append server Url - url: Uri.parse(auth.serverUrl + request.url.toString()), - headers: auth.token.isEmpty + headers: auth?.token?.isEmpty ?? true ? request.headers - : {...request.headers, 'Authorization': 'Token ${auth.token}'}, + : { + ...request.headers, + 'Authorization': 'Token ${auth!.token}', + }, ); } diff --git a/lib/core/interceptor/base_url_interceptor.dart b/lib/core/interceptor/base_url_interceptor.dart new file mode 100644 index 0000000..43189ce --- /dev/null +++ b/lib/core/interceptor/base_url_interceptor.dart @@ -0,0 +1,28 @@ +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; + + BaseUrlInterceptor(this._localVault); + @override + Future interceptRequest({required BaseRequest request}) async { + final auth = await _localVault.loadAuthenticationInformation(); + if (auth == null) { + throw Exception( + "Authentication information not available, cannot perform request!", + ); + } + return request.copyWith( + url: Uri.parse(auth.serverUrl + request.url.toString()), + ); + } + + @override + Future interceptResponse( + {required BaseResponse response}) async => + response; +} diff --git a/lib/core/interceptor/response_conversion.interceptor.dart b/lib/core/interceptor/response_conversion.interceptor.dart index 44c9c3e..69c3729 100644 --- a/lib/core/interceptor/response_conversion.interceptor.dart +++ b/lib/core/interceptor/response_conversion.interceptor.dart @@ -5,7 +5,6 @@ import 'package:injectable/injectable.dart'; const interceptedRoutes = ['thumb/']; @injectable -@dev @prod class ResponseConversionInterceptor implements InterceptorContract { @override diff --git a/lib/core/logic/timeout_client.dart b/lib/core/logic/timeout_client.dart index e6a2345..5d9cc2d 100644 --- a/lib/core/logic/timeout_client.dart +++ b/lib/core/logic/timeout_client.dart @@ -12,10 +12,9 @@ import 'package:injectable/injectable.dart'; /// /// Convenience class which handles timeout errors. /// -@Injectable(as: BaseClient) -@dev @prod @Named("timeoutClient") +@Injectable(as: BaseClient) class TimeoutClient implements BaseClient { final ConnectivityStatusService connectivityStatusService; static const Duration requestTimeout = Duration(seconds: 25); diff --git a/lib/core/service/connectivity_status.service.dart b/lib/core/service/connectivity_status.service.dart index c8dacee..a4fbbd1 100644 --- a/lib/core/service/connectivity_status.service.dart +++ b/lib/core/service/connectivity_status.service.dart @@ -9,7 +9,8 @@ abstract class ConnectivityStatusService { Stream connectivityChanges(); } -@Injectable(as: ConnectivityStatusService, env: ['prod', 'dev']) +@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 aef7558..d9e9b2c 100644 --- a/lib/core/service/file_service.dart +++ b/lib/core/service/file_service.dart @@ -56,7 +56,7 @@ class FileService { } } - static Future get downloadsDirectory async { + static Future get downloadsDirectory async { if (Platform.isAndroid) { return (await getExternalStorageDirectories( type: StorageDirectory.downloads))! diff --git a/lib/core/store/local_vault.dart b/lib/core/store/local_vault.dart index beb3f1f..9a8d929 100644 --- a/lib/core/store/local_vault.dart +++ b/lib/core/store/local_vault.dart @@ -17,9 +17,8 @@ abstract class LocalVault { Future clear(); } -@Injectable(as: LocalVault) @prod -@dev +@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 index f0e4a3f..3351135 100644 --- a/lib/di_initializer.dart +++ b/lib/di_initializer.dart @@ -2,18 +2,21 @@ import 'dart:io'; import 'package:paperless_mobile/di_initializer.config.dart'; import 'package:paperless_mobile/di_modules.dart'; +import 'package:paperless_mobile/di_paperless_api.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: r'$initGetIt', // default + initializerName: 'init', // default preferRelativeImports: true, // default asExtension: false, // default + includeMicroPackages: false, ) void configureDependencies(String environment) => - $initGetIt(getIt, environment: environment); + init(getIt, environment: environment); /// /// Registers new security context, which will be used by the HttpClient, see [RegisterModule]. diff --git a/lib/di_modules.dart b/lib/di_modules.dart index c688c0f..b99961d 100644 --- a/lib/di_modules.dart +++ b/lib/di_modules.dart @@ -2,10 +2,9 @@ import 'dart:io'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; -import 'package:paperless_api/paperless_api.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/http.dart'; @@ -16,34 +15,30 @@ import 'package:local_auth/local_auth.dart'; @module abstract class RegisterModule { - @singleton - @dev @prod + @singleton LocalAuthentication get localAuthentication => LocalAuthentication(); - @singleton - @dev @prod + @singleton EncryptedSharedPreferences get encryptedSharedPreferences => EncryptedSharedPreferences(); - @singleton - @dev @prod @test + @singleton + @Order(-1) SecurityContext get securityContext => SecurityContext(); - @singleton - @dev @prod + @singleton Connectivity get connectivity => Connectivity(); /// /// Factory method creating an [HttpClient] with the currently registered [SecurityContext]. /// - @injectable - @dev @prod + @Order(-1) HttpClient getHttpClient(SecurityContext securityContext) => HttpClient(context: securityContext) ..connectionTimeout = const Duration(seconds: 10); @@ -51,66 +46,26 @@ abstract class RegisterModule { /// /// Factory method creating a [InterceptedClient] on top of the currently registered [HttpClient]. /// - @injectable - @dev @prod + @Order(-1) BaseClient getBaseClient( AuthenticationInterceptor authInterceptor, ResponseConversionInterceptor responseConversionInterceptor, LanguageHeaderInterceptor languageHeaderInterceptor, + BaseUrlInterceptor baseUrlInterceptor, HttpClient client, ) => InterceptedClient.build( interceptors: [ + baseUrlInterceptor, authInterceptor, responseConversionInterceptor, - languageHeaderInterceptor + languageHeaderInterceptor, ], client: IOClient(client), ); - @injectable - @dev @prod CacheManager getCacheManager(BaseClient client) => CacheManager( Config('cacheKey', fileService: HttpFileService(httpClient: client))); - - @injectable - @dev - @prod - PaperlessAuthenticationApi authenticationModule(BaseClient client) => - PaperlessAuthenticationApiImpl(client); - - @injectable - @dev - @prod - PaperlessLabelsApi labelsModule( - @Named('timeoutClient') BaseClient timeoutClient, - ) => - PaperlessLabelApiImpl(timeoutClient); - - @injectable - @dev - @prod - PaperlessDocumentsApi documentsModule( - @Named('timeoutClient') BaseClient timeoutClient, - HttpClient httpClient, - ) => - PaperlessDocumentsApiImpl(timeoutClient, httpClient); - - @injectable - @dev - @prod - PaperlessSavedViewsApi savedViewsModule( - @Named('timeoutClient') BaseClient timeoutClient, - ) => - PaperlessSavedViewsApiImpl(timeoutClient); - - @injectable - @dev - @prod - PaperlessServerStatsApi serverStatsModule( - @Named('timeoutClient') BaseClient timeoutClient, - ) => - PaperlessServerStatsApiImpl(timeoutClient); } diff --git a/lib/di_paperless_api.dart b/lib/di_paperless_api.dart new file mode 100644 index 0000000..10b3e3b --- /dev/null +++ b/lib/di_paperless_api.dart @@ -0,0 +1,47 @@ +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/widgets/biometric_authentication_intro_slide.dart b/lib/features/app_intro/widgets/biometric_authentication_intro_slide.dart index 56a9fd9..afafc46 100644 --- a/lib/features/app_intro/widgets/biometric_authentication_intro_slide.dart +++ b/lib/features/app_intro/widgets/biometric_authentication_intro_slide.dart @@ -1,7 +1,7 @@ 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/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'; 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 7abba7a..546e7ed 100644 --- a/lib/features/document_details/view/pages/document_details_page.dart +++ b/lib/features/document_details/view/pages/document_details_page.dart @@ -12,6 +12,7 @@ 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'; import 'package:paperless_mobile/features/documents/view/pages/document_edit_page.dart'; import 'package:paperless_mobile/features/documents/view/pages/document_view.dart'; import 'package:paperless_mobile/features/documents/view/widgets/delete_document_confirmation_dialog.dart'; @@ -82,12 +83,9 @@ class _DocumentDetailsPageState extends State { ? () => _onDelete(state.document!) : null, ).padded(const EdgeInsets.symmetric(horizontal: 4)), - IconButton( - icon: const Icon(Icons.download), - onPressed: Platform.isAndroid && state.document != null - ? () => _onDownload(state.document!) - : null, - ).padded(const EdgeInsets.only(right: 4)), + DocumentDownloadButton( + document: state.document, + ), IconButton( icon: const Icon(Icons.open_in_new), onPressed: state.document != null @@ -404,25 +402,6 @@ class _DocumentDetailsPageState extends State { return const SizedBox(height: 32.0); } - Future _onDownload(DocumentModel document) async { - if (!Platform.isAndroid) { - showSnackBar( - context, "This feature is currently only supported on Android!"); - return; - } - setState(() => _isDownloadPending = true); - getIt().download(document).then((bytes) async { - final Directory dir = (await getExternalStorageDirectories( - type: StorageDirectory.downloads))! - .first; - String filePath = "${dir.path}/${document.originalFileName}"; - //TODO: Add replacement mechanism here (ask user if file should be replaced if exists) - await File(filePath).writeAsBytes(bytes); - setState(() => _isDownloadPending = false); - dev.log("File downloaded to $filePath"); - }); - } - /// /// Downloads file to temporary directory, from which it can then be shared. /// diff --git a/lib/features/document_details/view/widgets/document_download_button.dart b/lib/features/document_details/view/widgets/document_download_button.dart new file mode 100644 index 0000000..3de5f73 --- /dev/null +++ b/lib/features/document_details/view/widgets/document_download_button.dart @@ -0,0 +1,61 @@ +import 'dart:developer'; +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'; + +class DocumentDownloadButton extends StatefulWidget { + final DocumentModel? document; + const DocumentDownloadButton({super.key, required this.document}); + + @override + State createState() => _DocumentDownloadButtonState(); +} + +class _DocumentDownloadButtonState extends State { + bool _isDownloadPending = false; + + @override + Widget build(BuildContext context) { + return IconButton( + icon: _isDownloadPending + ? const SizedBox( + child: CircularProgressIndicator(), + height: 16, + width: 16, + ) + : const Icon(Icons.download), + onPressed: Platform.isAndroid && widget.document != null + ? () => _onDownload(widget.document!) + : null, + ).padded(const EdgeInsets.only(right: 4)); + } + + Future _onDownload(DocumentModel document) async { + if (!Platform.isAndroid) { + showSnackBar( + context, "This feature is currently only supported on Android!"); + return; + } + setState(() => _isDownloadPending = true); + try { + final bytes = await getIt().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) + await File(filePath).writeAsBytes(bytes); + showSnackBar(context, S.of(context).documentDownloadSuccessMessage); + } on PaperlessServerException catch (error, stackTrace) { + showErrorMessage(context, error, stackTrace); + } catch (error) { + showGenericError(context, error); + } finally { + setState(() => _isDownloadPending = false); + } + } +} diff --git a/lib/features/documents/bloc/documents_cubit.dart b/lib/features/documents/bloc/documents_cubit.dart index 8ee60a1..be59f59 100644 --- a/lib/features/documents/bloc/documents_cubit.dart +++ b/lib/features/documents/bloc/documents_cubit.dart @@ -1,11 +1,11 @@ -import 'dart:typed_data'; - import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:injectable/injectable.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; -@singleton +@prod +@test +@lazySingleton class DocumentsCubit extends Cubit { final PaperlessDocumentsApi _api; diff --git a/lib/features/documents/view/widgets/search/sort_field_selection_bottom_sheet.dart b/lib/features/documents/view/widgets/search/sort_field_selection_bottom_sheet.dart index 5041b3c..af714b4 100644 --- a/lib/features/documents/view/widgets/search/sort_field_selection_bottom_sheet.dart +++ b/lib/features/documents/view/widgets/search/sort_field_selection_bottom_sheet.dart @@ -64,7 +64,6 @@ class _SortFieldSelectionBottomSheetState contentPadding: const EdgeInsets.symmetric(horizontal: 32), title: Text( _localizedSortField(field), - style: Theme.of(context).textTheme.bodyText2, ), trailing: isNextSelected ? (_buildOrderIcon(_selectedOrderLoading!)) diff --git a/lib/features/labels/correspondent/bloc/correspondents_cubit.dart b/lib/features/labels/correspondent/bloc/correspondents_cubit.dart index 1265275..b287987 100644 --- a/lib/features/labels/correspondent/bloc/correspondents_cubit.dart +++ b/lib/features/labels/correspondent/bloc/correspondents_cubit.dart @@ -2,7 +2,9 @@ import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; import 'package:injectable/injectable.dart'; -@singleton +@prod +@test +@lazySingleton class CorrespondentCubit extends LabelCubit { CorrespondentCubit(super.metaDataService); diff --git a/lib/features/labels/document_type/bloc/document_type_cubit.dart b/lib/features/labels/document_type/bloc/document_type_cubit.dart index 347b32f..6c9e978 100644 --- a/lib/features/labels/document_type/bloc/document_type_cubit.dart +++ b/lib/features/labels/document_type/bloc/document_type_cubit.dart @@ -2,7 +2,9 @@ import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; import 'package:injectable/injectable.dart'; -@singleton +@prod +@test +@lazySingleton class DocumentTypeCubit extends LabelCubit { DocumentTypeCubit(super.metaDataService); diff --git a/lib/features/labels/storage_path/bloc/storage_path_cubit.dart b/lib/features/labels/storage_path/bloc/storage_path_cubit.dart index 507a096..f3f7fb8 100644 --- a/lib/features/labels/storage_path/bloc/storage_path_cubit.dart +++ b/lib/features/labels/storage_path/bloc/storage_path_cubit.dart @@ -2,7 +2,9 @@ import 'package:injectable/injectable.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; -@singleton +@prod +@test +@lazySingleton class StoragePathCubit extends LabelCubit { StoragePathCubit(super.metaDataService); diff --git a/lib/features/labels/tags/bloc/tags_cubit.dart b/lib/features/labels/tags/bloc/tags_cubit.dart index 38cbf76..ae4f247 100644 --- a/lib/features/labels/tags/bloc/tags_cubit.dart +++ b/lib/features/labels/tags/bloc/tags_cubit.dart @@ -2,7 +2,9 @@ import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; import 'package:injectable/injectable.dart'; -@singleton +@prod +@test +@lazySingleton class TagCubit extends LabelCubit { TagCubit(super.metaDataService); diff --git a/lib/features/labels/tags/view/widgets/tags_form_field.dart b/lib/features/labels/tags/view/widgets/tags_form_field.dart index 59a3332..d668af3 100644 --- a/lib/features/labels/tags/view/widgets/tags_form_field.dart +++ b/lib/features/labels/tags/view/widgets/tags_form_field.dart @@ -159,7 +159,7 @@ class _TagFormFieldState extends State { (query) => _buildTag( field, query, - tagState.getLabel(query.id)!, + tagState.getLabel(query.id), ), ) .toList(), @@ -235,11 +235,13 @@ class _TagFormFieldState extends State { Widget _buildTag( FormFieldState field, TagIdQuery query, - Tag tag, + Tag? tag, ) { final currentQuery = field.value as IdsTagsQuery; final isIncludedTag = currentQuery.includedIds.contains(query.id); - + if (tag == null) { + return Container(); + } return InputChip( label: Text( tag.name, diff --git a/lib/features/labels/view/pages/edit_label_page.dart b/lib/features/labels/view/pages/edit_label_page.dart index 1eaae5c..c75a75d 100644 --- a/lib/features/labels/view/pages/edit_label_page.dart +++ b/lib/features/labels/view/pages/edit_label_page.dart @@ -120,11 +120,15 @@ class _EditLabelPageState extends State> { child: Text(S.of(context).genericActionCancelLabel), ), TextButton( - onPressed: () { - Navigator.pop(context); - widget.onDelete(widget.label); - }, - child: Text(S.of(context).genericActionDeleteLabel)), + onPressed: () { + Navigator.pop(context); + widget.onDelete(widget.label); + }, + child: Text( + S.of(context).genericActionDeleteLabel, + style: TextStyle(color: Theme.of(context).errorColor), + ), + ), ], ), ); diff --git a/lib/features/login/bloc/authentication_cubit.dart b/lib/features/login/bloc/authentication_cubit.dart index e9dbf44..a51fc9b 100644 --- a/lib/features/login/bloc/authentication_cubit.dart +++ b/lib/features/login/bloc/authentication_cubit.dart @@ -8,19 +8,19 @@ import 'package:paperless_mobile/di_initializer.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/login/services/authentication_service.dart'; import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; -const authenticationKey = "authentication"; - +@prod +@test @singleton class AuthenticationCubit extends Cubit { final LocalAuthenticationService _localAuthService; final PaperlessAuthenticationApi _authApi; - final LocalVault localStore; + final LocalVault _localVault; AuthenticationCubit( - this.localStore, + this._localVault, this._localAuthService, this._authApi, ) : super(AuthenticationState.initial); @@ -37,33 +37,21 @@ class AuthenticationCubit extends Cubit { assert(credentials.username != null && credentials.password != null); try { registerSecurityContext(clientCertificate); - emit( - AuthenticationState( - isAuthenticated: false, - wasLoginStored: false, - authentication: AuthenticationInformation( - username: credentials.username!, - password: credentials.password!, - serverUrl: serverUrl, - token: "", - clientCertificate: clientCertificate, - ), - ), - ); - final token = await _authApi.login( - username: credentials.username!, - password: credentials.password!, - serverUrl: serverUrl, - ); - final auth = AuthenticationInformation( - username: credentials.username!, - password: credentials.password!, - token: token, + // Store information required to make requests + final currentAuth = AuthenticationInformation( serverUrl: serverUrl, clientCertificate: clientCertificate, ); + await _localVault.storeAuthenticationInformation(currentAuth); - await localStore.storeAuthenticationInformation(auth); + final token = await _authApi.login( + username: credentials.username!, + password: credentials.password!, + ); + + final auth = currentAuth.copyWith(token: token); + + await _localVault.storeAuthenticationInformation(auth); emit(AuthenticationState( isAuthenticated: true, @@ -84,10 +72,10 @@ class AuthenticationCubit extends Cubit { } Future restoreSessionState() async { - final storedAuth = await localStore.loadAuthenticationInformation(); + final storedAuth = await _localVault.loadAuthenticationInformation(); late ApplicationSettingsState? appSettings; try { - appSettings = await localStore.loadApplicationSettings() ?? + appSettings = await _localVault.loadApplicationSettings() ?? ApplicationSettingsState.defaultSettings; } catch (err) { appSettings = ApplicationSettingsState.defaultSettings; @@ -95,31 +83,40 @@ class AuthenticationCubit extends Cubit { if (storedAuth == null || !storedAuth.isValid) { emit(AuthenticationState(isAuthenticated: false, wasLoginStored: false)); } else { - if (!appSettings.isLocalAuthenticationEnabled || - await _localAuthService - .authenticateLocalUser("Authenticate to log back in")) { - registerSecurityContext(storedAuth.clientCertificate); - emit( - AuthenticationState( - isAuthenticated: true, + if (appSettings.isLocalAuthenticationEnabled) { + final localAuthSuccess = await _localAuthService + .authenticateLocalUser("Authenticate to log back in"); + if (localAuthSuccess) { + registerSecurityContext(storedAuth.clientCertificate); + return emit( + AuthenticationState( + isAuthenticated: true, + wasLoginStored: true, + authentication: storedAuth, + wasLocalAuthenticationSuccessful: true, + ), + ); + } else { + return emit(AuthenticationState( + isAuthenticated: false, wasLoginStored: true, - authentication: storedAuth, - ), - ); - } else { - emit(AuthenticationState(isAuthenticated: false, wasLoginStored: true)); + wasLocalAuthenticationSuccessful: false, + )); + } } + emit(AuthenticationState(isAuthenticated: false, wasLoginStored: true)); } } Future logout() async { - await localStore.clear(); + await _localVault.clear(); emit(AuthenticationState.initial); } } class AuthenticationState { final bool wasLoginStored; + final bool? wasLocalAuthenticationSuccessful; final bool isAuthenticated; final AuthenticationInformation? authentication; @@ -131,6 +128,7 @@ class AuthenticationState { AuthenticationState({ required this.isAuthenticated, required this.wasLoginStored, + this.wasLocalAuthenticationSuccessful, this.authentication, }); @@ -138,11 +136,14 @@ class AuthenticationState { 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, ); } } diff --git a/lib/features/login/bloc/local_authentication_cubit.dart b/lib/features/login/bloc/local_authentication_cubit.dart deleted file mode 100644 index 33e50ec..0000000 --- a/lib/features/login/bloc/local_authentication_cubit.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/di_initializer.dart'; -import 'package:local_auth/local_auth.dart'; - -class LocalAuthenticationCubit extends Cubit { - LocalAuthenticationCubit() : super(LocalAuthenticationState(false)); - - Future authorize(String localizedMessage) async { - final isAuthenticationSuccessful = await getIt() - .authenticate(localizedReason: localizedMessage); - if (isAuthenticationSuccessful) { - emit(LocalAuthenticationState(true)); - } else { - throw const PaperlessServerException( - ErrorCode.biometricAuthenticationFailed); - } - } -} - -class LocalAuthenticationState { - final bool isAuthorized; - - LocalAuthenticationState(this.isAuthorized); -} diff --git a/lib/features/login/model/authentication_information.dart b/lib/features/login/model/authentication_information.dart index 1fb3ef0..be838c3 100644 --- a/lib/features/login/model/authentication_information.dart +++ b/lib/features/login/model/authentication_information.dart @@ -2,30 +2,22 @@ import 'package:paperless_mobile/core/type/types.dart'; import 'package:paperless_mobile/features/login/model/client_certificate.dart'; class AuthenticationInformation { - static const usernameKey = 'username'; - static const passwordKey = 'password'; static const tokenKey = 'token'; static const serverUrlKey = 'serverUrl'; static const clientCertificateKey = 'clientCertificate'; - final String username; - final String password; - final String token; + final String? token; final String serverUrl; final ClientCertificate? clientCertificate; AuthenticationInformation({ - required this.username, - required this.password, - required this.token, + this.token, required this.serverUrl, this.clientCertificate, }); AuthenticationInformation.fromJson(JSON json) - : username = json[usernameKey], - password = json[passwordKey], - token = json[tokenKey], + : token = json[tokenKey], serverUrl = json[serverUrlKey], clientCertificate = json[clientCertificateKey] != null ? ClientCertificate.fromJson(json[clientCertificateKey]) @@ -33,8 +25,6 @@ class AuthenticationInformation { JSON toJson() { return { - usernameKey: username, - passwordKey: password, tokenKey: token, serverUrlKey: serverUrl, clientCertificateKey: clientCertificate?.toJson(), @@ -42,21 +32,16 @@ class AuthenticationInformation { } bool get isValid { - return serverUrl.isNotEmpty && token.isNotEmpty; + return serverUrl.isNotEmpty && (token?.isNotEmpty ?? false); } AuthenticationInformation copyWith({ - String? username, - String? password, String? token, String? serverUrl, ClientCertificate? clientCertificate, bool removeClientCertificate = false, - bool? isLocalAuthenticationEnabled, }) { return AuthenticationInformation( - username: username ?? this.username, - password: password ?? this.password, token: token ?? this.token, serverUrl: serverUrl ?? this.serverUrl, clientCertificate: clientCertificate ?? diff --git a/lib/features/login/services/authentication.service.dart b/lib/features/login/services/authentication_service.dart similarity index 98% rename from lib/features/login/services/authentication.service.dart rename to lib/features/login/services/authentication_service.dart index 1d7eee5..efb02a3 100644 --- a/lib/features/login/services/authentication.service.dart +++ b/lib/features/login/services/authentication_service.dart @@ -2,7 +2,7 @@ import 'package:injectable/injectable.dart'; import 'package:local_auth/local_auth.dart'; import 'package:paperless_mobile/core/store/local_vault.dart'; -@singleton +@lazySingleton class LocalAuthenticationService { final LocalVault localStore; final LocalAuthentication localAuthentication; diff --git a/lib/features/saved_view/bloc/saved_view_cubit.dart b/lib/features/saved_view/bloc/saved_view_cubit.dart index f268530..d1352d2 100644 --- a/lib/features/saved_view/bloc/saved_view_cubit.dart +++ b/lib/features/saved_view/bloc/saved_view_cubit.dart @@ -3,7 +3,9 @@ import 'package:injectable/injectable.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/features/saved_view/bloc/saved_view_state.dart'; -@singleton +@prod +@test +@lazySingleton class SavedViewCubit extends Cubit { final PaperlessSavedViewsApi _api; SavedViewCubit(this._api) : super(SavedViewState(value: {})); diff --git a/lib/features/scan/bloc/document_scanner_cubit.dart b/lib/features/scan/bloc/document_scanner_cubit.dart index 60c4e35..5cdf2a5 100644 --- a/lib/features/scan/bloc/document_scanner_cubit.dart +++ b/lib/features/scan/bloc/document_scanner_cubit.dart @@ -67,7 +67,7 @@ class DocumentScannerCubit extends Cubit> { correspondent: correspondent, tags: tags, createdAt: createdAt, - authToken: auth.token, + authToken: auth.token!, serverUrl: auth.serverUrl, ); if (onConsumptionFinished != null) { diff --git a/lib/features/settings/bloc/application_settings_cubit.dart b/lib/features/settings/bloc/application_settings_cubit.dart index 5986b11..ae96a6b 100644 --- a/lib/features/settings/bloc/application_settings_cubit.dart +++ b/lib/features/settings/bloc/application_settings_cubit.dart @@ -5,7 +5,9 @@ import 'package:paperless_mobile/features/settings/model/application_settings_st import 'package:injectable/injectable.dart'; import 'package:paperless_mobile/features/settings/model/view_type.dart'; -@singleton +@prod +@test +@lazySingleton class ApplicationSettingsCubit extends Cubit { final LocalVault localVault; diff --git a/lib/features/settings/view/widgets/biometric_authentication_setting.dart b/lib/features/settings/view/widgets/biometric_authentication_setting.dart index 6a5c4a9..be6e63a 100644 --- a/lib/features/settings/view/widgets/biometric_authentication_setting.dart +++ b/lib/features/settings/view/widgets/biometric_authentication_setting.dart @@ -1,7 +1,7 @@ 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/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'; diff --git a/lib/l10n/intl_cs.arb b/lib/l10n/intl_cs.arb index c80f288..3202d9a 100644 --- a/lib/l10n/intl_cs.arb +++ b/lib/l10n/intl_cs.arb @@ -70,6 +70,8 @@ "@documentDetailsPageTabOverviewLabel": {}, "documentDocumentTypePropertyLabel": "Typ dokumentu", "@documentDocumentTypePropertyLabel": {}, + "documentDownloadSuccessMessage": "Document successfully downloaded.", + "@documentDownloadSuccessMessage": {}, "documentEditPageTitle": "Upravit dokument", "@documentEditPageTitle": {}, "documentMetaDataChecksumLabel": "MD5 součet originálu", @@ -270,6 +272,10 @@ "@inboxPageMarkAllAsSeenLabel": {}, "inboxPageMarkAsSeenText": "Označit jako shlédnuté", "@inboxPageMarkAsSeenText": {}, + "inboxPageNoNewDocumentsRefreshLabel": "Refresh", + "@inboxPageNoNewDocumentsRefreshLabel": {}, + "inboxPageNoNewDocumentsText": "You do not have unseen documents.", + "@inboxPageNoNewDocumentsText": {}, "inboxPageTodayText": "Dnes", "@inboxPageTodayText": {}, "inboxPageUndoRemoveText": "VRÁTIT", @@ -415,7 +421,5 @@ "tagInboxTagPropertyLabel": "Tag inboxu", "@tagInboxTagPropertyLabel": {}, "uploadPageAutomaticallInferredFieldsHintText": "Pokud specifikuješ hodnoty pro tato pole, paperless instance nebude automaticky přiřazovat naučené hodnoty. Pokud mají být tato pole automaticky vyplňována, nevyplňujte zde nic.", - "@uploadPageAutomaticallInferredFieldsHintText": {}, - "inboxPageNoNewDocumentsText": "You do not have unseen documents.", - "inboxPageNoNewDocumentsRefreshLabel": "Refresh" + "@uploadPageAutomaticallInferredFieldsHintText": {} } \ No newline at end of file diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 3167197..2b11d27 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -70,6 +70,8 @@ "@documentDetailsPageTabOverviewLabel": {}, "documentDocumentTypePropertyLabel": "Dokumenttyp", "@documentDocumentTypePropertyLabel": {}, + "documentDownloadSuccessMessage": "Dokument erfolgreich heruntergeladen.", + "@documentDownloadSuccessMessage": {}, "documentEditPageTitle": "Dokument Bearbeiten", "@documentEditPageTitle": {}, "documentMetaDataChecksumLabel": "MD5-Prüfsumme Original", @@ -136,13 +138,13 @@ "@documentsPageEmptyStateOopsText": {}, "documentsPageOrderByLabel": "Sortiere nach", "@documentsPageOrderByLabel": {}, - "documentsPageSelectionBulkDeleteDialogContinueText": "Diese Aktion ist unwiderruflich. Möchten Sie trotzdem fortfahren?", + "documentsPageSelectionBulkDeleteDialogContinueText": "Diese Aktion ist unwiderruflich. Möchtest Du trotzdem fortfahren?", "@documentsPageSelectionBulkDeleteDialogContinueText": {}, "documentsPageSelectionBulkDeleteDialogTitle": "Löschen bestätigen", "@documentsPageSelectionBulkDeleteDialogTitle": {}, - "documentsPageSelectionBulkDeleteDialogWarningTextMany": "Sind Sie sicher, dass sie folgende Dokumente löschen wollen?", + "documentsPageSelectionBulkDeleteDialogWarningTextMany": "Bist Du sicher, dass Du folgende Dokumente löschen möchtest?", "@documentsPageSelectionBulkDeleteDialogWarningTextMany": {}, - "documentsPageSelectionBulkDeleteDialogWarningTextOne": "Sind Sie sicher, dass sie folgendes Dokument löschen wollen?", + "documentsPageSelectionBulkDeleteDialogWarningTextOne": "Bist Du sicher, dass Du folgendes Dokument löschen möchtest?", "@documentsPageSelectionBulkDeleteDialogWarningTextOne": {}, "documentsPageTitle": "Dokumente", "@documentsPageTitle": {}, @@ -190,7 +192,7 @@ "@errorMessageCorrespondentLoadFailed": {}, "errorMessageCreateSavedViewError": "Gespeicherte Ansicht konnte nicht erstellt werden, bitte versuche es erneut.", "@errorMessageCreateSavedViewError": {}, - "errorMessageDeleteSavedViewError": "Gespeicherte Ansicht konnte nicht gelöscht werden, bitte versuche es erneut.", + "errorMessageDeleteSavedViewError": "Gespeicherte Ansicht konnte nicht geklöscht werden, bitte versuche es erneut.", "@errorMessageDeleteSavedViewError": {}, "errorMessageDeviceOffline": "Daten konnten nicht geladen werden: Eine Verbindung zum Internet konnte nicht hergestellt werden.", "@errorMessageDeviceOffline": {}, @@ -262,14 +264,18 @@ "@genericMessageOfflineText": {}, "inboxPageDocumentRemovedMessageText": "Dokument aus Posteingang entfernt.", "@inboxPageDocumentRemovedMessageText": {}, - "inboxPageMarkAllAsSeenConfirmationDialogText": "Sind Sie sicher, dass Sie alle Dokumente als gesehen markieren möchten? Dadurch wird eine Massenbearbeitung durchgeführt, bei der alle Posteingangs-Tags von den Dokumenten entfernt werden.\nDiese Aktion kann nicht rückgängig gemacht werden! Möchten Sie trotzdem fortfahren?", + "inboxPageMarkAllAsSeenConfirmationDialogText": "Bist Du sicher, dass Du alle Dokumente als gesehen markieren möchtest? Dadurch wird eine Massenbearbeitung durchgeführt, bei der alle Posteingangs-Tags von den Dokumenten entfernt werden.\\nDiese Aktion kann nicht rückgängig gemacht werden! Möchtest Du trotzdem fortfahren?", "@inboxPageMarkAllAsSeenConfirmationDialogText": {}, - "inboxPageMarkAllAsSeenConfirmationDialogTitleText": "Alle als gesehen markieren?", + "inboxPageMarkAllAsSeenConfirmationDialogTitleText": "Alle als gelesen markieren?", "@inboxPageMarkAllAsSeenConfirmationDialogTitleText": {}, - "inboxPageMarkAllAsSeenLabel": "Alle als gesehen markieren", + "inboxPageMarkAllAsSeenLabel": "Alle als gelesen markieren", "@inboxPageMarkAllAsSeenLabel": {}, - "inboxPageMarkAsSeenText": "Als gesehen markieren", + "inboxPageMarkAsSeenText": "Als gelesen markieren", "@inboxPageMarkAsSeenText": {}, + "inboxPageNoNewDocumentsRefreshLabel": "Neu laden", + "@inboxPageNoNewDocumentsRefreshLabel": {}, + "inboxPageNoNewDocumentsText": "Du hast keine ungesehenen Dokumente.", + "@inboxPageNoNewDocumentsText": {}, "inboxPageTodayText": "Heute", "@inboxPageTodayText": {}, "inboxPageUndoRemoveText": "UNDO", @@ -415,7 +421,5 @@ "tagInboxTagPropertyLabel": "Posteingangs-Tag", "@tagInboxTagPropertyLabel": {}, "uploadPageAutomaticallInferredFieldsHintText": "Wenn Werte für diese Felder angegeben werden, wird Paperless nicht automatisch einen Wert zuweisen. Wenn diese Felder automatisch von Paperless erkannt werden sollen, sollten die Felder leer bleiben.", - "@uploadPageAutomaticallInferredFieldsHintText": {}, - "inboxPageNoNewDocumentsText": "You do not have unseen documents.", - "inboxPageNoNewDocumentsRefreshLabel": "Refresh" + "@uploadPageAutomaticallInferredFieldsHintText": {} } \ No newline at end of file diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 3d81f8e..12178ef 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -70,6 +70,8 @@ "@documentDetailsPageTabOverviewLabel": {}, "documentDocumentTypePropertyLabel": "Document Type", "@documentDocumentTypePropertyLabel": {}, + "documentDownloadSuccessMessage": "Document successfully downloaded.", + "@documentDownloadSuccessMessage": {}, "documentEditPageTitle": "Edit Document", "@documentEditPageTitle": {}, "documentMetaDataChecksumLabel": "Original MD5-Checksum", @@ -262,7 +264,7 @@ "@genericMessageOfflineText": {}, "inboxPageDocumentRemovedMessageText": "Document removed from inbox.", "@inboxPageDocumentRemovedMessageText": {}, - "inboxPageMarkAllAsSeenConfirmationDialogText": "Are you sure you want to mark all documents as seen? This will perform a bulk edit operation removing all inbox tags from the documents.\nThis action is not reversible! Are you sure you want to continue?", + "inboxPageMarkAllAsSeenConfirmationDialogText": "Are you sure you want to mark all documents as seen? This will perform a bulk edit operation removing all inbox tags from the documents.\\nThis action is not reversible! Are you sure you want to continue?", "@inboxPageMarkAllAsSeenConfirmationDialogText": {}, "inboxPageMarkAllAsSeenConfirmationDialogTitleText": "Mark all as seen?", "@inboxPageMarkAllAsSeenConfirmationDialogTitleText": {}, @@ -270,6 +272,10 @@ "@inboxPageMarkAllAsSeenLabel": {}, "inboxPageMarkAsSeenText": "Mark as seen", "@inboxPageMarkAsSeenText": {}, + "inboxPageNoNewDocumentsRefreshLabel": "Refresh", + "@inboxPageNoNewDocumentsRefreshLabel": {}, + "inboxPageNoNewDocumentsText": "You do not have unseen documents.", + "@inboxPageNoNewDocumentsText": {}, "inboxPageTodayText": "Today", "@inboxPageTodayText": {}, "inboxPageUndoRemoveText": "UNDO", @@ -415,7 +421,5 @@ "tagInboxTagPropertyLabel": "Inbox-Tag", "@tagInboxTagPropertyLabel": {}, "uploadPageAutomaticallInferredFieldsHintText": "If you specify values for these fields, your paperless instance will not automatically derive a value. If you want these values to be automatically populated by your server, leave the fields blank.", - "@uploadPageAutomaticallInferredFieldsHintText": {}, - "inboxPageNoNewDocumentsText": "You do not have unseen documents.", - "inboxPageNoNewDocumentsRefreshLabel": "Refresh" + "@uploadPageAutomaticallInferredFieldsHintText": {} } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index eb78da7..0e5f42e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -32,8 +32,9 @@ import 'package:paperless_mobile/features/settings/model/application_settings_st import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/util.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; +import 'package:paperless_mobile/extensions/flutter_extensions.dart'; -Future startAppProd() async { +void main() async { Bloc.observer = BlocChangesObserver(); final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); @@ -54,10 +55,6 @@ Future startAppProd() async { runApp(const PaperlessMobileEntrypoint()); } -void main() async { - await startAppProd(); -} - class PaperlessMobileEntrypoint extends StatefulWidget { const PaperlessMobileEntrypoint({Key? key}) : super(key: key); @@ -71,10 +68,18 @@ class _PaperlessMobileEntrypointState extends State { Widget build(BuildContext context) { return MultiBlocProvider( providers: [ - BlocProvider.value(value: getIt()), - BlocProvider.value(value: getIt()), - BlocProvider.value(value: getIt()), - BlocProvider.value(value: getIt()), + BlocProvider.value( + value: getIt(), + ), + BlocProvider.value( + value: getIt(), + ), + BlocProvider.value( + value: getIt(), + ), + BlocProvider.value( + value: getIt(), + ), ], child: BlocBuilder( builder: (context, settings) { @@ -234,6 +239,10 @@ class _AuthenticationWrapperState extends State { child: const HomePage(), ); } else { + if (authentication.wasLoginStored && + !(authentication.wasLocalAuthenticationSuccessful ?? false)) { + return BiometricAuthenticationPage(); + } return const LoginPage(); } }, @@ -241,3 +250,43 @@ class _AuthenticationWrapperState extends State { ); } } + +class BiometricAuthenticationPage extends StatelessWidget { + const BiometricAuthenticationPage({super.key}); + + @override + Widget build(BuildContext context) { + return Material( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "The app is locked!", + style: Theme.of(context).textTheme.titleLarge, + ), + Text( + "You can now either try to authenticate again or disconnect from the current server.", + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.caption, + ).padded(), + const SizedBox(height: 48), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + ElevatedButton( + onPressed: () => + BlocProvider.of(context).logout(), + child: Text("Log out"), + ), + ElevatedButton( + onPressed: () => BlocProvider.of(context) + .restoreSessionState(), + child: Text("Authenticate"), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/util.dart b/lib/util.dart index 6776cd0..6acfeac 100644 --- a/lib/util.dart +++ b/lib/util.dart @@ -22,15 +22,17 @@ void showSnackBar( String? details, SnackBarAction? action, }) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - message + (details != null ? ' ($details)' : ''), + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar( + content: Text( + message + (details != null ? ' ($details)' : ''), + ), + action: action, + duration: const Duration(seconds: 5), ), - action: action, - duration: const Duration(seconds: 5), - ), - ); + ); } void showGenericError( diff --git a/packages/paperless_api/lib/src/models/labels/correspondent_model.dart b/packages/paperless_api/lib/src/models/labels/correspondent_model.dart index bdbed30..215be66 100644 --- a/packages/paperless_api/lib/src/models/labels/correspondent_model.dart +++ b/packages/paperless_api/lib/src/models/labels/correspondent_model.dart @@ -1,12 +1,14 @@ +import 'package:json_annotation/json_annotation.dart'; import 'package:paperless_api/src/models/labels/label_model.dart'; import 'package:paperless_api/src/models/labels/matching_algorithm.dart'; +part 'correspondent_model.g.dart'; + +@JsonSerializable(includeIfNull: false, fieldRename: FieldRename.snake) class Correspondent extends Label { - static const lastCorrespondenceKey = 'last_correspondence'; + final DateTime? lastCorrespondence; - late DateTime? lastCorrespondence; - - Correspondent({ + const Correspondent({ required super.id, required super.name, super.slug, @@ -17,24 +19,16 @@ class Correspondent extends Label { this.lastCorrespondence, }); - Correspondent.fromJson(Map json) - : lastCorrespondence = - DateTime.tryParse(json[lastCorrespondenceKey] ?? ''), - super.fromJson(json); + factory Correspondent.fromJson(Map json) => + _$CorrespondentFromJson(json); + + Map toJson() => _$CorrespondentToJson(this); @override String toString() { return name; } - @override - void addSpecificFieldsToJson(Map json) { - if (lastCorrespondence != null) { - json.putIfAbsent( - lastCorrespondenceKey, () => lastCorrespondence!.toIso8601String()); - } - } - @override Correspondent copyWith({ int? id, @@ -51,13 +45,25 @@ class Correspondent extends Label { name: name ?? this.name, documentCount: documentCount ?? documentCount, isInsensitive: isInsensitive ?? isInsensitive, - lastCorrespondence: lastCorrespondence ?? this.lastCorrespondence, match: match ?? this.match, matchingAlgorithm: matchingAlgorithm ?? this.matchingAlgorithm, slug: slug ?? this.slug, + lastCorrespondence: lastCorrespondence ?? this.lastCorrespondence, ); } @override String get queryEndpoint => 'correspondents'; + + @override + List get props => [ + id, + name, + slug, + isInsensitive, + documentCount, + lastCorrespondence, + matchingAlgorithm, + match, + ]; } diff --git a/packages/paperless_api/lib/src/models/labels/correspondent_model.g.dart b/packages/paperless_api/lib/src/models/labels/correspondent_model.g.dart new file mode 100644 index 0000000..7ec9652 --- /dev/null +++ b/packages/paperless_api/lib/src/models/labels/correspondent_model.g.dart @@ -0,0 +1,53 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'correspondent_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Correspondent _$CorrespondentFromJson(Map json) => + Correspondent( + id: json['id'] as int?, + name: json['name'] as String, + slug: json['slug'] as String?, + match: json['match'] as String?, + matchingAlgorithm: $enumDecodeNullable( + _$MatchingAlgorithmEnumMap, json['matching_algorithm']), + isInsensitive: json['is_insensitive'] as bool?, + documentCount: json['document_count'] as int?, + lastCorrespondence: json['last_correspondence'] == null + ? null + : DateTime.parse(json['last_correspondence'] as String), + ); + +Map _$CorrespondentToJson(Correspondent instance) { + final val = {}; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('id', instance.id); + val['name'] = instance.name; + writeNotNull('slug', instance.slug); + writeNotNull('match', instance.match); + writeNotNull('matching_algorithm', + _$MatchingAlgorithmEnumMap[instance.matchingAlgorithm]); + writeNotNull('is_insensitive', instance.isInsensitive); + writeNotNull('document_count', instance.documentCount); + writeNotNull( + 'last_correspondence', instance.lastCorrespondence?.toIso8601String()); + return val; +} + +const _$MatchingAlgorithmEnumMap = { + MatchingAlgorithm.anyWord: 1, + MatchingAlgorithm.allWords: 2, + MatchingAlgorithm.exactMatch: 3, + MatchingAlgorithm.regex: 4, + MatchingAlgorithm.similarWord: 5, + MatchingAlgorithm.auto: 6, +}; diff --git a/packages/paperless_api/lib/src/models/labels/document_type_model.dart b/packages/paperless_api/lib/src/models/labels/document_type_model.dart index df646fa..f244d8f 100644 --- a/packages/paperless_api/lib/src/models/labels/document_type_model.dart +++ b/packages/paperless_api/lib/src/models/labels/document_type_model.dart @@ -1,8 +1,11 @@ +import 'package:json_annotation/json_annotation.dart'; import 'package:paperless_api/src/models/labels/label_model.dart'; import 'package:paperless_api/src/models/labels/matching_algorithm.dart'; +part 'document_type_model.g.dart'; +@JsonSerializable(includeIfNull: false, fieldRename: FieldRename.snake) class DocumentType extends Label { - DocumentType({ + const DocumentType({ required super.id, required super.name, super.slug, @@ -12,10 +15,8 @@ class DocumentType extends Label { super.documentCount, }); - DocumentType.fromJson(Map json) : super.fromJson(json); - - @override - void addSpecificFieldsToJson(Map json) {} + factory DocumentType.fromJson(Map json) => + _$DocumentTypeFromJson(json); @override String get queryEndpoint => 'document_types'; @@ -40,4 +41,18 @@ class DocumentType extends Label { slug: slug ?? this.slug, ); } + + @override + Map toJson() => _$DocumentTypeToJson(this); + + @override + List get props => [ + id, + name, + slug, + isInsensitive, + documentCount, + matchingAlgorithm, + match, + ]; } diff --git a/packages/paperless_api/lib/src/models/labels/document_type_model.g.dart b/packages/paperless_api/lib/src/models/labels/document_type_model.g.dart new file mode 100644 index 0000000..9b443eb --- /dev/null +++ b/packages/paperless_api/lib/src/models/labels/document_type_model.g.dart @@ -0,0 +1,47 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'document_type_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +DocumentType _$DocumentTypeFromJson(Map json) => DocumentType( + id: json['id'] as int?, + name: json['name'] as String, + slug: json['slug'] as String?, + match: json['match'] as String?, + matchingAlgorithm: $enumDecodeNullable( + _$MatchingAlgorithmEnumMap, json['matching_algorithm']), + isInsensitive: json['is_insensitive'] as bool?, + documentCount: json['document_count'] as int?, + ); + +Map _$DocumentTypeToJson(DocumentType instance) { + final val = {}; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('id', instance.id); + val['name'] = instance.name; + writeNotNull('slug', instance.slug); + writeNotNull('match', instance.match); + writeNotNull('matching_algorithm', + _$MatchingAlgorithmEnumMap[instance.matchingAlgorithm]); + writeNotNull('is_insensitive', instance.isInsensitive); + writeNotNull('document_count', instance.documentCount); + return val; +} + +const _$MatchingAlgorithmEnumMap = { + MatchingAlgorithm.anyWord: 1, + MatchingAlgorithm.allWords: 2, + MatchingAlgorithm.exactMatch: 3, + MatchingAlgorithm.regex: 4, + MatchingAlgorithm.similarWord: 5, + MatchingAlgorithm.auto: 6, +}; diff --git a/packages/paperless_api/lib/src/models/labels/label_model.dart b/packages/paperless_api/lib/src/models/labels/label_model.dart index 1e9b3ab..10c5bcc 100644 --- a/packages/paperless_api/lib/src/models/labels/label_model.dart +++ b/packages/paperless_api/lib/src/models/labels/label_model.dart @@ -1,7 +1,8 @@ import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; import 'package:paperless_api/src/models/labels/matching_algorithm.dart'; -abstract class Label with EquatableMixin implements Comparable { +abstract class Label extends Equatable implements Comparable { static const idKey = "id"; static const nameKey = "name"; static const slugKey = "slug"; @@ -11,13 +12,19 @@ abstract class Label with EquatableMixin implements Comparable { static const documentCountKey = "document_count"; String get queryEndpoint; - + @JsonKey() final int? id; + @JsonKey() final String name; + @JsonKey() final String? slug; + @JsonKey() final String? match; + @JsonKey() final MatchingAlgorithm? matchingAlgorithm; + @JsonKey() final bool? isInsensitive; + @JsonKey() final int? documentCount; const Label({ @@ -30,31 +37,6 @@ abstract class Label with EquatableMixin implements Comparable { this.slug, }); - Label.fromJson(Map json) - : id = json[idKey], - name = json[nameKey], - slug = json[slugKey], - match = json[matchKey], - matchingAlgorithm = - MatchingAlgorithm.fromInt(json[matchingAlgorithmKey]), - isInsensitive = json[isInsensitiveKey], - documentCount = json[documentCountKey]; - - Map toJson() { - Map json = {}; - json.putIfAbsent(idKey, () => id); - json.putIfAbsent(nameKey, () => name); - json.putIfAbsent(slugKey, () => slug); - json.putIfAbsent(matchKey, () => match); - json.putIfAbsent(matchingAlgorithmKey, () => matchingAlgorithm?.value); - json.putIfAbsent(isInsensitiveKey, () => isInsensitive); - json.putIfAbsent(documentCountKey, () => documentCount); - addSpecificFieldsToJson(json); - return json; - } - - void addSpecificFieldsToJson(Map json); - Label copyWith({ int? id, String? name, @@ -75,6 +57,5 @@ abstract class Label with EquatableMixin implements Comparable { return toString().toLowerCase().compareTo(other.toString().toLowerCase()); } - @override - List get props => [id]; + Map toJson(); } diff --git a/packages/paperless_api/lib/src/models/labels/matching_algorithm.dart b/packages/paperless_api/lib/src/models/labels/matching_algorithm.dart index ac4b486..ed2c2a6 100644 --- a/packages/paperless_api/lib/src/models/labels/matching_algorithm.dart +++ b/packages/paperless_api/lib/src/models/labels/matching_algorithm.dart @@ -1,3 +1,6 @@ +import 'package:json_annotation/json_annotation.dart'; + +@JsonEnum(valueField: 'value') enum MatchingAlgorithm { anyWord(1, "Any: Match one of the following words"), allWords(2, "All: Match all of the following words"), diff --git a/packages/paperless_api/lib/src/models/labels/storage_path_model.dart b/packages/paperless_api/lib/src/models/labels/storage_path_model.dart index fdd1393..2611aec 100644 --- a/packages/paperless_api/lib/src/models/labels/storage_path_model.dart +++ b/packages/paperless_api/lib/src/models/labels/storage_path_model.dart @@ -1,9 +1,11 @@ +import 'package:json_annotation/json_annotation.dart'; import 'package:paperless_api/src/models/labels/label_model.dart'; import 'package:paperless_api/src/models/labels/matching_algorithm.dart'; +part 'storage_path_model.g.dart'; +@JsonSerializable(includeIfNull: false, fieldRename: FieldRename.snake) class StoragePath extends Label { static const pathKey = 'path'; - late String? path; StoragePath({ @@ -17,23 +19,14 @@ class StoragePath extends Label { required this.path, }); - StoragePath.fromJson(Map json) - : path = json[pathKey], - super.fromJson(json); + factory StoragePath.fromJson(Map json) => + _$StoragePathFromJson(json); @override String toString() { return name; } - @override - void addSpecificFieldsToJson(Map json) { - json.putIfAbsent( - pathKey, - () => path, - ); - } - @override StoragePath copyWith({ int? id, @@ -59,4 +52,19 @@ class StoragePath extends Label { @override String get queryEndpoint => 'storage_paths'; + + @override + List get props => [ + id, + name, + slug, + isInsensitive, + documentCount, + path, + matchingAlgorithm, + match, + ]; + + @override + Map toJson() => _$StoragePathToJson(this); } diff --git a/packages/paperless_api/lib/src/models/labels/storage_path_model.g.dart b/packages/paperless_api/lib/src/models/labels/storage_path_model.g.dart new file mode 100644 index 0000000..68abf15 --- /dev/null +++ b/packages/paperless_api/lib/src/models/labels/storage_path_model.g.dart @@ -0,0 +1,49 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'storage_path_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +StoragePath _$StoragePathFromJson(Map json) => StoragePath( + id: json['id'] as int?, + name: json['name'] as String, + slug: json['slug'] as String?, + match: json['match'] as String?, + matchingAlgorithm: $enumDecodeNullable( + _$MatchingAlgorithmEnumMap, json['matching_algorithm']), + isInsensitive: json['is_insensitive'] as bool?, + documentCount: json['document_count'] as int?, + path: json['path'] as String?, + ); + +Map _$StoragePathToJson(StoragePath instance) { + final val = {}; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('id', instance.id); + val['name'] = instance.name; + writeNotNull('slug', instance.slug); + writeNotNull('match', instance.match); + writeNotNull('matching_algorithm', + _$MatchingAlgorithmEnumMap[instance.matchingAlgorithm]); + writeNotNull('is_insensitive', instance.isInsensitive); + writeNotNull('document_count', instance.documentCount); + writeNotNull('path', instance.path); + return val; +} + +const _$MatchingAlgorithmEnumMap = { + MatchingAlgorithm.anyWord: 1, + MatchingAlgorithm.allWords: 2, + MatchingAlgorithm.exactMatch: 3, + MatchingAlgorithm.regex: 4, + MatchingAlgorithm.similarWord: 5, + MatchingAlgorithm.auto: 6, +}; diff --git a/packages/paperless_api/lib/src/models/labels/tag_model.dart b/packages/paperless_api/lib/src/models/labels/tag_model.dart index f5bc3a3..3267d27 100644 --- a/packages/paperless_api/lib/src/models/labels/tag_model.dart +++ b/packages/paperless_api/lib/src/models/labels/tag_model.dart @@ -1,6 +1,7 @@ import 'dart:developer'; import 'dart:ui'; +import 'package:json_annotation/json_annotation.dart'; import 'package:paperless_api/src/models/labels/label_model.dart'; import 'package:paperless_api/src/models/labels/matching_algorithm.dart'; @@ -10,11 +11,17 @@ class Tag extends Label { static const textColorKey = 'text_color'; static const legacyColourKey = 'colour'; - final Color? color; + final Color? _apiV2color; + + final Color? _apiV1color; + final Color? textColor; + final bool? isInboxTag; - Tag({ + Color? get color => _apiV2color ?? _apiV1color; + + const Tag({ required super.id, required super.name, super.documentCount, @@ -22,42 +29,17 @@ class Tag extends Label { super.match, super.matchingAlgorithm, super.slug, - this.color, + Color? color, this.textColor, this.isInboxTag, - }); - - Tag.fromJson(Map json) - : isInboxTag = json[isInboxTagKey], - textColor = Color(_colorStringToInt(json[textColorKey]) ?? 0), - color = _parseColorFromJson(json), - super.fromJson(json); - - /// - /// The `color` field of the json object can either be of type [Color] or a hex [String]. - /// Since API version 2, the old attribute `colour` has been replaced with `color`. - /// - static Color _parseColorFromJson(Map json) { - if (json.containsKey(legacyColourKey)) { - return Color(_colorStringToInt(json[legacyColourKey]) ?? 0); - } - if (json[colorKey] is Color) { - return json[colorKey]; - } - return Color(_colorStringToInt(json[colorKey]) ?? 0); - } + }) : _apiV1color = color, + _apiV2color = color; @override String toString() { return name; } - @override - void addSpecificFieldsToJson(Map json) { - json.putIfAbsent(colorKey, () => _toHex(color)); - json.putIfAbsent(isInboxTagKey, () => isInboxTag); - } - @override Tag copyWith({ int? id, @@ -87,22 +69,103 @@ class Tag extends Label { @override String get queryEndpoint => 'tags'; -} -/// -/// Taken from [FormBuilderColorPicker]. -/// -String? _toHex(Color? color) { - if (color == null) { + @override + List get props => [ + id, + name, + slug, + isInsensitive, + documentCount, + matchingAlgorithm, + color, + textColor, + isInboxTag, + match, + ]; + + factory Tag.fromJson(Map json) { + const $MatchingAlgorithmEnumMap = { + MatchingAlgorithm.anyWord: 1, + MatchingAlgorithm.allWords: 2, + MatchingAlgorithm.exactMatch: 3, + MatchingAlgorithm.regex: 4, + MatchingAlgorithm.similarWord: 5, + MatchingAlgorithm.auto: 6, + }; + + return Tag( + id: json['id'] as int?, + name: json['name'] as String, + documentCount: json['document_count'] as int?, + isInsensitive: json['is_insensitive'] as bool?, + match: json['match'] as String?, + matchingAlgorithm: $enumDecodeNullable( + $MatchingAlgorithmEnumMap, json['matching_algorithm']), + slug: json['slug'] as String?, + textColor: _colorFromJson(json['text_color']), + isInboxTag: json['is_inbox_tag'] as bool?, + color: _colorFromJson(json['color']) ?? _colorFromJson(json['colour']), + ); + } + + @override + Map toJson() { + final val = {}; + + const $MatchingAlgorithmEnumMap = { + MatchingAlgorithm.anyWord: 1, + MatchingAlgorithm.allWords: 2, + MatchingAlgorithm.exactMatch: 3, + MatchingAlgorithm.regex: 4, + MatchingAlgorithm.similarWord: 5, + MatchingAlgorithm.auto: 6, + }; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('id', id); + val['name'] = name; + writeNotNull('slug', slug); + writeNotNull('match', match); + writeNotNull( + 'matching_algorithm', $MatchingAlgorithmEnumMap[matchingAlgorithm]); + writeNotNull('is_insensitive', isInsensitive); + writeNotNull('document_count', documentCount); + writeNotNull('color', _toHex(_apiV2color)); + writeNotNull('colour', _toHex(_apiV1color)); + writeNotNull('text_color', _toHex(textColor)); + writeNotNull('is_inbox_tag', isInboxTag); + return val; + } + + static Color? _colorFromJson(dynamic color) { + if (color is Color) { + return color; + } + if (color is String) { + final decoded = int.tryParse(color.replaceAll("#", "ff"), radix: 16); + if (decoded == null) { + return null; + } + return Color(decoded); + } return null; } - String val = - '#${(color.value & 0xFFFFFF).toRadixString(16).padLeft(6, '0').toLowerCase()}'; - log("Color in Tag#_toHex is $val"); - return val; -} -int? _colorStringToInt(String? color) { - if (color == null) return null; - return int.tryParse(color.replaceAll("#", "ff"), radix: 16); + /// + /// Taken from [FormBuilderColorPicker]. + /// + static String? _toHex(Color? color) { + if (color == null) { + return null; + } + String val = + '#${(color.value & 0xFFFFFF).toRadixString(16).padLeft(6, '0').toLowerCase()}'; + return val; + } } diff --git a/packages/paperless_api/lib/src/modules/authentication_api/authentication_api.dart b/packages/paperless_api/lib/src/modules/authentication_api/authentication_api.dart index ea5bba2..f1d685e 100644 --- a/packages/paperless_api/lib/src/modules/authentication_api/authentication_api.dart +++ b/packages/paperless_api/lib/src/modules/authentication_api/authentication_api.dart @@ -2,6 +2,5 @@ abstract class PaperlessAuthenticationApi { Future login({ required String username, required String password, - required String serverUrl, }); } 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 735c0bf..21ab4a0 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 @@ -14,13 +14,15 @@ class PaperlessAuthenticationApiImpl implements PaperlessAuthenticationApi { Future login({ required String username, required String password, - required String serverUrl, }) async { late Response response; try { response = await client.post( Uri.parse("/api/token/"), - body: {"username": username, "password": password}, + body: { + "username": username, + "password": password, + }, ); } on FormatException catch (e) { final source = e.source; diff --git a/pubspec.lock b/pubspec.lock index e440191..186326e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -644,6 +644,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + hive: + dependency: "direct main" + description: + name: hive + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.3" html: dependency: transitive description: @@ -699,14 +706,14 @@ packages: name: injectable url: "https://pub.dartlang.org" source: hosted - version: "1.5.3" + version: "2.1.0" injectable_generator: dependency: "direct dev" description: name: injectable_generator url: "https://pub.dartlang.org" source: hosted - version: "1.5.4" + version: "2.1.2" integration_test: dependency: "direct dev" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 2130926..2e5897a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,7 +33,7 @@ dependencies: flutter_localizations: sdk: flutter get_it: ^7.2.0 - injectable: ^1.5.3 + injectable: ^2.1.0 encrypted_shared_preferences: ^3.0.0 permission_handler: ^9.2.0 pdf: ^3.8.1 @@ -80,6 +80,7 @@ dependencies: fluttertoast: ^8.1.1 paperless_api: path: packages/paperless_api + hive: ^2.2.3 dev_dependencies: integration_test: @@ -87,7 +88,7 @@ dev_dependencies: flutter_test: sdk: flutter build_runner: ^2.1.11 - injectable_generator: ^1.5.3 + injectable_generator: ^2.1.0 mockito: ^5.3.2 bloc_test: ^9.1.0 dependency_validator: ^3.0.0