diff --git a/lib/constants.dart b/lib/constants.dart new file mode 100644 index 0000000..2a86892 --- /dev/null +++ b/lib/constants.dart @@ -0,0 +1,7 @@ +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +// Globally accessible variables which are definitely initialized after main(). +late final PackageInfo packageInfo; +late final AndroidDeviceInfo? androidInfo; +late final IosDeviceInfo? iosInfo; diff --git a/lib/core/service/status_service.dart b/lib/core/service/status_service.dart index 76b2428..dce014d 100644 --- a/lib/core/service/status_service.dart +++ b/lib/core/service/status_service.dart @@ -8,7 +8,7 @@ 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/features/login/model/authentication_information.dart'; -import 'package:paperless_mobile/util.dart'; +import 'package:paperless_mobile/constants.dart'; import 'package:web_socket_channel/io.dart'; abstract class StatusService { diff --git a/lib/core/store/local_vault.dart b/lib/core/store/local_vault.dart deleted file mode 100644 index 9859e8f..0000000 --- a/lib/core/store/local_vault.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'dart:convert'; - -import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart'; -import 'package:flutter/foundation.dart'; -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'; - -abstract class LocalVault { - Future storeAuthenticationInformation(AuthenticationInformation auth); - Future loadAuthenticationInformation(); - Future loadCertificate(); - Future storeApplicationSettings(ApplicationSettingsState settings); - Future loadApplicationSettings(); - Future clear(); -} - -class LocalVaultImpl implements LocalVault { - static const applicationSettingsKey = "applicationSettings"; - static const authenticationKey = "authentication"; - - final EncryptedSharedPreferences sharedPreferences; - - LocalVaultImpl(this.sharedPreferences); - - @override - Future storeAuthenticationInformation( - AuthenticationInformation auth, - ) async { - await sharedPreferences.setString( - authenticationKey, - jsonEncode(auth.toJson()), - ); - } - - @override - Future loadAuthenticationInformation() async { - if ((await sharedPreferences.getString(authenticationKey)).isEmpty) { - return null; - } - return AuthenticationInformation.fromJson( - jsonDecode(await sharedPreferences.getString(authenticationKey)), - ); - } - - @override - Future loadCertificate() async { - return loadAuthenticationInformation() - .then((value) => value?.clientCertificate); - } - - @override - Future storeApplicationSettings(ApplicationSettingsState settings) { - return sharedPreferences.setString( - applicationSettingsKey, - jsonEncode(settings.toJson()), - ); - } - - @override - Future loadApplicationSettings() async { - final settings = await sharedPreferences.getString(applicationSettingsKey); - if (settings.isEmpty) { - return null; - } - return compute( - ApplicationSettingsState.fromJson, - jsonDecode(settings) as JSON, - ); - } - - @override - Future clear() { - return sharedPreferences.clear(); - } -} diff --git a/lib/core/translation/color_scheme_option_localization_mapper.dart b/lib/core/translation/color_scheme_option_localization_mapper.dart new file mode 100644 index 0000000..c4b3f76 --- /dev/null +++ b/lib/core/translation/color_scheme_option_localization_mapper.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; +import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; + +String translateColorSchemeOption( + BuildContext context, ColorSchemeOption option) { + switch (option) { + case ColorSchemeOption.classic: + return S.of(context).colorSchemeOptionClassic; + case ColorSchemeOption.dynamic: + return S.of(context).colorSchemeOptionDynamic; + } +} diff --git a/lib/extensions/hydrated_storage_extension.dart b/lib/extensions/hydrated_storage_extension.dart index 3646bea..96ba2da 100644 --- a/lib/extensions/hydrated_storage_extension.dart +++ b/lib/extensions/hydrated_storage_extension.dart @@ -1,6 +1,6 @@ import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:paperless_mobile/features/login/bloc/authentication_state.dart'; -import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; +import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart'; extension AddressableHydratedStorage on Storage { ApplicationSettingsState get settings { diff --git a/lib/features/document_details/bloc/document_details_cubit.dart b/lib/features/document_details/bloc/document_details_cubit.dart index 71ed062..0752697 100644 --- a/lib/features/document_details/bloc/document_details_cubit.dart +++ b/lib/features/document_details/bloc/document_details_cubit.dart @@ -1,8 +1,10 @@ import 'dart:developer'; import 'dart:io'; +import 'dart:typed_data'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -49,14 +51,18 @@ class DocumentDetailsCubit extends Cubit { } Future openDocumentInSystemViewer() async { - final downloadDir = await FileService.temporaryDirectory; + final cacheDir = await FileService.temporaryDirectory; + final metaData = await _api.getMetaData(state.document); - final docBytes = await _api.download(state.document); - File f = File('${downloadDir.path}/${metaData.mediaFilename}'); - f.createSync(recursive: true); - f.writeAsBytesSync(docBytes); - return OpenFilex.open(f.path, type: "application/pdf") - .then((value) => value.type); + final bytes = await _api.download(state.document); + + final file = File('${cacheDir.path}/${metaData.mediaFilename}') + ..createSync(recursive: true) + ..writeAsBytesSync(bytes); + + return OpenFilex.open(file.path, type: "application/pdf").then( + (value) => value.type, + ); } void replaceDocument(DocumentModel document) { 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 5fa8ae6..3a03f58 100644 --- a/lib/features/document_details/view/pages/document_details_page.dart +++ b/lib/features/document_details/view/pages/document_details_page.dart @@ -26,6 +26,7 @@ import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.d import 'package:paperless_mobile/features/labels/view/widgets/label_text.dart'; import 'package:paperless_mobile/features/similar_documents/cubit/similar_documents_cubit.dart'; import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:paperless_mobile/helpers/format_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; @@ -556,15 +557,6 @@ class _DocumentDetailsPageState extends State { ); } - static String formatBytes(int bytes, int decimals) { - if (bytes <= 0) return "0 B"; - const suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; - var i = (log(bytes) / log(1024)).floor(); - return ((bytes / pow(1024, i)).toStringAsFixed(decimals)) + - ' ' + - suffixes[i]; - } - Widget _buildSimilarDocumentsView() { return const SimilarDocumentsView(); } diff --git a/lib/features/document_details/view/pages/similar_documents_view.dart b/lib/features/document_details/view/pages/similar_documents_view.dart index b426422..2fb2b53 100644 --- a/lib/features/document_details/view/pages/similar_documents_view.dart +++ b/lib/features/document_details/view/pages/similar_documents_view.dart @@ -7,7 +7,7 @@ import 'package:paperless_mobile/features/documents/view/widgets/documents_empty import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart'; import 'package:paperless_mobile/features/similar_documents/cubit/similar_documents_cubit.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; -import 'package:paperless_mobile/util.dart'; +import 'package:paperless_mobile/constants.dart'; class SimilarDocumentsView extends StatefulWidget { const SimilarDocumentsView({super.key}); 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 b719eda..ccaf894 100644 --- a/lib/features/document_details/view/widgets/document_download_button.dart +++ b/lib/features/document_details/view/widgets/document_download_button.dart @@ -6,7 +6,7 @@ import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; -import 'package:paperless_mobile/util.dart'; +import 'package:paperless_mobile/constants.dart'; import 'package:provider/provider.dart'; class DocumentDownloadButton extends StatefulWidget { @@ -48,20 +48,24 @@ class _DocumentDownloadButtonState extends State { return; } setState(() => _isDownloadPending = true); + final service = context.read(); try { - final bytes = - await context.read().download(document); + final bytes = await service.download(document); + final meta = await service.getMetaData(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); + String filePath = "${dir.path}/${meta.mediaFilename}"; + final createdFile = File(filePath); + createdFile.createSync(recursive: true); + createdFile.writeAsBytesSync(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); + if (mounted) { + setState(() => _isDownloadPending = false); + } } } } diff --git a/lib/features/document_search/cubit/document_search_cubit.dart b/lib/features/document_search/cubit/document_search_cubit.dart new file mode 100644 index 0000000..bd81066 --- /dev/null +++ b/lib/features/document_search/cubit/document_search_cubit.dart @@ -0,0 +1,38 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_api/src/modules/documents_api/paperless_documents_api.dart'; +import 'package:paperless_mobile/features/paged_document_view/documents_paging_mixin.dart'; + +import 'document_search_state.dart'; + +class DocumentSearchCubit extends HydratedCubit + with DocumentsPagingMixin { + DocumentSearchCubit(this.api) : super(const DocumentSearchState()); + + @override + final PaperlessDocumentsApi api; + + Future updateResults(String query) async { + await updateFilter( + filter: state.filter.copyWith(query: TextQuery.titleAndContent(query)), + ); + emit(state.copyWith(searchHistory: [query, ...state.searchHistory])); + } + + Future updateSuggestions(String query) async { + final suggestions = await api.autocomplete(query); + emit(state.copyWith(suggestions: suggestions)); + } + + @override + DocumentSearchState? fromJson(Map json) { + return DocumentSearchState.fromJson(json); + } + + @override + Map? toJson(DocumentSearchState state) { + return state.toJson(); + } +} diff --git a/lib/features/document_search/cubit/document_search_state.dart b/lib/features/document_search/cubit/document_search_state.dart new file mode 100644 index 0000000..a6b0be7 --- /dev/null +++ b/lib/features/document_search/cubit/document_search_state.dart @@ -0,0 +1,75 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/features/paged_document_view/model/documents_paged_state.dart'; + +part 'document_search_state.g.dart'; + + + +@JsonSerializable(ignoreUnannotated: true) +class DocumentSearchState extends DocumentsPagedState { + @JsonKey() + final List searchHistory; + + final List suggestions; + + const DocumentSearchState({ + this.searchHistory = const [], + this.suggestions = const [], + super.filter, + super.hasLoaded, + super.isLoading, + super.value, + }); + + @override + List get props => [ + hasLoaded, + isLoading, + filter, + value, + searchHistory, + suggestions, + ]; + + @override + DocumentSearchState copyWithPaged({ + bool? hasLoaded, + bool? isLoading, + List>? value, + DocumentFilter? filter, + }) { + return copyWith( + hasLoaded: hasLoaded, + isLoading: isLoading, + filter: filter, + value: value, + ); + } + + DocumentSearchState copyWith({ + List? searchHistory, + bool? hasLoaded, + bool? isLoading, + List>? value, + DocumentFilter? filter, + List? suggestions, + }) { + return DocumentSearchState( + value: value ?? this.value, + filter: filter ?? this.filter, + hasLoaded: hasLoaded ?? this.hasLoaded, + isLoading: isLoading ?? this.isLoading, + searchHistory: searchHistory ?? this.searchHistory, + suggestions: suggestions ?? this.suggestions, + ); + } + + factory DocumentSearchState.fromJson(Map json) => + _$DocumentSearchStateFromJson(json); + + Map toJson() => _$DocumentSearchStateToJson(this); +} + +class diff --git a/lib/features/document_search/document_search_delegate.dart b/lib/features/document_search/document_search_delegate.dart new file mode 100644 index 0000000..d70af8b --- /dev/null +++ b/lib/features/document_search/document_search_delegate.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; +import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.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/document_search/cubit/document_search_cubit.dart'; +import 'package:paperless_mobile/features/document_search/cubit/document_search_state.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart'; +import 'package:provider/provider.dart'; + +class DocumentSearchDelegate extends SearchDelegate { + DocumentSearchDelegate({ + required String hintText, + required super.searchFieldStyle, + }) : super( + searchFieldLabel: hintText, + keyboardType: TextInputType.text, + textInputAction: TextInputAction.search, + ); + + @override + Widget buildLeading(BuildContext context) => const BackButton(); + + @override + Widget buildSuggestions(BuildContext context) { + BlocBuilder( + builder: (context, state) { + if (!state.hasLoaded && state.isLoading) { + return const DocumentsListLoadingWidget(); + } + return ListView.builder(itemBuilder: (context, index) => ListTile( + title: Text(snapshot.data![index]), + onTap: () { + query = snapshot.data![index]; + super.showResults(context); + }, + ),); + }, + ) + return FutureBuilder( + future: context.read().autocomplete(query), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Center( + child: CircularProgressIndicator(), + ); + } + return ListView.builder( + itemCount: snapshot.data!.length, + itemBuilder: (context, index) => ListTile( + title: Text(snapshot.data![index]), + onTap: () { + query = snapshot.data![index]; + super.showResults(context); + }, + ), + ); + }, + ); + } + + @override + Widget buildResults(BuildContext context) { + return FutureBuilder( + future: context + .read() + .findAll(DocumentFilter(query: TextQuery.titleAndContent(query))), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Center( + child: CircularProgressIndicator(), + ); + } + final documents = snapshot.data!.results; + return ListView.builder( + itemBuilder: (context, index) => DocumentListItem( + document: documents[index], + onTap: (document) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => BlocProvider( + create: (context) => DocumentDetailsCubit( + context.read(), + document, + ), + child: const LabelRepositoriesProvider( + child: DocumentDetailsPage( + isLabelClickable: false, + ), + ), + ), + ), + ); + }, + ), + ); + }, + ); + } + + @override + List buildActions(BuildContext context) => []; +} diff --git a/lib/features/document_upload/cubit/document_upload_cubit.dart b/lib/features/document_upload/cubit/document_upload_cubit.dart index fcc159c..dec42e2 100644 --- a/lib/features/document_upload/cubit/document_upload_cubit.dart +++ b/lib/features/document_upload/cubit/document_upload_cubit.dart @@ -8,7 +8,6 @@ import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart'; import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart'; import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart'; -import 'package:paperless_mobile/core/store/local_vault.dart'; part 'document_upload_state.dart'; @@ -24,7 +23,6 @@ class DocumentUploadCubit extends Cubit { final List _subs = []; DocumentUploadCubit({ - required LocalVault localVault, required PaperlessDocumentsApi documentApi, required LabelRepository tagRepository, required LabelRepository 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 de7b66e..dc91d9e 100644 --- a/lib/features/document_upload/view/document_upload_preparation_page.dart +++ b/lib/features/document_upload/view/document_upload_preparation_page.dart @@ -20,7 +20,7 @@ import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_fie import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; -import 'package:paperless_mobile/util.dart'; +import 'package:paperless_mobile/constants.dart'; class DocumentUploadPreparationPage extends StatefulWidget { final Uint8List fileBytes; diff --git a/lib/features/documents/view/pages/document_edit_page.dart b/lib/features/documents/view/pages/document_edit_page.dart index 44fa050..b854897 100644 --- a/lib/features/documents/view/pages/document_edit_page.dart +++ b/lib/features/documents/view/pages/document_edit_page.dart @@ -21,7 +21,7 @@ import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_fie import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; -import 'package:paperless_mobile/util.dart'; +import 'package:paperless_mobile/constants.dart'; class DocumentEditPage extends StatefulWidget { final FieldSuggestions suggestions; diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index d615a2b..75c8da3 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -8,6 +8,7 @@ import 'package:paperless_mobile/core/repository/provider/label_repositories_pro 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/pages/document_details_page.dart'; +import 'package:paperless_mobile/features/document_search/document_search_delegate.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart'; @@ -22,12 +23,12 @@ import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; import 'package:paperless_mobile/features/saved_view/view/saved_view_selection_widget.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/bloc/application_settings_state.dart'; import 'package:paperless_mobile/features/settings/model/view_type.dart'; import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; -import 'package:paperless_mobile/util.dart'; +import 'package:paperless_mobile/constants.dart'; class DocumentFilterIntent { final DocumentFilter? filter; @@ -148,9 +149,42 @@ class _DocumentsPageState extends State { builder: (context, state) { if (state.selection.isEmpty) { return AppBar( - title: Text( - "${S.of(context).documentsPageTitle} (${_formatDocumentCount(state.count)})", + title: TextField( + onTap: () => showSearch( + context: context, + delegate: DocumentSearchDelegate( + searchFieldStyle: + Theme.of(context).textTheme.bodyLarge, + hintText: "Search your documents", + ), + ), + readOnly: true, + decoration: InputDecoration( + hintText: "Search your documents", + hintStyle: Theme.of(context) + .textTheme + .bodyLarge + ?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurfaceVariant), + filled: true, + fillColor: Theme.of(context).colorScheme.surface, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(32), + borderSide: BorderSide.none, + ), + prefixIcon: IconButton( + icon: const Icon(Icons.menu), + onPressed: () { + Scaffold.of(context).openDrawer(); + }, + ), + ), ), + // title: Text( + // "${S.of(context).documentsPageTitle} (${_formatDocumentCount(state.count)})", + // ), actions: [ const SortDocumentsButton(), BlocBuilder { ? const LinearProgressIndicator() : const SizedBox(height: 4.0), ), + automaticallyImplyLeading: false, ); } else { return AppBar( diff --git a/lib/features/edit_label/view/edit_label_page.dart b/lib/features/edit_label/view/edit_label_page.dart index 1eb72eb..28d2273 100644 --- a/lib/features/edit_label/view/edit_label_page.dart +++ b/lib/features/edit_label/view/edit_label_page.dart @@ -10,7 +10,7 @@ import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart import 'package:paperless_mobile/features/edit_label/view/label_form.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; -import 'package:paperless_mobile/util.dart'; +import 'package:paperless_mobile/constants.dart'; class EditLabelPage extends StatelessWidget { final T label; diff --git a/lib/features/edit_label/view/label_form.dart b/lib/features/edit_label/view/label_form.dart index 2ddc1b5..8fa7327 100644 --- a/lib/features/edit_label/view/label_form.dart +++ b/lib/features/edit_label/view/label_form.dart @@ -7,7 +7,7 @@ import 'package:paperless_mobile/core/type/types.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; -import 'package:paperless_mobile/util.dart'; +import 'package:paperless_mobile/constants.dart'; class SubmitButtonConfig { final Widget icon; diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index b069f10..fafd6bd 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -109,7 +109,6 @@ class _HomePageState extends State { MaterialPageRoute( builder: (context) => BlocProvider.value( value: DocumentUploadCubit( - localVault: context.read(), documentApi: context.read(), tagRepository: context.read(), correspondentRepository: context.read(), diff --git a/lib/features/home/view/route_description.dart b/lib/features/home/view/route_description.dart index ee4c36a..6fc36a6 100644 --- a/lib/features/home/view/route_description.dart +++ b/lib/features/home/view/route_description.dart @@ -4,11 +4,13 @@ class RouteDescription { final String label; final Icon icon; final Icon selectedIcon; + final Widget Function(Widget icon)? badgeBuilder; RouteDescription({ required this.label, required this.icon, required this.selectedIcon, + this.badgeBuilder, }); NavigationDestination toNavigationDestination() { @@ -30,8 +32,8 @@ class RouteDescription { BottomNavigationBarItem toBottomNavigationBarItem() { return BottomNavigationBarItem( label: label, - icon: icon, - activeIcon: selectedIcon, + icon: badgeBuilder?.call(icon) ?? icon, + activeIcon: badgeBuilder?.call(selectedIcon) ?? selectedIcon, ); } } diff --git a/lib/features/home/view/widget/app_drawer.dart b/lib/features/home/view/widget/app_drawer.dart index 7113df3..f6b5c98 100644 --- a/lib/features/home/view/widget/app_drawer.dart +++ b/lib/features/home/view/widget/app_drawer.dart @@ -12,7 +12,6 @@ import 'package:paperless_mobile/core/repository/state/impl/correspondent_reposi import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart'; import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart'; import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart'; -import 'package:paperless_mobile/core/store/local_vault.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'; @@ -21,7 +20,7 @@ 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/helpers/message_helpers.dart'; -import 'package:paperless_mobile/util.dart'; +import 'package:paperless_mobile/constants.dart'; import 'package:url_launcher/link.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -43,12 +42,9 @@ class AppDrawer extends StatefulWidget { // } class _AppDrawerState extends State { - late final Future _packageInfo; - @override void initState() { super.initState(); - _packageInfo = PackageInfo.fromPlatform(); } @override @@ -120,162 +116,150 @@ class _AppDrawerState extends State { bottomRight: Radius.circular(16.0), ), ), - child: Theme( - data: Theme.of(context).copyWith( - listTileTheme: ListTileThemeData( - tileColor: Colors.transparent, - ), - ), - child: ListView( - children: [ - DrawerHeader( - padding: const EdgeInsets.only( - top: 8, - left: 8, - bottom: 0, - right: 8, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Image.asset( - 'assets/logos/paperless_logo_white.png', - height: 32, - width: 32, - color: Theme.of(context) - .colorScheme - .onPrimaryContainer, - ).paddedOnly(right: 8.0), - Text( - S.of(context).appTitleText, - style: Theme.of(context) - .textTheme - .headlineSmall - ?.copyWith( - color: Theme.of(context) - .colorScheme - .onPrimaryContainer, - ), - ), - ], - ), - Align( - alignment: Alignment.bottomRight, - child: BlocBuilder( - builder: (context, state) { - if (!state.isLoaded) { - return Container(); - } - final info = state.information!; - return Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - ListTile( - contentPadding: EdgeInsets.zero, - dense: true, - title: Text( - S - .of(context) - .appDrawerHeaderLoggedInAsText + - (info.username ?? '?'), - style: - Theme.of(context).textTheme.bodyMedium, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.end, - maxLines: 1, - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - state.information!.host ?? '', - style: Theme.of(context) - .textTheme - .bodyMedium, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.end, - maxLines: 1, - ), - Text( - '${S.of(context).serverInformationPaperlessVersionText} ${info.version} (API v${info.apiVersion})', - style: Theme.of(context) - .textTheme - .bodySmall, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.end, - maxLines: 1, - ), - ], - ), - isThreeLine: true, - ), - ], - ); - }, + child: ListView( + children: [ + DrawerHeader( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondaryContainer, + ), + padding: const EdgeInsets.only( + top: 8, + left: 8, + bottom: 0, + right: 8, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Image.asset( + 'assets/logos/paperless_logo_white.png', + height: 32, + width: 32, + color: + Theme.of(context).colorScheme.onPrimaryContainer, + ).paddedOnly(right: 8.0), + Text( + S.of(context).appTitleText, + style: Theme.of(context) + .textTheme + .headlineSmall + ?.copyWith( + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), ), + ], + ), + Align( + alignment: Alignment.bottomRight, + child: BlocBuilder( + builder: (context, state) { + if (!state.isLoaded) { + return Container(); + } + final info = state.information!; + return Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + ListTile( + contentPadding: EdgeInsets.zero, + dense: true, + title: Text( + S.of(context).appDrawerHeaderLoggedInAsText + + (info.username ?? '?'), + style: Theme.of(context).textTheme.bodyMedium, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.end, + maxLines: 1, + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + state.information!.host ?? '', + style: Theme.of(context) + .textTheme + .bodyMedium, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.end, + maxLines: 1, + ), + Text( + '${S.of(context).serverInformationPaperlessVersionText} ${info.version} (API v${info.apiVersion})', + style: + Theme.of(context).textTheme.bodySmall, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.end, + maxLines: 1, + ), + ], + ), + isThreeLine: true, + ), + ], + ); + }, ), - ], + ), + ], + ), + ), + ...[ + ListTile( + title: Text(S.of(context).bottomNavInboxPageLabel), + leading: const Icon(Icons.inbox), + onTap: () => _onOpenInbox(), + shape: listtTileShape, + ), + ListTile( + leading: const Icon(Icons.settings), + shape: listtTileShape, + title: Text( + S.of(context).appDrawerSettingsLabel, ), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primaryContainer, + onTap: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => BlocProvider.value( + value: context.read(), + child: const SettingsPage(), + ), + ), ), ), - ...[ - ListTile( - title: Text(S.of(context).bottomNavInboxPageLabel), - leading: const Icon(Icons.inbox), - onTap: () => _onOpenInbox(), - shape: listtTileShape, - ), - ListTile( - leading: const Icon(Icons.settings), - shape: listtTileShape, - title: Text( - S.of(context).appDrawerSettingsLabel, - ), - onTap: () => Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => BlocProvider.value( - value: context.read(), - child: const SettingsPage(), - ), - ), - ), - ), - const Divider( - indent: 16, - endIndent: 16, - ), - ListTile( - leading: const Icon(Icons.bug_report), - title: Text(S.of(context).appDrawerReportBugLabel), - onTap: () { - launchUrlString( - 'https://github.com/astubenbord/paperless-mobile/issues/new'); - }, - shape: listtTileShape, - ), - ListTile( - title: Text(S.of(context).appDrawerAboutLabel), - leading: Icon(Icons.info_outline_rounded), - onTap: _onShowAboutDialog, - shape: listtTileShape, - ), - ListTile( - leading: const Icon(Icons.logout), - title: Text(S.of(context).appDrawerLogoutLabel), - shape: listtTileShape, - onTap: () { - _onLogout(); - }, - ) - ], + const Divider( + indent: 16, + endIndent: 16, + ), + ListTile( + leading: const Icon(Icons.bug_report), + title: Text(S.of(context).appDrawerReportBugLabel), + onTap: () { + launchUrlString( + 'https://github.com/astubenbord/paperless-mobile/issues/new'); + }, + shape: listtTileShape, + ), + ListTile( + title: Text(S.of(context).appDrawerAboutLabel), + leading: Icon(Icons.info_outline_rounded), + onTap: _onShowAboutDialog, + shape: listtTileShape, + ), + ListTile( + leading: const Icon(Icons.logout), + title: Text(S.of(context).appDrawerLogoutLabel), + shape: listtTileShape, + onTap: () { + _onLogout(); + }, + ) ], - ), + ], ), ), ), @@ -285,7 +269,6 @@ class _AppDrawerState extends State { void _onLogout() async { try { await context.read().logout(); - await context.read().clear(); await context.read().clear(); await context.read>().clear(); await context @@ -354,15 +337,14 @@ class _AppDrawerState extends State { ); } - Future _onShowAboutDialog() async { - final snapshot = await _packageInfo; + void _onShowAboutDialog() { showAboutDialog( context: context, applicationIcon: const ImageIcon( AssetImage('assets/logos/paperless_logo_green.png'), ), applicationName: 'Paperless Mobile', - applicationVersion: snapshot.version + '+' + snapshot.buildNumber, + applicationVersion: packageInfo.version + '+' + packageInfo.buildNumber, children: [ Text(S.of(context).aboutDialogDevelopedByText('Anton Stubenbord')), Link( diff --git a/lib/features/inbox/view/pages/inbox_page.dart b/lib/features/inbox/view/pages/inbox_page.dart index 60077e3..c464e53 100644 --- a/lib/features/inbox/view/pages/inbox_page.dart +++ b/lib/features/inbox/view/pages/inbox_page.dart @@ -14,7 +14,7 @@ import 'package:paperless_mobile/features/inbox/view/widgets/inbox_empty_widget. import 'package:paperless_mobile/features/inbox/view/widgets/inbox_item.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; -import 'package:paperless_mobile/util.dart'; +import 'package:paperless_mobile/constants.dart'; class InboxPage extends StatefulWidget { const InboxPage({super.key}); diff --git a/lib/features/login/services/authentication_service.dart b/lib/features/login/services/authentication_service.dart index 84aa8c4..6600125 100644 --- a/lib/features/login/services/authentication_service.dart +++ b/lib/features/login/services/authentication_service.dart @@ -1,12 +1,9 @@ import 'package:local_auth/local_auth.dart'; -import 'package:paperless_mobile/core/store/local_vault.dart'; class LocalAuthenticationService { - final LocalVault localStore; final LocalAuthentication localAuthentication; LocalAuthenticationService( - this.localStore, this.localAuthentication, ); diff --git a/lib/features/login/view/login_page.dart b/lib/features/login/view/login_page.dart index 1e3f343..30edca1 100644 --- a/lib/features/login/view/login_page.dart +++ b/lib/features/login/view/login_page.dart @@ -10,7 +10,7 @@ import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_cr import 'package:paperless_mobile/features/login/view/widgets/login_pages/server_connection_page.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; -import 'package:paperless_mobile/util.dart'; +import 'package:paperless_mobile/constants.dart'; import 'widgets/never_scrollable_scroll_behavior.dart'; import 'widgets/login_pages/server_login_page.dart'; diff --git a/lib/features/login/view/widgets/form_fields/client_certificate_form_field.dart b/lib/features/login/view/widgets/form_fields/client_certificate_form_field.dart index 408577b..a77605f 100644 --- a/lib/features/login/view/widgets/form_fields/client_certificate_form_field.dart +++ b/lib/features/login/view/widgets/form_fields/client_certificate_form_field.dart @@ -6,7 +6,7 @@ import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/login/model/client_certificate.dart'; import 'package:paperless_mobile/generated/l10n.dart'; -import 'package:paperless_mobile/util.dart'; +import 'package:paperless_mobile/constants.dart'; import 'package:permission_handler/permission_handler.dart'; import 'obscured_input_text_form_field.dart'; diff --git a/lib/features/saved_view/view/saved_view_selection_widget.dart b/lib/features/saved_view/view/saved_view_selection_widget.dart index b25cfff..bbc21b8 100644 --- a/lib/features/saved_view/view/saved_view_selection_widget.dart +++ b/lib/features/saved_view/view/saved_view_selection_widget.dart @@ -13,7 +13,7 @@ import 'package:paperless_mobile/features/saved_view/cubit/saved_view_state.dart import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; -import 'package:paperless_mobile/util.dart'; +import 'package:paperless_mobile/constants.dart'; import 'package:shimmer/shimmer.dart'; class SavedViewSelectionWidget extends StatelessWidget { diff --git a/lib/features/scan/view/scanner_page.dart b/lib/features/scan/view/scanner_page.dart index 9141da7..37f2776 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/state/impl/correspondent_reposi import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart'; import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart'; 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/features/document_upload/cubit/document_upload_cubit.dart'; import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart'; @@ -148,7 +147,6 @@ class _ScannerPageState extends State builder: (_) => LabelRepositoriesProvider( child: BlocProvider( create: (context) => DocumentUploadCubit( - localVault: context.read(), documentApi: context.read(), correspondentRepository: context.read< LabelRepository builder: (_) => LabelRepositoriesProvider( child: BlocProvider( create: (context) => DocumentUploadCubit( - localVault: context.read(), documentApi: context.read(), correspondentRepository: context.read< LabelRepository { @@ -27,17 +28,23 @@ class ApplicationSettingsCubit extends HydratedCubit { } } - Future setThemeMode(ThemeMode? selectedMode) async { + void setThemeMode(ThemeMode? selectedMode) { final updatedSettings = state.copyWith(preferredThemeMode: selectedMode); _updateSettings(updatedSettings); } - Future setViewType(ViewType viewType) async { + void setViewType(ViewType viewType) { final updatedSettings = state.copyWith(preferredViewType: viewType); _updateSettings(updatedSettings); } - Future _updateSettings(ApplicationSettingsState settings) async { + void setColorSchemeOption(ColorSchemeOption schemeOption) { + final updatedSettings = + state.copyWith(preferredColorSchemeOption: schemeOption); + _updateSettings(updatedSettings); + } + + void _updateSettings(ApplicationSettingsState settings) async { emit(settings); } diff --git a/lib/features/settings/model/application_settings_state.dart b/lib/features/settings/bloc/application_settings_state.dart similarity index 75% rename from lib/features/settings/model/application_settings_state.dart rename to lib/features/settings/bloc/application_settings_state.dart index 05c284d..7dfc48f 100644 --- a/lib/features/settings/model/application_settings_state.dart +++ b/lib/features/settings/bloc/application_settings_state.dart @@ -2,7 +2,7 @@ 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/color_scheme_option.dart'; import 'package:paperless_mobile/features/settings/model/view_type.dart'; part 'application_settings_state.g.dart'; @@ -13,22 +13,21 @@ part 'application_settings_state.g.dart'; @JsonSerializable() class ApplicationSettingsState { static final defaultSettings = ApplicationSettingsState( - isLocalAuthenticationEnabled: false, preferredLocaleSubtag: Platform.localeName.split('_').first, - preferredThemeMode: ThemeMode.system, - preferredViewType: ViewType.list, ); final bool isLocalAuthenticationEnabled; final String preferredLocaleSubtag; final ThemeMode preferredThemeMode; final ViewType preferredViewType; + final ColorSchemeOption preferredColorSchemeOption; ApplicationSettingsState({ required this.preferredLocaleSubtag, - required this.preferredThemeMode, - required this.isLocalAuthenticationEnabled, - required this.preferredViewType, + this.preferredThemeMode = ThemeMode.system, + this.isLocalAuthenticationEnabled = false, + this.preferredViewType = ViewType.list, + this.preferredColorSchemeOption = ColorSchemeOption.dynamic, }); Map toJson() => _$ApplicationSettingsStateToJson(this); @@ -40,6 +39,7 @@ class ApplicationSettingsState { String? preferredLocaleSubtag, ThemeMode? preferredThemeMode, ViewType? preferredViewType, + ColorSchemeOption? preferredColorSchemeOption, }) { return ApplicationSettingsState( isLocalAuthenticationEnabled: @@ -48,6 +48,8 @@ class ApplicationSettingsState { preferredLocaleSubtag ?? this.preferredLocaleSubtag, preferredThemeMode: preferredThemeMode ?? this.preferredThemeMode, preferredViewType: preferredViewType ?? this.preferredViewType, + preferredColorSchemeOption: + preferredColorSchemeOption ?? this.preferredColorSchemeOption, ); } } diff --git a/lib/features/settings/model/application_settings_state.g.dart b/lib/features/settings/bloc/application_settings_state.g.dart similarity index 63% rename from lib/features/settings/model/application_settings_state.g.dart rename to lib/features/settings/bloc/application_settings_state.g.dart index 6166a38..a131830 100644 --- a/lib/features/settings/model/application_settings_state.g.dart +++ b/lib/features/settings/bloc/application_settings_state.g.dart @@ -11,11 +11,16 @@ ApplicationSettingsState _$ApplicationSettingsStateFromJson( ApplicationSettingsState( preferredLocaleSubtag: json['preferredLocaleSubtag'] as String, preferredThemeMode: - $enumDecode(_$ThemeModeEnumMap, json['preferredThemeMode']), + $enumDecodeNullable(_$ThemeModeEnumMap, json['preferredThemeMode']) ?? + ThemeMode.system, isLocalAuthenticationEnabled: - json['isLocalAuthenticationEnabled'] as bool, + json['isLocalAuthenticationEnabled'] as bool? ?? false, preferredViewType: - $enumDecode(_$ViewTypeEnumMap, json['preferredViewType']), + $enumDecodeNullable(_$ViewTypeEnumMap, json['preferredViewType']) ?? + ViewType.list, + preferredColorSchemeOption: $enumDecodeNullable( + _$ColorSchemeOptionEnumMap, json['preferredColorSchemeOption']) ?? + ColorSchemeOption.dynamic, ); Map _$ApplicationSettingsStateToJson( @@ -25,6 +30,8 @@ Map _$ApplicationSettingsStateToJson( 'preferredLocaleSubtag': instance.preferredLocaleSubtag, 'preferredThemeMode': _$ThemeModeEnumMap[instance.preferredThemeMode]!, 'preferredViewType': _$ViewTypeEnumMap[instance.preferredViewType]!, + 'preferredColorSchemeOption': + _$ColorSchemeOptionEnumMap[instance.preferredColorSchemeOption]!, }; const _$ThemeModeEnumMap = { @@ -37,3 +44,8 @@ const _$ViewTypeEnumMap = { ViewType.grid: 'grid', ViewType.list: 'list', }; + +const _$ColorSchemeOptionEnumMap = { + ColorSchemeOption.classic: 'classic', + ColorSchemeOption.dynamic: 'dynamic', +}; diff --git a/lib/features/settings/model/color_scheme_option.dart b/lib/features/settings/model/color_scheme_option.dart new file mode 100644 index 0000000..6bd92e3 --- /dev/null +++ b/lib/features/settings/model/color_scheme_option.dart @@ -0,0 +1,4 @@ +enum ColorSchemeOption { + classic, + dynamic; +} diff --git a/lib/features/settings/view/pages/application_settings_page.dart b/lib/features/settings/view/pages/application_settings_page.dart index b1b0720..6c8acb3 100644 --- a/lib/features/settings/view/pages/application_settings_page.dart +++ b/lib/features/settings/view/pages/application_settings_page.dart @@ -1,7 +1,12 @@ +import 'dart:developer'; +import 'dart:io'; + import 'package:flutter/material.dart'; +import 'package:paperless_mobile/features/settings/view/widgets/color_scheme_option_setting.dart'; import 'package:paperless_mobile/features/settings/view/widgets/language_selection_setting.dart'; import 'package:paperless_mobile/features/settings/view/widgets/theme_mode_setting.dart'; import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:paperless_mobile/constants.dart'; class ApplicationSettingsPage extends StatelessWidget { const ApplicationSettingsPage({super.key}); @@ -16,6 +21,7 @@ class ApplicationSettingsPage extends StatelessWidget { children: const [ LanguageSelectionSetting(), ThemeModeSetting(), + ColorSchemeOptionSetting(), ], ), ); diff --git a/lib/features/settings/view/pages/storage_settings_page.dart b/lib/features/settings/view/pages/storage_settings_page.dart index 0aefad0..1dba0e8 100644 --- a/lib/features/settings/view/pages/storage_settings_page.dart +++ b/lib/features/settings/view/pages/storage_settings_page.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:paperless_mobile/features/settings/view/widgets/clear_storage_setting.dart'; +import 'package:paperless_mobile/features/settings/view/widgets/clear_storage_settings.dart'; import 'package:paperless_mobile/generated/l10n.dart'; class StorageSettingsPage extends StatelessWidget { @@ -13,7 +13,7 @@ class StorageSettingsPage extends StatelessWidget { ), body: ListView( children: const [ - ClearStorageSetting(), + ClearCacheSetting(), ], ), ); diff --git a/lib/features/settings/view/widgets/biometric_authentication_setting.dart b/lib/features/settings/view/widgets/biometric_authentication_setting.dart index 3a49d6e..b182f42 100644 --- a/lib/features/settings/view/widgets/biometric_authentication_setting.dart +++ b/lib/features/settings/view/widgets/biometric_authentication_setting.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.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/features/settings/bloc/application_settings_state.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:provider/provider.dart'; diff --git a/lib/features/settings/view/widgets/clear_storage_setting.dart b/lib/features/settings/view/widgets/clear_storage_setting.dart deleted file mode 100644 index a956cfe..0000000 --- a/lib/features/settings/view/widgets/clear_storage_setting.dart +++ /dev/null @@ -1,21 +0,0 @@ -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:provider/provider.dart'; - -class ClearStorageSetting extends StatelessWidget { - const ClearStorageSetting({super.key}); - - @override - Widget build(BuildContext context) { - return ListTile( - title: Text("Clear data"), - subtitle: - Text("Remove downloaded files, scans and clear the cache's content"), - onTap: () { - context.read().emptyCache(); - FileService.clearUserData(); - }, - ); - } -} diff --git a/lib/features/settings/view/widgets/clear_storage_settings.dart b/lib/features/settings/view/widgets/clear_storage_settings.dart new file mode 100644 index 0000000..78709bf --- /dev/null +++ b/lib/features/settings/view/widgets/clear_storage_settings.dart @@ -0,0 +1,70 @@ +import 'dart:io'; + +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/helpers/format_helpers.dart'; +import 'package:paperless_mobile/helpers/message_helpers.dart'; +import 'package:provider/provider.dart'; + +class ClearCacheSetting extends StatelessWidget { + const ClearCacheSetting({super.key}); + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text("Clear downloaded files"), //TODO: INTL + subtitle: + Text("Deletes all files downloaded from this app."), //TODO: INTL + onTap: () async { + final dir = await FileService.downloadsDirectory; + final deletedSize = _dirSize(dir); + await dir.delete(recursive: true); + // await context.read().emptyCache(); + showSnackBar( + context, + "Downloads successfully cleared, removed $deletedSize.", + ); + }, + ); + } +} + +class ClearDownloadsSetting extends StatelessWidget { + const ClearDownloadsSetting({super.key}); + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text("Clear downloads"), //TODO: INTL + subtitle: Text( + "Remove downloaded files, scans and clear the cache's content"), //TODO: INTL + onTap: () { + FileService.documentsDirectory; + FileService.downloadsDirectory; + context.read().emptyCache(); + FileService.clearUserData(); + //TODO: Show notification about clearing (include size?) + }, + ); + } +} + +String _dirSize(Directory dir) { + int totalSize = 0; + try { + if (dir.existsSync()) { + dir + .listSync(recursive: true, followLinks: false) + .forEach((FileSystemEntity entity) { + if (entity is File) { + totalSize += entity.lengthSync(); + } + }); + } + } catch (e) { + print(e.toString()); + } + + return formatBytes(totalSize, 2); +} diff --git a/lib/features/settings/view/widgets/color_scheme_option_setting.dart b/lib/features/settings/view/widgets/color_scheme_option_setting.dart new file mode 100644 index 0000000..15a6566 --- /dev/null +++ b/lib/features/settings/view/widgets/color_scheme_option_setting.dart @@ -0,0 +1,86 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_mobile/core/translation/color_scheme_option_localization_mapper.dart'; +import 'package:paperless_mobile/core/widgets/hint_card.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/bloc/application_settings_state.dart'; +import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart'; +import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:paperless_mobile/constants.dart'; +import 'package:provider/provider.dart'; + +class ColorSchemeOptionSetting extends StatelessWidget { + const ColorSchemeOptionSetting({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, settings) { + return ListTile( + title: Text(S.of(context).settingsPageColorSchemeSettingLabel), + subtitle: Text( + translateColorSchemeOption( + context, + settings.preferredColorSchemeOption, + ), + ), + onTap: () => showDialog( + context: context, + builder: (_) => RadioSettingsDialog( + titleText: S.of(context).settingsPageColorSchemeSettingLabel, + descriptionText: + S.of(context).settingsPageColorSchemeSettingDialogDescription, + options: [ + RadioOption( + value: ColorSchemeOption.classic, + label: translateColorSchemeOption( + context, ColorSchemeOption.classic), + ), + RadioOption( + value: ColorSchemeOption.dynamic, + label: translateColorSchemeOption( + context, + ColorSchemeOption.dynamic, + ), + ), + ], + footer: _isBelowAndroid12() + ? HintCard( + hintText: S + .of(context) + .settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning, + hintIcon: Icons.warning_amber, + ) + : null, + initialValue: context + .read() + .state + .preferredColorSchemeOption, + ), + ).then( + (value) { + if (value != null) { + context + .read() + .setColorSchemeOption(value); + } + }, + ), + ); + }, + ); + } + + bool _isBelowAndroid12() { + if (Platform.isAndroid) { + final int version = + int.tryParse(androidInfo!.version.release ?? '0') ?? 0; + return version < 12; + } + return false; + } +} diff --git a/lib/features/settings/view/widgets/language_selection_setting.dart b/lib/features/settings/view/widgets/language_selection_setting.dart index b2b0a09..38470e4 100644 --- a/lib/features/settings/view/widgets/language_selection_setting.dart +++ b/lib/features/settings/view/widgets/language_selection_setting.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.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/bloc/application_settings_state.dart'; import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart'; import 'package:paperless_mobile/generated/l10n.dart'; @@ -30,7 +30,7 @@ class _LanguageSelectionSettingState extends State { onTap: () => showDialog( context: context, builder: (_) => RadioSettingsDialog( - title: Text(S.of(context).settingsPageLanguageSettingLabel), + titleText: S.of(context).settingsPageLanguageSettingLabel, options: [ RadioOption( value: 'en', @@ -54,8 +54,11 @@ class _LanguageSelectionSettingState extends State { .state .preferredLocaleSubtag, ), - ).then((value) => - context.read().setLocale(value)), + ).then((value) { + if (value != null) { + context.read().setLocale(value); + } + }), ); }, ); diff --git a/lib/features/settings/view/widgets/radio_settings_dialog.dart b/lib/features/settings/view/widgets/radio_settings_dialog.dart index 47c337c..c2d5d4c 100644 --- a/lib/features/settings/view/widgets/radio_settings_dialog.dart +++ b/lib/features/settings/view/widgets/radio_settings_dialog.dart @@ -4,7 +4,9 @@ import 'package:paperless_mobile/generated/l10n.dart'; class RadioSettingsDialog extends StatefulWidget { final List> options; final T initialValue; - final Widget? title; + final String? titleText; + final String? descriptionText; + final Widget? footer; final Widget? confirmButton; final Widget? cancelButton; @@ -12,9 +14,11 @@ class RadioSettingsDialog extends StatefulWidget { super.key, required this.options, required this.initialValue, - this.title, + this.titleText, this.confirmButton, this.cancelButton, + this.descriptionText, + this.footer, }); @override @@ -43,10 +47,16 @@ class _RadioSettingsDialogState extends State> { onPressed: () => Navigator.pop(context, _groupValue), child: Text(S.of(context).genericActionOkLabel)), ], - title: widget.title, + title: widget.titleText != null ? Text(widget.titleText!) : null, content: Column( mainAxisSize: MainAxisSize.min, - children: widget.options.map(_buildOptionListTile).toList(), + children: [ + if (widget.descriptionText != null) + Text(widget.descriptionText!, + style: Theme.of(context).textTheme.bodySmall), + ...widget.options.map(_buildOptionListTile), + if (widget.footer != null) widget.footer!, + ], ), ); } diff --git a/lib/features/settings/view/widgets/theme_mode_setting.dart b/lib/features/settings/view/widgets/theme_mode_setting.dart index 4430e2e..3c573ca 100644 --- a/lib/features/settings/view/widgets/theme_mode_setting.dart +++ b/lib/features/settings/view/widgets/theme_mode_setting.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.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/bloc/application_settings_state.dart'; import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart'; import 'package:paperless_mobile/generated/l10n.dart'; @@ -19,6 +19,11 @@ class ThemeModeSetting extends StatelessWidget { onTap: () => showDialog( context: context, builder: (_) => RadioSettingsDialog( + titleText: S.of(context).settingsPageAppearanceSettingTitle, + initialValue: context + .read() + .state + .preferredThemeMode, options: [ RadioOption( value: ThemeMode.system, @@ -38,14 +43,11 @@ class ThemeModeSetting extends StatelessWidget { S.of(context).settingsPageAppearanceSettingDarkThemeLabel, ) ], - initialValue: context - .read() - .state - .preferredThemeMode, - title: Text(S.of(context).settingsPageAppearanceSettingTitle), ), ).then((value) { - return context.read().setThemeMode(value); + if (value != null) { + context.read().setThemeMode(value); + } }), ); }, diff --git a/lib/helpers/format_helpers.dart b/lib/helpers/format_helpers.dart index a768ad0..5b863c4 100644 --- a/lib/helpers/format_helpers.dart +++ b/lib/helpers/format_helpers.dart @@ -1,6 +1,15 @@ +import 'dart:math'; + String formatMaxCount(int? count, [int maxCount = 99]) { if ((count ?? 0) > maxCount) { return "$maxCount+"; } return (count ?? 0).toString().padLeft(maxCount.toString().length); } + +String formatBytes(int bytes, int decimals) { + if (bytes <= 0) return "0 B"; + const suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; + var i = (log(bytes) / log(1024)).floor(); + return ((bytes / pow(1024, i)).toStringAsFixed(decimals)) + ' ' + suffixes[i]; +} diff --git a/lib/l10n/intl_cs.arb b/lib/l10n/intl_cs.arb index b425bbc..98806d0 100644 --- a/lib/l10n/intl_cs.arb +++ b/lib/l10n/intl_cs.arb @@ -611,5 +611,10 @@ "verifyIdentityPageTitle": "Ověř svou identitu", "@verifyIdentityPageTitle": {}, "verifyIdentityPageVerifyIdentityButtonLabel": "Ověřit identitu", - "@verifyIdentityPageVerifyIdentityButtonLabel": {} + "@verifyIdentityPageVerifyIdentityButtonLabel": {}, + "settingsPageColorSchemeSettingLabel": "Colors", + "colorSchemeOptionClassic": "Classic", + "colorSchemeOptionDznamic": "Dynamic", + "settingsPageColorSchemeSettingDialogDescription": "Choose between the classic color scheme in Paperless inspired green or use the dynamic color scheme based on your system theme.", + "settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": "Dynamic theming is only supported for devices running Android 12 and above. Using the 'dynamic' option might not have any effect depending on your OS implementation." } \ No newline at end of file diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 02b9924..e32b4e3 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -611,5 +611,10 @@ "verifyIdentityPageTitle": "Verifiziere deine Identität", "@verifyIdentityPageTitle": {}, "verifyIdentityPageVerifyIdentityButtonLabel": "Identität verifizieren", - "@verifyIdentityPageVerifyIdentityButtonLabel": {} + "@verifyIdentityPageVerifyIdentityButtonLabel": {}, + "settingsPageColorSchemeSettingLabel": "Colors", + "colorSchemeOptionClassic": "Classic", + "colorSchemeOptionDznamic": "Dynamic", + "settingsPageColorSchemeSettingDialogDescription": "Choose between the classic color scheme in Paperless inspired green or use the dynamic color scheme based on your system theme.", + "settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": "Dynamic theming is only supported for devices running Android 12 and above. Using the 'dynamic' option might not have any effect depending on your OS implementation." } \ No newline at end of file diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 72a3ce5..ba3400d 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -611,5 +611,10 @@ "verifyIdentityPageTitle": "Verify your identity", "@verifyIdentityPageTitle": {}, "verifyIdentityPageVerifyIdentityButtonLabel": "Verify Identity", - "@verifyIdentityPageVerifyIdentityButtonLabel": {} + "@verifyIdentityPageVerifyIdentityButtonLabel": {}, + "settingsPageColorSchemeSettingLabel": "Colors", + "colorSchemeOptionClassic": "Classic", + "colorSchemeOptionDynamic": "Dynamic", + "settingsPageColorSchemeSettingDialogDescription": "Choose between a classic color scheme inspired by a traditional Paperless green or use the dynamic color scheme based on your system theme.", + "settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": "Dynamic theming is only supported for devices running Android 12 and above. Selecting the 'Dynamic' option might not have any effect depending on your OS implementation." } \ No newline at end of file diff --git a/lib/l10n/intl_tr.arb b/lib/l10n/intl_tr.arb index 61dd7c7..f6a1e0b 100644 --- a/lib/l10n/intl_tr.arb +++ b/lib/l10n/intl_tr.arb @@ -611,5 +611,10 @@ "verifyIdentityPageTitle": "Kimliğinizi doğrulayın", "@verifyIdentityPageTitle": {}, "verifyIdentityPageVerifyIdentityButtonLabel": "Kimliği Doğrula", - "@verifyIdentityPageVerifyIdentityButtonLabel": {} + "@verifyIdentityPageVerifyIdentityButtonLabel": {}, + "settingsPageColorSchemeSettingLabel": "Colors", + "colorSchemeOptionClassic": "Classic", + "colorSchemeOptionDznamic": "Dynamic", + "settingsPageColorSchemeSettingDialogDescription": "Choose between the classic color scheme in Paperless inspired green or use the dynamic color scheme based on your system theme.", + "settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": "Dynamic theming is only supported for devices running Android 12 and above. Using the 'dynamic' option might not have any effect depending on your OS implementation." } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 06e69ca..6cf6736 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,8 @@ import 'dart:developer'; +import 'dart:io'; import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -12,6 +14,7 @@ import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:intl/date_symbol_data_local.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'; @@ -33,7 +36,6 @@ import 'package:paperless_mobile/core/security/session_manager.dart'; import 'package:paperless_mobile/core/service/connectivity_status_service.dart'; import 'package:paperless_mobile/core/service/dio_file_service.dart'; import 'package:paperless_mobile/core/service/file_service.dart'; -import 'package:paperless_mobile/core/store/local_vault.dart'; import 'package:paperless_mobile/features/app_intro/application_intro_slideshow.dart'; import 'package:paperless_mobile/features/home/view/home_page.dart'; import 'package:paperless_mobile/features/home/view/widget/verify_identity_page.dart'; @@ -43,29 +45,36 @@ import 'package:paperless_mobile/features/login/services/authentication_service. import 'package:paperless_mobile/features/login/view/login_page.dart'; import 'package:paperless_mobile/features/notifications/services/local_notification_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/features/settings/bloc/application_settings_state.dart'; import 'package:paperless_mobile/features/sharing/share_intent_queue.dart'; import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:paperless_mobile/theme.dart'; +import 'package:paperless_mobile/constants.dart'; import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; +import 'package:dynamic_color/dynamic_color.dart'; void main() async { Bloc.observer = BlocChangesObserver(); final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); await findSystemLocale(); + packageInfo = await PackageInfo.fromPlatform(); + if (Platform.isAndroid) { + androidInfo = await DeviceInfoPlugin().androidInfo; + } + if (Platform.isIOS) { + iosInfo = await DeviceInfoPlugin().iosInfo; + } // Initialize External dependencies final connectivity = Connectivity(); - final encryptedSharedPreferences = EncryptedSharedPreferences(); final localAuthentication = LocalAuthentication(); // Initialize other utility classes final connectivityStatusService = ConnectivityStatusServiceImpl(connectivity); - final localVault = LocalVaultImpl(encryptedSharedPreferences); - final localAuthService = - LocalAuthenticationService(localVault, localAuthentication); + final localAuthService = LocalAuthenticationService(localAuthentication); final hiveDir = await getApplicationDocumentsDirectory(); HydratedBloc.storage = await HydratedStorage.build( @@ -152,7 +161,6 @@ void main() async { ), ), ), - Provider.value(value: localVault), Provider.value( value: connectivityStatusService, ), @@ -207,22 +215,6 @@ class PaperlessMobileEntrypoint extends StatefulWidget { } class _PaperlessMobileEntrypointState extends State { - final _lightTheme = ThemeData.from( - colorScheme: ColorScheme.fromSeed( - seedColor: Colors.lightGreen, - brightness: Brightness.light, - ), - useMaterial3: true, - ); - - final _darkTheme = ThemeData.from( - colorScheme: ColorScheme.fromSeed( - seedColor: Colors.lightGreen, - brightness: Brightness.dark, - ), - useMaterial3: true, - ); - @override Widget build(BuildContext context) { return MultiBlocProvider( @@ -235,52 +227,36 @@ class _PaperlessMobileEntrypointState extends State { ], child: BlocBuilder( builder: (context, settings) { - return MaterialApp( - debugShowCheckedModeBanner: true, - title: "Paperless Mobile", - theme: _lightTheme.copyWith( - inputDecorationTheme: InputDecorationTheme( - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), + return DynamicColorBuilder( + builder: (lightDynamic, darkDynamic) { + return MaterialApp( + debugShowCheckedModeBanner: true, + title: "Paperless Mobile", + theme: buildTheme( + brightness: Brightness.light, + dynamicScheme: lightDynamic, + preferredColorScheme: settings.preferredColorSchemeOption, ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 16.0, + darkTheme: buildTheme( + brightness: Brightness.dark, + dynamicScheme: darkDynamic, + preferredColorScheme: settings.preferredColorSchemeOption, ), - ), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - listTileTheme: const ListTileThemeData( - tileColor: Colors.transparent, - ), - ), - darkTheme: _darkTheme.copyWith( - inputDecorationTheme: InputDecorationTheme( - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), + themeMode: settings.preferredThemeMode, + supportedLocales: S.delegate.supportedLocales, + locale: Locale.fromSubtags( + languageCode: settings.preferredLocaleSubtag, ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 16.0, - ), - ), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - listTileTheme: const ListTileThemeData( - tileColor: Colors.transparent, - ), - ), - themeMode: settings.preferredThemeMode, - supportedLocales: S.delegate.supportedLocales, - locale: Locale.fromSubtags( - languageCode: settings.preferredLocaleSubtag, - ), - localizationsDelegates: const [ - S.delegate, - GlobalMaterialLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - FormBuilderLocalizations.delegate, - ], - home: const AuthenticationWrapper(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + FormBuilderLocalizations.delegate, + ], + home: const AuthenticationWrapper(), + ); + }, ); }, ), diff --git a/lib/theme.dart b/lib/theme.dart new file mode 100644 index 0000000..172978b --- /dev/null +++ b/lib/theme.dart @@ -0,0 +1,47 @@ +import 'package:dynamic_color/dynamic_color.dart'; +import 'package:flutter/material.dart'; +import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart'; + +const _classicThemeColorSeed = Colors.lightGreen; + +const _defaultListTileTheme = ListTileThemeData( + tileColor: Colors.transparent, +); + +final _defaultInputDecorationTheme = InputDecorationTheme( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 16.0, + ), +); + +ThemeData buildTheme({ + required Brightness brightness, + required ColorSchemeOption preferredColorScheme, + ColorScheme? dynamicScheme, +}) { + final classicScheme = ColorScheme.fromSeed( + seedColor: _classicThemeColorSeed, + brightness: brightness, + ); + late ColorScheme colorScheme; + switch (preferredColorScheme) { + case ColorSchemeOption.classic: + colorScheme = classicScheme; + break; + case ColorSchemeOption.dynamic: + colorScheme = dynamicScheme ?? classicScheme; + break; + } + return ThemeData.from( + colorScheme: colorScheme.harmonized(), + useMaterial3: true, + ).copyWith( + inputDecorationTheme: _defaultInputDecorationTheme, + listTileTheme: _defaultListTileTheme, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ); +} diff --git a/lib/util.dart b/lib/util.dart deleted file mode 100644 index 8b13789..0000000 --- a/lib/util.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/pubspec.lock b/pubspec.lock index 09ea6a0..1157538 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -433,6 +433,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + dynamic_color: + dependency: "direct main" + description: + name: dynamic_color + sha256: "37a15576f5a0bfd5555b613cf20ea3bd379607cf88d457374a16032f4e942174" + url: "https://pub.dev" + source: hosted + version: "1.5.4" edge_detection: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 0f94483..c50f2f0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -87,6 +87,7 @@ dependencies: flutter_staggered_grid_view: ^0.6.2 responsive_builder: ^0.4.3 open_filex: ^4.3.2 + dynamic_color: ^1.5.4 dev_dependencies: integration_test: