From 67ddf90a411c0364f00ef69eb5dbc11c9ee4c55e Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Tue, 15 Nov 2022 16:12:35 +0100 Subject: [PATCH] Implemented better tags form field, persistent grid view setting, fixed hidden items in documents list --- lib/core/store/local_vault.dart | 15 +- .../repository/document_repository_impl.dart | 3 +- .../documents/view/pages/documents_page.dart | 159 ++++++------ .../widgets/search/document_filter_panel.dart | 1 + .../labels/tags/view/pages/add_tag_page.dart | 4 +- .../tags/view/widgets/tags_form_field.dart | 228 +++++++++++++----- .../labels/tags/view/widgets/tags_widget.dart | 2 + .../labels/view/pages/labels_page.dart | 11 +- .../login/bloc/authentication_cubit.dart | 10 +- .../bloc/application_settings_cubit.dart | 16 +- .../model/application_settings_state.dart | 23 +- lib/features/settings/model/view_type.dart | 8 + lib/main.dart | 3 +- pubspec.lock | 2 +- pubspec.yaml | 1 + 15 files changed, 322 insertions(+), 164 deletions(-) create mode 100644 lib/features/settings/model/view_type.dart diff --git a/lib/core/store/local_vault.dart b/lib/core/store/local_vault.dart index 442ddb6..b8adcb9 100644 --- a/lib/core/store/local_vault.dart +++ b/lib/core/store/local_vault.dart @@ -1,6 +1,8 @@ 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'; @@ -20,7 +22,7 @@ class LocalVault { ) async { await sharedPreferences.setString( authenticationKey, - json.encode(auth.toJson()), + jsonEncode(auth.toJson()), ); } @@ -29,7 +31,7 @@ class LocalVault { return null; } return AuthenticationInformation.fromJson( - json.decode(await sharedPreferences.getString(authenticationKey)), + jsonDecode(await sharedPreferences.getString(authenticationKey)), ); } @@ -40,7 +42,9 @@ class LocalVault { Future storeApplicationSettings(ApplicationSettingsState settings) { return sharedPreferences.setString( - applicationSettingsKey, json.encode(settings.toJson())); + applicationSettingsKey, + jsonEncode(settings.toJson()), + ); } Future loadApplicationSettings() async { @@ -48,7 +52,10 @@ class LocalVault { if (settings.isEmpty) { return null; } - return ApplicationSettingsState.fromJson(json.decode(settings)); + return compute( + ApplicationSettingsState.fromJson, + jsonDecode(settings) as JSON, + ); } Future clear() { diff --git a/lib/features/documents/repository/document_repository_impl.dart b/lib/features/documents/repository/document_repository_impl.dart index 06a2f32..5198731 100644 --- a/lib/features/documents/repository/document_repository_impl.dart +++ b/lib/features/documents/repository/document_repository_impl.dart @@ -138,8 +138,7 @@ class DocumentRepositoryImpl implements DocumentRepository { body: json.encode(doc.toJson()), headers: {"Content-Type": "application/json"}).timeout(requestTimeout); if (response.statusCode == 200) { - return compute( - DocumentModel.fromJson, + return DocumentModel.fromJson( jsonDecode(utf8.decode(response.bodyBytes)) as JSON, ); } else { diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index 46403b9..ffe754c 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -22,6 +22,9 @@ import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart'; import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart'; import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart'; import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart'; +import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; +import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; +import 'package:paperless_mobile/features/settings/model/view_type.dart'; import 'package:paperless_mobile/util.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:sliding_up_panel/sliding_up_panel.dart'; @@ -40,7 +43,6 @@ class _DocumentsPageState extends State { ); final PanelController _panelController = PanelController(); - ViewType _viewType = ViewType.list; @override void initState() { @@ -149,76 +151,79 @@ class _DocumentsPageState extends State { } Widget _buildBody(ConnectivityState connectivityState) { - return BlocBuilder( - builder: (context, state) { - // Some ugly tricks to make it work with bloc, update pageController - _pagingController.value = PagingState( - itemList: state.documents, - nextPageKey: state.nextPageNumber, - ); - - late Widget child; - switch (_viewType) { - case ViewType.list: - child = DocumentListView( - onTap: _openDocumentDetails, - state: state, - onSelected: _onSelected, - pagingController: _pagingController, - hasInternetConnection: - connectivityState == ConnectivityState.connected, + return BlocBuilder( + builder: (context, settings) { + return BlocBuilder( + builder: (context, state) { + // Some ugly tricks to make it work with bloc, update pageController + _pagingController.value = PagingState( + itemList: state.documents, + nextPageKey: state.nextPageNumber, ); - break; - case ViewType.grid: - child = DocumentGridView( - onTap: _openDocumentDetails, - state: state, - onSelected: _onSelected, - pagingController: _pagingController, - hasInternetConnection: - connectivityState == ConnectivityState.connected); - break; - } - if (state.isLoaded && state.documents.isEmpty) { - child = SliverToBoxAdapter( - child: DocumentsEmptyState( - state: state, - ), - ); - } + late Widget child; + switch (settings.preferredViewType) { + case ViewType.list: + child = DocumentListView( + onTap: _openDocumentDetails, + state: state, + onSelected: _onSelected, + pagingController: _pagingController, + hasInternetConnection: + connectivityState == ConnectivityState.connected, + ); + break; + case ViewType.grid: + child = DocumentGridView( + onTap: _openDocumentDetails, + state: state, + onSelected: _onSelected, + pagingController: _pagingController, + hasInternetConnection: + connectivityState == ConnectivityState.connected); + break; + } - return RefreshIndicator( - onRefresh: _onRefresh, - child: Container( - padding: const EdgeInsets.only( - bottom: 48 + kBottomNavigationBarHeight + 48, - ), // Prevents panel from hiding scrollable content - child: CustomScrollView( - slivers: [ - DocumentsPageAppBar( - actions: [ - const SortDocumentsButton(), - IconButton( - icon: Icon( - _viewType == ViewType.grid - ? Icons.list - : Icons.grid_view, - ), - onPressed: () => - setState(() => _viewType = _viewType.toggle()), + if (state.isLoaded && state.documents.isEmpty) { + child = SliverToBoxAdapter( + child: DocumentsEmptyState( + state: state, + ), + ); + } + + return RefreshIndicator( + onRefresh: _onRefresh, + child: Container( + child: CustomScrollView( + slivers: [ + DocumentsPageAppBar( + actions: [ + const SortDocumentsButton(), + IconButton( + icon: Icon( + settings.preferredViewType == ViewType.grid + ? Icons.list + : Icons.grid_view, + ), + onPressed: () => + BlocProvider.of(context) + .setViewType( + settings.preferredViewType.toggle()), + ), + ], ), + child, + SliverToBoxAdapter( + child: SizedBox( + height: MediaQuery.of(context).size.height / 4, + ), + ) ], ), - child, - // SliverToBoxAdapter( - // child: SizedBox( - // height: MediaQuery.of(context).size.height / 3, - // ), - // ) - ], - ), - ), + ), + ); + }, ); }, ); @@ -228,13 +233,16 @@ class _DocumentsPageState extends State { Navigator.push( context, MaterialPageRoute( - builder: (context) => MultiBlocProvider( + builder: (_) => 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: BlocProvider.of(context)), + BlocProvider.value( + value: BlocProvider.of(context)), + BlocProvider.value( + value: BlocProvider.of(context)), + BlocProvider.value(value: BlocProvider.of(context)), + BlocProvider.value( + value: BlocProvider.of(context)), ], child: DocumentDetailsPage( documentId: model.id, @@ -244,12 +252,3 @@ class _DocumentsPageState extends State { ); } } - -enum ViewType { - grid, - list; - - ViewType toggle() { - return this == grid ? list : grid; - } -} diff --git a/lib/features/documents/view/widgets/search/document_filter_panel.dart b/lib/features/documents/view/widgets/search/document_filter_panel.dart index 6257cd1..0d406bd 100644 --- a/lib/features/documents/view/widgets/search/document_filter_panel.dart +++ b/lib/features/documents/view/widgets/search/document_filter_panel.dart @@ -155,6 +155,7 @@ class _DocumentFilterPanelState extends State { TagFormField( name: DocumentModel.tagsKey, initialValue: state.filter.tags, + allowCreation: false, ).padded(), // Required in order for the storage path field to be visible when typing const SizedBox( diff --git a/lib/features/labels/tags/view/pages/add_tag_page.dart b/lib/features/labels/tags/view/pages/add_tag_page.dart index bd9d201..b527643 100644 --- a/lib/features/labels/tags/view/pages/add_tag_page.dart +++ b/lib/features/labels/tags/view/pages/add_tag_page.dart @@ -8,7 +8,8 @@ import 'package:paperless_mobile/generated/l10n.dart'; import 'package:form_builder_extra_fields/form_builder_extra_fields.dart'; class AddTagPage extends StatelessWidget { - const AddTagPage({Key? key}) : super(key: key); + final String? initialValue; + const AddTagPage({Key? key, this.initialValue}) : super(key: key); @override Widget build(BuildContext context) { @@ -16,6 +17,7 @@ class AddTagPage extends StatelessWidget { addLabelStr: S.of(context).addTagPageTitle, fromJson: Tag.fromJson, cubit: BlocProvider.of(context), + initialName: initialValue, additionalFields: [ FormBuilderColorPickerField( name: Tag.colorKey, 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 8f495ef..ad4c14b 100644 --- a/lib/features/labels/tags/view/widgets/tags_form_field.dart +++ b/lib/features/labels/tags/view/widgets/tags_form_field.dart @@ -1,20 +1,25 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; -import 'package:paperless_mobile/extensions/flutter_extensions.dart'; +import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:paperless_mobile/features/documents/model/query_parameters/tags_query.dart'; import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart'; import 'package:paperless_mobile/features/labels/tags/model/tag.model.dart'; +import 'package:paperless_mobile/features/labels/tags/view/pages/add_tag_page.dart'; import 'package:paperless_mobile/generated/l10n.dart'; class TagFormField extends StatefulWidget { final TagsQuery? initialValue; final String name; + final bool allowCreation; + final bool notAssignedSelectable; const TagFormField({ super.key, required this.name, this.initialValue, + this.allowCreation = true, + this.notAssignedSelectable = true, }); @override @@ -22,72 +27,110 @@ class TagFormField extends StatefulWidget { } class _TagFormFieldState extends State { + late final TextEditingController _textEditingController; + bool _showCreationSuffixIcon = false; + bool _showClearSuffixIcon = false; + + @override + void initState() { + super.initState(); + final state = BlocProvider.of(context).state; + _textEditingController = TextEditingController() + ..addListener(() { + setState(() { + _showCreationSuffixIcon = state.values + .where( + (item) => item.name.toLowerCase().startsWith( + _textEditingController.text.toLowerCase(), + ), + ) + .isEmpty; + }); + setState(() => + _showClearSuffixIcon = _textEditingController.text.isNotEmpty); + }); + } + @override Widget build(BuildContext context) { return BlocBuilder>( builder: (context, tagState) { return FormBuilderField( builder: (field) { - final sortedTags = tagState.values.toList() - ..sort( - (a, b) => a.name.compareTo(b.name), - ); - //TODO: this is either not correctly resetting on filter reset or (when adding UniqueKey to FormField or ChipsInput) unmounts widget. - // return ChipsInput( - // chipBuilder: (context, state, data) => Chip( - // onDeleted: () => state.deleteChip(data), - // shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - // backgroundColor: Color(tagState[data]!.color ?? Colors.white.value), - // label: Text( - // tagState[data]!.name, - // style: TextStyle(color: Color(tagState[data]!.textColor ?? Colors.black.value)), - // ), - // ), - // suggestionBuilder: (context, state, data) => ListTile( - // title: Text(tagState[data]!.name), - // textColor: Color(tagState[data]!.textColor!), - // tileColor: Color(tagState[data]!.color!), - // onTap: () => state.selectSuggestion(data), - // ), - // findSuggestions: (query) => tagState.values - // .where((element) => element.name.toLowerCase().startsWith(query.toLowerCase())) - // .map((e) => e.id!) - // .toList(), - // onChanged: (tags) => field.didChange(tags), - // initialValue: field.value!, - // ); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - S.of(context).documentTagsPropertyLabel, - ), - Wrap( - children: sortedTags - .map((tag) => FilterChip( - label: Text( - tag.name, - style: TextStyle( - color: tag.textColor, - ), - ), - selectedColor: tag.color, - selected: - field.value?.ids.contains(tag.id) ?? false, - onSelected: (isSelected) { - List ids = [...field.value?.ids ?? []]; - if (isSelected) { - ids.add(tag.id!); - } else { - ids.remove(tag.id); - } - field.didChange(TagsQuery.fromIds(ids)); - }, - backgroundColor: tag.color, - )) - .toList() - .padded(const EdgeInsets.only(right: 4.0)), + TypeAheadField( + textFieldConfiguration: TextFieldConfiguration( + decoration: InputDecoration( + prefixIcon: const Icon( + Icons.label_outline, + ), + suffixIcon: _buildSuffixIcon(context, field), + labelText: S.of(context).documentTagsPropertyLabel, + hintText: S.of(context).tagFormFieldSearchHintText, + ), + controller: _textEditingController, + ), + suggestionsCallback: (query) { + final suggestions = tagState.values + .where((element) => element.name + .toLowerCase() + .startsWith(query.toLowerCase())) + .map((e) => e.id!) + .toList() + ..removeWhere((element) => + field.value?.ids.contains(element) ?? false); + if (widget.notAssignedSelectable) { + suggestions.insert(0, -1); + } + return suggestions; + }, + getImmediateSuggestions: true, + animationStart: 1, + itemBuilder: (context, data) { + if (data == -1) { + return ListTile( + title: Text(S.of(context).labelNotAssignedText), + ); + } + final tag = tagState[data]!; + return ListTile( + leading: Icon( + Icons.circle, + color: tag.color, + ), + title: Text( + tag.name, + style: TextStyle( + color: Theme.of(context).colorScheme.onBackground), + ), + ); + }, + onSuggestionSelected: (id) { + if (id == -1) { + field.didChange(const TagsQuery.notAssigned()); + return; + } else { + field.didChange( + TagsQuery.fromIds([...field.value?.ids ?? [], id])); + } + _textEditingController.clear(); + }, + direction: AxisDirection.up, ), + if (field.value?.onlyNotAssigned ?? false) ...[ + _buildNotAssignedTag(field) + ] else ...[ + Wrap( + alignment: WrapAlignment.start, + runAlignment: WrapAlignment.start, + spacing: 8.0, + children: (field.value?.ids ?? []) + .map((id) => _buildTag(field, tagState[id]!)) + .toList(), + ), + ] ], ); }, @@ -97,4 +140,75 @@ class _TagFormFieldState extends State { }, ); } + + Widget? _buildSuffixIcon( + BuildContext context, + FormFieldState field, + ) { + if (_showCreationSuffixIcon && widget.allowCreation) { + return IconButton( + onPressed: () => _onAddTag(context, field), + icon: const Icon( + Icons.new_label, + ), + ); + } + if (_showClearSuffixIcon) { + return IconButton( + icon: const Icon(Icons.clear), + onPressed: _textEditingController.clear, + ); + } + return null; + } + + void _onAddTag(BuildContext context, FormFieldState field) async { + final Tag? tag = await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => BlocProvider.value( + value: BlocProvider.of(context), + child: AddTagPage(initialValue: _textEditingController.text), + ), + ), + ); + if (tag != null) { + field.didChange( + TagsQuery.fromIds([...field.value?.ids ?? [], tag.id!]), + ); + } + _textEditingController.clear(); + // Call has to be delayed as otherwise the framework will not hide the keyboard directly after closing the add page. + Future.delayed( + const Duration(milliseconds: 100), + FocusScope.of(context).unfocus, + ); + } + + Widget _buildNotAssignedTag(FormFieldState field) { + return InputChip( + label: Text( + S.of(context).labelNotAssignedText, + ), + backgroundColor: + Theme.of(context).colorScheme.onSurface.withOpacity(0.12), + onDeleted: () => field.didChange( + const TagsQuery.unset(), + ), + ); + } + + Widget _buildTag(FormFieldState field, Tag tag) { + return InputChip( + label: Text( + tag.name, + style: TextStyle(color: tag.textColor), + ), + backgroundColor: tag.color, + onDeleted: () => field.didChange( + TagsQuery.fromIds( + field.value?.ids.where((element) => element != tag.id).toList() ?? [], + ), + ), + ); + } } diff --git a/lib/features/labels/tags/view/widgets/tags_widget.dart b/lib/features/labels/tags/view/widgets/tags_widget.dart index 212fa8b..d37d1be 100644 --- a/lib/features/labels/tags/view/widgets/tags_widget.dart +++ b/lib/features/labels/tags/view/widgets/tags_widget.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart'; diff --git a/lib/features/labels/view/pages/labels_page.dart b/lib/features/labels/view/pages/labels_page.dart index 1cc5e22..50dff9a 100644 --- a/lib/features/labels/view/pages/labels_page.dart +++ b/lib/features/labels/view/pages/labels_page.dart @@ -151,7 +151,16 @@ class _LabelsPageState extends State pageSize: label.documentCount ?? 0, ), onOpenEditPage: _openEditTagPage, - leadingBuilder: (t) => CircleAvatar(backgroundColor: t.color), + leadingBuilder: (t) => CircleAvatar( + backgroundColor: t.color, + child: t.isInboxTag ?? false + ? Icon( + Icons.inbox, + color: t.textColor, + ) + : null, + ), + contentBuilder: (t) => Text(t.match ?? ''), emptyStateActionButtonLabel: S.of(context).labelsPageTagsEmptyStateAddNewLabel, emptyStateDescription: diff --git a/lib/features/login/bloc/authentication_cubit.dart b/lib/features/login/bloc/authentication_cubit.dart index 7d50760..fa2692d 100644 --- a/lib/features/login/bloc/authentication_cubit.dart +++ b/lib/features/login/bloc/authentication_cubit.dart @@ -84,9 +84,13 @@ class AuthenticationCubit extends Cubit { Future restoreSessionState() async { final storedAuth = await localStore.loadAuthenticationInformation(); - final appSettings = await localStore.loadApplicationSettings() ?? - ApplicationSettingsState.defaultSettings; - + late ApplicationSettingsState? appSettings; + try { + appSettings = await localStore.loadApplicationSettings() ?? + ApplicationSettingsState.defaultSettings; + } catch (err) { + appSettings = ApplicationSettingsState.defaultSettings; + } if (storedAuth == null || !storedAuth.isValid) { emit(AuthenticationState(isAuthenticated: false, wasLoginStored: false)); } else { diff --git a/lib/features/settings/bloc/application_settings_cubit.dart b/lib/features/settings/bloc/application_settings_cubit.dart index e8f064d..5986b11 100644 --- a/lib/features/settings/bloc/application_settings_cubit.dart +++ b/lib/features/settings/bloc/application_settings_cubit.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_mobile/core/store/local_vault.dart'; import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; import 'package:injectable/injectable.dart'; +import 'package:paperless_mobile/features/settings/model/view_type.dart'; @singleton class ApplicationSettingsCubit extends Cubit { @@ -28,13 +29,18 @@ class ApplicationSettingsCubit extends Cubit { _updateSettings(updatedSettings); } - Future _updateSettings(ApplicationSettingsState settings) async { - await localVault.storeApplicationSettings(settings); - emit(settings); - } - Future setThemeMode(ThemeMode? selectedMode) async { final updatedSettings = state.copyWith(preferredThemeMode: selectedMode); _updateSettings(updatedSettings); } + + Future setViewType(ViewType viewType) async { + final updatedSettings = state.copyWith(preferredViewType: viewType); + _updateSettings(updatedSettings); + } + + Future _updateSettings(ApplicationSettingsState settings) async { + await localVault.storeApplicationSettings(settings); + emit(settings); + } } diff --git a/lib/features/settings/model/application_settings_state.dart b/lib/features/settings/model/application_settings_state.dart index fdad39f..31924a1 100644 --- a/lib/features/settings/model/application_settings_state.dart +++ b/lib/features/settings/model/application_settings_state.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:paperless_mobile/core/type/types.dart'; +import 'package:paperless_mobile/features/settings/model/view_type.dart'; /// /// State holding the current application settings such as selected language, theme mode and more. @@ -12,44 +13,47 @@ class ApplicationSettingsState { isLocalAuthenticationEnabled: false, preferredLocaleSubtag: Platform.localeName.split('_').first, preferredThemeMode: ThemeMode.system, + preferredViewType: ViewType.list, ); static const isLocalAuthenticationEnabledKey = "isLocalAuthenticationEnabled"; static const preferredLocaleSubtagKey = "localeSubtag"; static const preferredThemeModeKey = "preferredThemeModeKey"; + static const preferredViewTypeKey = 'preferredViewType'; final bool isLocalAuthenticationEnabled; final String preferredLocaleSubtag; - final ThemeMode preferredThemeMode; + final ViewType preferredViewType; ApplicationSettingsState({ required this.preferredLocaleSubtag, required this.preferredThemeMode, required this.isLocalAuthenticationEnabled, + required this.preferredViewType, }); JSON toJson() { return { isLocalAuthenticationEnabledKey: isLocalAuthenticationEnabled, preferredLocaleSubtagKey: preferredLocaleSubtag, - preferredThemeModeKey: preferredThemeMode.index, + preferredThemeModeKey: preferredThemeMode.name, + preferredViewTypeKey: preferredViewType.name, }; } ApplicationSettingsState.fromJson(JSON json) - : isLocalAuthenticationEnabled = json[isLocalAuthenticationEnabledKey] ?? - defaultSettings.isLocalAuthenticationEnabled, - preferredLocaleSubtag = json[preferredLocaleSubtagKey] ?? - Platform.localeName.split("_").first, - preferredThemeMode = json[preferredThemeModeKey] != null - ? ThemeMode.values[(json[preferredThemeModeKey])] - : defaultSettings.preferredThemeMode; + : isLocalAuthenticationEnabled = json[isLocalAuthenticationEnabledKey], + preferredLocaleSubtag = json[preferredLocaleSubtagKey], + preferredThemeMode = + ThemeMode.values.byName(json[preferredThemeModeKey]), + preferredViewType = ViewType.values.byName(json[preferredViewTypeKey]); ApplicationSettingsState copyWith({ bool? isLocalAuthenticationEnabled, String? preferredLocaleSubtag, ThemeMode? preferredThemeMode, + ViewType? preferredViewType, }) { return ApplicationSettingsState( isLocalAuthenticationEnabled: @@ -57,6 +61,7 @@ class ApplicationSettingsState { preferredLocaleSubtag: preferredLocaleSubtag ?? this.preferredLocaleSubtag, preferredThemeMode: preferredThemeMode ?? this.preferredThemeMode, + preferredViewType: preferredViewType ?? this.preferredViewType, ); } } diff --git a/lib/features/settings/model/view_type.dart b/lib/features/settings/model/view_type.dart new file mode 100644 index 0000000..6407dec --- /dev/null +++ b/lib/features/settings/model/view_type.dart @@ -0,0 +1,8 @@ +enum ViewType { + grid, + list; + + ViewType toggle() { + return this == grid ? list : grid; + } +} diff --git a/lib/main.dart b/lib/main.dart index f18cb1b..1a1b7c2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -46,6 +46,7 @@ void main() async { getIt().initialize(); await getIt().initialize(); await getIt().initialize(); + runApp(const MyApp()); } @@ -64,9 +65,9 @@ class _MyAppState extends State { BlocProvider.value(value: getIt()), BlocProvider.value(value: getIt()), BlocProvider.value(value: getIt()), + BlocProvider.value(value: getIt()), ], child: BlocBuilder( - bloc: getIt(), builder: (context, settings) { return MaterialApp( debugShowCheckedModeBanner: true, diff --git a/pubspec.lock b/pubspec.lock index 050e6d9..7dc7da5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -577,7 +577,7 @@ packages: source: hosted version: "2.0.0" flutter_typeahead: - dependency: transitive + dependency: "direct main" description: name: flutter_typeahead url: "https://pub.dartlang.org" diff --git a/pubspec.yaml b/pubspec.yaml index bde34e3..a21adb5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -79,6 +79,7 @@ dependencies: mime: ^1.0.2 receive_sharing_intent: ^1.4.5 uuid: ^3.0.6 + flutter_typeahead: ^4.1.1 dev_dependencies: integration_test: