Implemented better tags form field, persistent grid view setting, fixed hidden items in documents list

This commit is contained in:
Anton Stubenbord
2022-11-15 16:12:35 +01:00
parent 7fac53522a
commit 67ddf90a41
15 changed files with 322 additions and 164 deletions

View File

@@ -1,6 +1,8 @@
import 'dart:convert'; import 'dart:convert';
import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart'; 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/authentication_information.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart'; import 'package:paperless_mobile/features/login/model/client_certificate.dart';
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
@@ -20,7 +22,7 @@ class LocalVault {
) async { ) async {
await sharedPreferences.setString( await sharedPreferences.setString(
authenticationKey, authenticationKey,
json.encode(auth.toJson()), jsonEncode(auth.toJson()),
); );
} }
@@ -29,7 +31,7 @@ class LocalVault {
return null; return null;
} }
return AuthenticationInformation.fromJson( return AuthenticationInformation.fromJson(
json.decode(await sharedPreferences.getString(authenticationKey)), jsonDecode(await sharedPreferences.getString(authenticationKey)),
); );
} }
@@ -40,7 +42,9 @@ class LocalVault {
Future<bool> storeApplicationSettings(ApplicationSettingsState settings) { Future<bool> storeApplicationSettings(ApplicationSettingsState settings) {
return sharedPreferences.setString( return sharedPreferences.setString(
applicationSettingsKey, json.encode(settings.toJson())); applicationSettingsKey,
jsonEncode(settings.toJson()),
);
} }
Future<ApplicationSettingsState?> loadApplicationSettings() async { Future<ApplicationSettingsState?> loadApplicationSettings() async {
@@ -48,7 +52,10 @@ class LocalVault {
if (settings.isEmpty) { if (settings.isEmpty) {
return null; return null;
} }
return ApplicationSettingsState.fromJson(json.decode(settings)); return compute(
ApplicationSettingsState.fromJson,
jsonDecode(settings) as JSON,
);
} }
Future<void> clear() { Future<void> clear() {

View File

@@ -138,8 +138,7 @@ class DocumentRepositoryImpl implements DocumentRepository {
body: json.encode(doc.toJson()), body: json.encode(doc.toJson()),
headers: {"Content-Type": "application/json"}).timeout(requestTimeout); headers: {"Content-Type": "application/json"}).timeout(requestTimeout);
if (response.statusCode == 200) { if (response.statusCode == 200) {
return compute( return DocumentModel.fromJson(
DocumentModel.fromJson,
jsonDecode(utf8.decode(response.bodyBytes)) as JSON, jsonDecode(utf8.decode(response.bodyBytes)) as JSON,
); );
} else { } else {

View File

@@ -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/labels/storage_path/bloc/storage_path_cubit.dart';
import 'package:paperless_mobile/features/login/bloc/authentication_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/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:paperless_mobile/util.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:sliding_up_panel/sliding_up_panel.dart'; import 'package:sliding_up_panel/sliding_up_panel.dart';
@@ -40,7 +43,6 @@ class _DocumentsPageState extends State<DocumentsPage> {
); );
final PanelController _panelController = PanelController(); final PanelController _panelController = PanelController();
ViewType _viewType = ViewType.list;
@override @override
void initState() { void initState() {
@@ -149,76 +151,79 @@ class _DocumentsPageState extends State<DocumentsPage> {
} }
Widget _buildBody(ConnectivityState connectivityState) { Widget _buildBody(ConnectivityState connectivityState) {
return BlocBuilder<DocumentsCubit, DocumentsState>( return BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
builder: (context, state) { builder: (context, settings) {
// Some ugly tricks to make it work with bloc, update pageController return BlocBuilder<DocumentsCubit, DocumentsState>(
_pagingController.value = PagingState( builder: (context, state) {
itemList: state.documents, // Some ugly tricks to make it work with bloc, update pageController
nextPageKey: state.nextPageNumber, _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,
); );
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) { late Widget child;
child = SliverToBoxAdapter( switch (settings.preferredViewType) {
child: DocumentsEmptyState( case ViewType.list:
state: state, 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( if (state.isLoaded && state.documents.isEmpty) {
onRefresh: _onRefresh, child = SliverToBoxAdapter(
child: Container( child: DocumentsEmptyState(
padding: const EdgeInsets.only( state: state,
bottom: 48 + kBottomNavigationBarHeight + 48, ),
), // Prevents panel from hiding scrollable content );
child: CustomScrollView( }
slivers: [
DocumentsPageAppBar( return RefreshIndicator(
actions: [ onRefresh: _onRefresh,
const SortDocumentsButton(), child: Container(
IconButton( child: CustomScrollView(
icon: Icon( slivers: [
_viewType == ViewType.grid DocumentsPageAppBar(
? Icons.list actions: [
: Icons.grid_view, const SortDocumentsButton(),
), IconButton(
onPressed: () => icon: Icon(
setState(() => _viewType = _viewType.toggle()), settings.preferredViewType == ViewType.grid
? Icons.list
: Icons.grid_view,
),
onPressed: () =>
BlocProvider.of<ApplicationSettingsCubit>(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<DocumentsPage> {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => MultiBlocProvider( builder: (_) => MultiBlocProvider(
providers: [ providers: [
BlocProvider.value(value: getIt<DocumentsCubit>()), BlocProvider.value(value: BlocProvider.of<DocumentsCubit>(context)),
BlocProvider.value(value: getIt<CorrespondentCubit>()), BlocProvider.value(
BlocProvider.value(value: getIt<DocumentTypeCubit>()), value: BlocProvider.of<CorrespondentCubit>(context)),
BlocProvider.value(value: getIt<TagCubit>()), BlocProvider.value(
BlocProvider.value(value: getIt<StoragePathCubit>()), value: BlocProvider.of<DocumentTypeCubit>(context)),
BlocProvider.value(value: BlocProvider.of<TagCubit>(context)),
BlocProvider.value(
value: BlocProvider.of<StoragePathCubit>(context)),
], ],
child: DocumentDetailsPage( child: DocumentDetailsPage(
documentId: model.id, documentId: model.id,
@@ -244,12 +252,3 @@ class _DocumentsPageState extends State<DocumentsPage> {
); );
} }
} }
enum ViewType {
grid,
list;
ViewType toggle() {
return this == grid ? list : grid;
}
}

View File

@@ -155,6 +155,7 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
TagFormField( TagFormField(
name: DocumentModel.tagsKey, name: DocumentModel.tagsKey,
initialValue: state.filter.tags, initialValue: state.filter.tags,
allowCreation: false,
).padded(), ).padded(),
// Required in order for the storage path field to be visible when typing // Required in order for the storage path field to be visible when typing
const SizedBox( const SizedBox(

View File

@@ -8,7 +8,8 @@ import 'package:paperless_mobile/generated/l10n.dart';
import 'package:form_builder_extra_fields/form_builder_extra_fields.dart'; import 'package:form_builder_extra_fields/form_builder_extra_fields.dart';
class AddTagPage extends StatelessWidget { class AddTagPage extends StatelessWidget {
const AddTagPage({Key? key}) : super(key: key); final String? initialValue;
const AddTagPage({Key? key, this.initialValue}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -16,6 +17,7 @@ class AddTagPage extends StatelessWidget {
addLabelStr: S.of(context).addTagPageTitle, addLabelStr: S.of(context).addTagPageTitle,
fromJson: Tag.fromJson, fromJson: Tag.fromJson,
cubit: BlocProvider.of<TagCubit>(context), cubit: BlocProvider.of<TagCubit>(context),
initialName: initialValue,
additionalFields: [ additionalFields: [
FormBuilderColorPickerField( FormBuilderColorPickerField(
name: Tag.colorKey, name: Tag.colorKey,

View File

@@ -1,20 +1,25 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.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/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/bloc/tags_cubit.dart';
import 'package:paperless_mobile/features/labels/tags/model/tag.model.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'; import 'package:paperless_mobile/generated/l10n.dart';
class TagFormField extends StatefulWidget { class TagFormField extends StatefulWidget {
final TagsQuery? initialValue; final TagsQuery? initialValue;
final String name; final String name;
final bool allowCreation;
final bool notAssignedSelectable;
const TagFormField({ const TagFormField({
super.key, super.key,
required this.name, required this.name,
this.initialValue, this.initialValue,
this.allowCreation = true,
this.notAssignedSelectable = true,
}); });
@override @override
@@ -22,72 +27,110 @@ class TagFormField extends StatefulWidget {
} }
class _TagFormFieldState extends State<TagFormField> { class _TagFormFieldState extends State<TagFormField> {
late final TextEditingController _textEditingController;
bool _showCreationSuffixIcon = false;
bool _showClearSuffixIcon = false;
@override
void initState() {
super.initState();
final state = BlocProvider.of<TagCubit>(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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<TagCubit, Map<int, Tag>>( return BlocBuilder<TagCubit, Map<int, Tag>>(
builder: (context, tagState) { builder: (context, tagState) {
return FormBuilderField<TagsQuery>( return FormBuilderField<TagsQuery>(
builder: (field) { 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<int>(
// 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( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( TypeAheadField<int>(
S.of(context).documentTagsPropertyLabel, textFieldConfiguration: TextFieldConfiguration(
), decoration: InputDecoration(
Wrap( prefixIcon: const Icon(
children: sortedTags Icons.label_outline,
.map((tag) => FilterChip( ),
label: Text( suffixIcon: _buildSuffixIcon(context, field),
tag.name, labelText: S.of(context).documentTagsPropertyLabel,
style: TextStyle( hintText: S.of(context).tagFormFieldSearchHintText,
color: tag.textColor, ),
), controller: _textEditingController,
), ),
selectedColor: tag.color, suggestionsCallback: (query) {
selected: final suggestions = tagState.values
field.value?.ids.contains(tag.id) ?? false, .where((element) => element.name
onSelected: (isSelected) { .toLowerCase()
List<int> ids = [...field.value?.ids ?? []]; .startsWith(query.toLowerCase()))
if (isSelected) { .map((e) => e.id!)
ids.add(tag.id!); .toList()
} else { ..removeWhere((element) =>
ids.remove(tag.id); field.value?.ids.contains(element) ?? false);
} if (widget.notAssignedSelectable) {
field.didChange(TagsQuery.fromIds(ids)); suggestions.insert(0, -1);
}, }
backgroundColor: tag.color, return suggestions;
)) },
.toList() getImmediateSuggestions: true,
.padded(const EdgeInsets.only(right: 4.0)), 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<TagFormField> {
}, },
); );
} }
Widget? _buildSuffixIcon(
BuildContext context,
FormFieldState<TagsQuery> 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<TagsQuery> field) async {
final Tag? tag = await Navigator.of(context).push<Tag>(
MaterialPageRoute(
builder: (_) => BlocProvider.value(
value: BlocProvider.of<TagCubit>(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<TagsQuery> 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<TagsQuery> 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() ?? [],
),
),
);
}
} }

View File

@@ -1,3 +1,5 @@
import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart'; import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart';

View File

@@ -151,7 +151,16 @@ class _LabelsPageState extends State<LabelsPage>
pageSize: label.documentCount ?? 0, pageSize: label.documentCount ?? 0,
), ),
onOpenEditPage: _openEditTagPage, 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: emptyStateActionButtonLabel:
S.of(context).labelsPageTagsEmptyStateAddNewLabel, S.of(context).labelsPageTagsEmptyStateAddNewLabel,
emptyStateDescription: emptyStateDescription:

View File

@@ -84,9 +84,13 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
Future<void> restoreSessionState() async { Future<void> restoreSessionState() async {
final storedAuth = await localStore.loadAuthenticationInformation(); final storedAuth = await localStore.loadAuthenticationInformation();
final appSettings = await localStore.loadApplicationSettings() ?? late ApplicationSettingsState? appSettings;
ApplicationSettingsState.defaultSettings; try {
appSettings = await localStore.loadApplicationSettings() ??
ApplicationSettingsState.defaultSettings;
} catch (err) {
appSettings = ApplicationSettingsState.defaultSettings;
}
if (storedAuth == null || !storedAuth.isValid) { if (storedAuth == null || !storedAuth.isValid) {
emit(AuthenticationState(isAuthenticated: false, wasLoginStored: false)); emit(AuthenticationState(isAuthenticated: false, wasLoginStored: false));
} else { } else {

View File

@@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/store/local_vault.dart'; import 'package:paperless_mobile/core/store/local_vault.dart';
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart';
@singleton @singleton
class ApplicationSettingsCubit extends Cubit<ApplicationSettingsState> { class ApplicationSettingsCubit extends Cubit<ApplicationSettingsState> {
@@ -28,13 +29,18 @@ class ApplicationSettingsCubit extends Cubit<ApplicationSettingsState> {
_updateSettings(updatedSettings); _updateSettings(updatedSettings);
} }
Future<void> _updateSettings(ApplicationSettingsState settings) async {
await localVault.storeApplicationSettings(settings);
emit(settings);
}
Future<void> setThemeMode(ThemeMode? selectedMode) async { Future<void> setThemeMode(ThemeMode? selectedMode) async {
final updatedSettings = state.copyWith(preferredThemeMode: selectedMode); final updatedSettings = state.copyWith(preferredThemeMode: selectedMode);
_updateSettings(updatedSettings); _updateSettings(updatedSettings);
} }
Future<void> setViewType(ViewType viewType) async {
final updatedSettings = state.copyWith(preferredViewType: viewType);
_updateSettings(updatedSettings);
}
Future<void> _updateSettings(ApplicationSettingsState settings) async {
await localVault.storeApplicationSettings(settings);
emit(settings);
}
} }

View File

@@ -2,6 +2,7 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:paperless_mobile/core/type/types.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. /// State holding the current application settings such as selected language, theme mode and more.
@@ -12,44 +13,47 @@ class ApplicationSettingsState {
isLocalAuthenticationEnabled: false, isLocalAuthenticationEnabled: false,
preferredLocaleSubtag: Platform.localeName.split('_').first, preferredLocaleSubtag: Platform.localeName.split('_').first,
preferredThemeMode: ThemeMode.system, preferredThemeMode: ThemeMode.system,
preferredViewType: ViewType.list,
); );
static const isLocalAuthenticationEnabledKey = "isLocalAuthenticationEnabled"; static const isLocalAuthenticationEnabledKey = "isLocalAuthenticationEnabled";
static const preferredLocaleSubtagKey = "localeSubtag"; static const preferredLocaleSubtagKey = "localeSubtag";
static const preferredThemeModeKey = "preferredThemeModeKey"; static const preferredThemeModeKey = "preferredThemeModeKey";
static const preferredViewTypeKey = 'preferredViewType';
final bool isLocalAuthenticationEnabled; final bool isLocalAuthenticationEnabled;
final String preferredLocaleSubtag; final String preferredLocaleSubtag;
final ThemeMode preferredThemeMode; final ThemeMode preferredThemeMode;
final ViewType preferredViewType;
ApplicationSettingsState({ ApplicationSettingsState({
required this.preferredLocaleSubtag, required this.preferredLocaleSubtag,
required this.preferredThemeMode, required this.preferredThemeMode,
required this.isLocalAuthenticationEnabled, required this.isLocalAuthenticationEnabled,
required this.preferredViewType,
}); });
JSON toJson() { JSON toJson() {
return { return {
isLocalAuthenticationEnabledKey: isLocalAuthenticationEnabled, isLocalAuthenticationEnabledKey: isLocalAuthenticationEnabled,
preferredLocaleSubtagKey: preferredLocaleSubtag, preferredLocaleSubtagKey: preferredLocaleSubtag,
preferredThemeModeKey: preferredThemeMode.index, preferredThemeModeKey: preferredThemeMode.name,
preferredViewTypeKey: preferredViewType.name,
}; };
} }
ApplicationSettingsState.fromJson(JSON json) ApplicationSettingsState.fromJson(JSON json)
: isLocalAuthenticationEnabled = json[isLocalAuthenticationEnabledKey] ?? : isLocalAuthenticationEnabled = json[isLocalAuthenticationEnabledKey],
defaultSettings.isLocalAuthenticationEnabled, preferredLocaleSubtag = json[preferredLocaleSubtagKey],
preferredLocaleSubtag = json[preferredLocaleSubtagKey] ?? preferredThemeMode =
Platform.localeName.split("_").first, ThemeMode.values.byName(json[preferredThemeModeKey]),
preferredThemeMode = json[preferredThemeModeKey] != null preferredViewType = ViewType.values.byName(json[preferredViewTypeKey]);
? ThemeMode.values[(json[preferredThemeModeKey])]
: defaultSettings.preferredThemeMode;
ApplicationSettingsState copyWith({ ApplicationSettingsState copyWith({
bool? isLocalAuthenticationEnabled, bool? isLocalAuthenticationEnabled,
String? preferredLocaleSubtag, String? preferredLocaleSubtag,
ThemeMode? preferredThemeMode, ThemeMode? preferredThemeMode,
ViewType? preferredViewType,
}) { }) {
return ApplicationSettingsState( return ApplicationSettingsState(
isLocalAuthenticationEnabled: isLocalAuthenticationEnabled:
@@ -57,6 +61,7 @@ class ApplicationSettingsState {
preferredLocaleSubtag: preferredLocaleSubtag:
preferredLocaleSubtag ?? this.preferredLocaleSubtag, preferredLocaleSubtag ?? this.preferredLocaleSubtag,
preferredThemeMode: preferredThemeMode ?? this.preferredThemeMode, preferredThemeMode: preferredThemeMode ?? this.preferredThemeMode,
preferredViewType: preferredViewType ?? this.preferredViewType,
); );
} }
} }

View File

@@ -0,0 +1,8 @@
enum ViewType {
grid,
list;
ViewType toggle() {
return this == grid ? list : grid;
}
}

View File

@@ -46,6 +46,7 @@ void main() async {
getIt<ConnectivityCubit>().initialize(); getIt<ConnectivityCubit>().initialize();
await getIt<ApplicationSettingsCubit>().initialize(); await getIt<ApplicationSettingsCubit>().initialize();
await getIt<AuthenticationCubit>().initialize(); await getIt<AuthenticationCubit>().initialize();
runApp(const MyApp()); runApp(const MyApp());
} }
@@ -64,9 +65,9 @@ class _MyAppState extends State<MyApp> {
BlocProvider.value(value: getIt<ConnectivityCubit>()), BlocProvider.value(value: getIt<ConnectivityCubit>()),
BlocProvider.value(value: getIt<AuthenticationCubit>()), BlocProvider.value(value: getIt<AuthenticationCubit>()),
BlocProvider.value(value: getIt<PaperlessServerInformationCubit>()), BlocProvider.value(value: getIt<PaperlessServerInformationCubit>()),
BlocProvider.value(value: getIt<ApplicationSettingsCubit>()),
], ],
child: BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>( child: BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
bloc: getIt<ApplicationSettingsCubit>(),
builder: (context, settings) { builder: (context, settings) {
return MaterialApp( return MaterialApp(
debugShowCheckedModeBanner: true, debugShowCheckedModeBanner: true,

View File

@@ -577,7 +577,7 @@ packages:
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
flutter_typeahead: flutter_typeahead:
dependency: transitive dependency: "direct main"
description: description:
name: flutter_typeahead name: flutter_typeahead
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"

View File

@@ -79,6 +79,7 @@ dependencies:
mime: ^1.0.2 mime: ^1.0.2
receive_sharing_intent: ^1.4.5 receive_sharing_intent: ^1.4.5
uuid: ^3.0.6 uuid: ^3.0.6
flutter_typeahead: ^4.1.1
dev_dependencies: dev_dependencies:
integration_test: integration_test: