mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-07 13:15:55 -06:00
Implemented better tags form field, persistent grid view setting, fixed hidden items in documents list
This commit is contained in:
@@ -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<bool> storeApplicationSettings(ApplicationSettingsState settings) {
|
||||
return sharedPreferences.setString(
|
||||
applicationSettingsKey, json.encode(settings.toJson()));
|
||||
applicationSettingsKey,
|
||||
jsonEncode(settings.toJson()),
|
||||
);
|
||||
}
|
||||
|
||||
Future<ApplicationSettingsState?> 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<void> clear() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<DocumentsPage> {
|
||||
);
|
||||
|
||||
final PanelController _panelController = PanelController();
|
||||
ViewType _viewType = ViewType.list;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -149,76 +151,79 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
||||
}
|
||||
|
||||
Widget _buildBody(ConnectivityState connectivityState) {
|
||||
return BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
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<ApplicationSettingsCubit, ApplicationSettingsState>(
|
||||
builder: (context, settings) {
|
||||
return BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
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<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(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MultiBlocProvider(
|
||||
builder: (_) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(value: getIt<DocumentsCubit>()),
|
||||
BlocProvider.value(value: getIt<CorrespondentCubit>()),
|
||||
BlocProvider.value(value: getIt<DocumentTypeCubit>()),
|
||||
BlocProvider.value(value: getIt<TagCubit>()),
|
||||
BlocProvider.value(value: getIt<StoragePathCubit>()),
|
||||
BlocProvider.value(value: BlocProvider.of<DocumentsCubit>(context)),
|
||||
BlocProvider.value(
|
||||
value: BlocProvider.of<CorrespondentCubit>(context)),
|
||||
BlocProvider.value(
|
||||
value: BlocProvider.of<DocumentTypeCubit>(context)),
|
||||
BlocProvider.value(value: BlocProvider.of<TagCubit>(context)),
|
||||
BlocProvider.value(
|
||||
value: BlocProvider.of<StoragePathCubit>(context)),
|
||||
],
|
||||
child: DocumentDetailsPage(
|
||||
documentId: model.id,
|
||||
@@ -244,12 +252,3 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum ViewType {
|
||||
grid,
|
||||
list;
|
||||
|
||||
ViewType toggle() {
|
||||
return this == grid ? list : grid;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,6 +155,7 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
||||
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(
|
||||
|
||||
@@ -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<TagCubit>(context),
|
||||
initialName: initialValue,
|
||||
additionalFields: [
|
||||
FormBuilderColorPickerField(
|
||||
name: Tag.colorKey,
|
||||
|
||||
@@ -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<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
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<TagCubit, Map<int, Tag>>(
|
||||
builder: (context, tagState) {
|
||||
return FormBuilderField<TagsQuery>(
|
||||
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(
|
||||
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<int> 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<int>(
|
||||
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<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() ?? [],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -151,7 +151,16 @@ class _LabelsPageState extends State<LabelsPage>
|
||||
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:
|
||||
|
||||
@@ -84,9 +84,13 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
|
||||
Future<void> 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 {
|
||||
|
||||
@@ -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<ApplicationSettingsState> {
|
||||
@@ -28,13 +29,18 @@ class ApplicationSettingsCubit extends Cubit<ApplicationSettingsState> {
|
||||
_updateSettings(updatedSettings);
|
||||
}
|
||||
|
||||
Future<void> _updateSettings(ApplicationSettingsState settings) async {
|
||||
await localVault.storeApplicationSettings(settings);
|
||||
emit(settings);
|
||||
}
|
||||
|
||||
Future<void> setThemeMode(ThemeMode? selectedMode) async {
|
||||
final updatedSettings = state.copyWith(preferredThemeMode: selectedMode);
|
||||
_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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
8
lib/features/settings/model/view_type.dart
Normal file
8
lib/features/settings/model/view_type.dart
Normal file
@@ -0,0 +1,8 @@
|
||||
enum ViewType {
|
||||
grid,
|
||||
list;
|
||||
|
||||
ViewType toggle() {
|
||||
return this == grid ? list : grid;
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,7 @@ void main() async {
|
||||
getIt<ConnectivityCubit>().initialize();
|
||||
await getIt<ApplicationSettingsCubit>().initialize();
|
||||
await getIt<AuthenticationCubit>().initialize();
|
||||
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
@@ -64,9 +65,9 @@ class _MyAppState extends State<MyApp> {
|
||||
BlocProvider.value(value: getIt<ConnectivityCubit>()),
|
||||
BlocProvider.value(value: getIt<AuthenticationCubit>()),
|
||||
BlocProvider.value(value: getIt<PaperlessServerInformationCubit>()),
|
||||
BlocProvider.value(value: getIt<ApplicationSettingsCubit>()),
|
||||
],
|
||||
child: BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
|
||||
bloc: getIt<ApplicationSettingsCubit>(),
|
||||
builder: (context, settings) {
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: true,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user