mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-08 18:08:07 -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 '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() {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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() ?? [],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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();
|
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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user