mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2026-01-31 02:24:59 -06:00
feat: Implement new tag form field
This commit is contained in:
+11
-5
@@ -54,6 +54,8 @@ PODS:
|
|||||||
- FMDB (2.7.5):
|
- FMDB (2.7.5):
|
||||||
- FMDB/standard (= 2.7.5)
|
- FMDB/standard (= 2.7.5)
|
||||||
- FMDB/standard (2.7.5)
|
- FMDB/standard (2.7.5)
|
||||||
|
- in_app_review (0.2.0):
|
||||||
|
- Flutter
|
||||||
- integration_test (0.0.1):
|
- integration_test (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- local_auth_ios (0.0.1):
|
- local_auth_ios (0.0.1):
|
||||||
@@ -99,16 +101,17 @@ DEPENDENCIES:
|
|||||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||||
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
||||||
|
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
|
||||||
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
||||||
- local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`)
|
- local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`)
|
||||||
- open_filex (from `.symlinks/plugins/open_filex/ios`)
|
- open_filex (from `.symlinks/plugins/open_filex/ios`)
|
||||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`)
|
||||||
- pdfx (from `.symlinks/plugins/pdfx/ios`)
|
- pdfx (from `.symlinks/plugins/pdfx/ios`)
|
||||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||||
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
||||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/ios`)
|
||||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
||||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||||
|
|
||||||
@@ -142,6 +145,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||||
fluttertoast:
|
fluttertoast:
|
||||||
:path: ".symlinks/plugins/fluttertoast/ios"
|
:path: ".symlinks/plugins/fluttertoast/ios"
|
||||||
|
in_app_review:
|
||||||
|
:path: ".symlinks/plugins/in_app_review/ios"
|
||||||
integration_test:
|
integration_test:
|
||||||
:path: ".symlinks/plugins/integration_test/ios"
|
:path: ".symlinks/plugins/integration_test/ios"
|
||||||
local_auth_ios:
|
local_auth_ios:
|
||||||
@@ -151,7 +156,7 @@ EXTERNAL SOURCES:
|
|||||||
package_info_plus:
|
package_info_plus:
|
||||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
:path: ".symlinks/plugins/path_provider_foundation/ios"
|
||||||
pdfx:
|
pdfx:
|
||||||
:path: ".symlinks/plugins/pdfx/ios"
|
:path: ".symlinks/plugins/pdfx/ios"
|
||||||
permission_handler_apple:
|
permission_handler_apple:
|
||||||
@@ -161,7 +166,7 @@ EXTERNAL SOURCES:
|
|||||||
share_plus:
|
share_plus:
|
||||||
:path: ".symlinks/plugins/share_plus/ios"
|
:path: ".symlinks/plugins/share_plus/ios"
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
:path: ".symlinks/plugins/shared_preferences_foundation/ios"
|
||||||
sqflite:
|
sqflite:
|
||||||
:path: ".symlinks/plugins/sqflite/ios"
|
:path: ".symlinks/plugins/sqflite/ios"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
@@ -180,6 +185,7 @@ SPEC CHECKSUMS:
|
|||||||
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
|
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
|
||||||
fluttertoast: eb263d302cc92e04176c053d2385237e9f43fad0
|
fluttertoast: eb263d302cc92e04176c053d2385237e9f43fad0
|
||||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||||
|
in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d
|
||||||
integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5
|
integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5
|
||||||
local_auth_ios: 0d333dde7780f669e66f19d2ff6005f3ea84008d
|
local_auth_ios: 0d333dde7780f669e66f19d2ff6005f3ea84008d
|
||||||
open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
|
open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
|
||||||
@@ -195,7 +201,7 @@ SPEC CHECKSUMS:
|
|||||||
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
||||||
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
|
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
|
||||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
||||||
url_launcher_ios: ae1517e5e344f5544fb090b079e11f399dfbe4d2
|
url_launcher_ios: fb12c43172927bb5cf75aeebd073f883801f1993
|
||||||
WeScan: fed582f6c38014d529afb5aa9ffd1bad38fc72b7
|
WeScan: fed582f6c38014d529afb5aa9ffd1bad38fc72b7
|
||||||
|
|
||||||
PODFILE CHECKSUM: 7daa35cc908d9fba025075df27cb57a1ba1ebf13
|
PODFILE CHECKSUM: 7daa35cc908d9fba025075df27cb57a1ba1ebf13
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import 'package:paperless_mobile/features/document_edit/cubit/document_edit_cubi
|
|||||||
import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart';
|
import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart';
|
||||||
import 'package:paperless_mobile/features/edit_label/view/impl/add_document_type_page.dart';
|
import 'package:paperless_mobile/features/edit_label/view/impl/add_document_type_page.dart';
|
||||||
import 'package:paperless_mobile/features/edit_label/view/impl/add_storage_path_page.dart';
|
import 'package:paperless_mobile/features/edit_label/view/impl/add_storage_path_page.dart';
|
||||||
|
import 'package:paperless_mobile/features/labels/tags/view/widgets/tag_query_form_field.dart';
|
||||||
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
|
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
|
||||||
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
|
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
@@ -57,7 +58,6 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<DocumentEditCubit, DocumentEditState>(
|
return BlocBuilder<DocumentEditCubit, DocumentEditState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
log("Updated state. correspondents have ${state.correspondents.length} items.");
|
|
||||||
return DefaultTabController(
|
return DefaultTabController(
|
||||||
length: 2,
|
length: 2,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
@@ -207,57 +207,48 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
|||||||
],
|
],
|
||||||
).padded(),
|
).padded(),
|
||||||
// Tag form field
|
// Tag form field
|
||||||
TagFormField(
|
TagQueryFormField(
|
||||||
initialValue: IdsTagsQuery.included(
|
options: state.tags,
|
||||||
state.document.tags.toList()),
|
|
||||||
notAssignedSelectable: false,
|
|
||||||
anyAssignedSelectable: false,
|
|
||||||
excludeAllowed: false,
|
|
||||||
name: fkTags,
|
name: fkTags,
|
||||||
selectableOptions: state.tags,
|
allowOnlySelection: true,
|
||||||
suggestions: (_filteredSuggestions?.tags.toSet() ??
|
allowCreation: true,
|
||||||
{})
|
allowExclude: false,
|
||||||
.difference(state.document.tags.toSet())
|
initialValue: IdsTagsQuery.included(
|
||||||
.isNotEmpty
|
state.document.tags,
|
||||||
? _buildSuggestionsSkeleton<int>(
|
),
|
||||||
suggestions:
|
|
||||||
(_filteredSuggestions?.tags.toSet() ??
|
|
||||||
{}),
|
|
||||||
itemBuilder: (context, itemData) {
|
|
||||||
final tag = state.tags[itemData]!;
|
|
||||||
return ActionChip(
|
|
||||||
label: Text(
|
|
||||||
tag.name,
|
|
||||||
style:
|
|
||||||
TextStyle(color: tag.textColor),
|
|
||||||
),
|
|
||||||
backgroundColor: tag.color,
|
|
||||||
onPressed: () {
|
|
||||||
final currentTags = _formKey
|
|
||||||
.currentState
|
|
||||||
?.fields[fkTags]
|
|
||||||
?.value as TagsQuery;
|
|
||||||
if (currentTags is IdsTagsQuery) {
|
|
||||||
_formKey
|
|
||||||
.currentState?.fields[fkTags]
|
|
||||||
?.didChange(
|
|
||||||
(IdsTagsQuery.fromIds({
|
|
||||||
...currentTags.ids,
|
|
||||||
itemData
|
|
||||||
})));
|
|
||||||
} else {
|
|
||||||
_formKey
|
|
||||||
.currentState?.fields[fkTags]
|
|
||||||
?.didChange(
|
|
||||||
(IdsTagsQuery.fromIds(
|
|
||||||
{itemData})));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
).padded(),
|
).padded(),
|
||||||
|
if (_filteredSuggestions?.tags
|
||||||
|
.toSet()
|
||||||
|
.difference(state.document.tags.toSet())
|
||||||
|
.isNotEmpty ??
|
||||||
|
false)
|
||||||
|
_buildSuggestionsSkeleton<int>(
|
||||||
|
suggestions:
|
||||||
|
(_filteredSuggestions?.tags.toSet() ?? {}),
|
||||||
|
itemBuilder: (context, itemData) {
|
||||||
|
final tag = state.tags[itemData]!;
|
||||||
|
return ActionChip(
|
||||||
|
label: Text(
|
||||||
|
tag.name,
|
||||||
|
style: TextStyle(color: tag.textColor),
|
||||||
|
),
|
||||||
|
backgroundColor: tag.color,
|
||||||
|
onPressed: () {
|
||||||
|
final currentTags = _formKey.currentState
|
||||||
|
?.fields[fkTags]?.value as TagsQuery;
|
||||||
|
if (currentTags is IdsTagsQuery) {
|
||||||
|
_formKey.currentState?.fields[fkTags]
|
||||||
|
?.didChange((IdsTagsQuery.fromIds(
|
||||||
|
{...currentTags.ids, itemData})));
|
||||||
|
} else {
|
||||||
|
_formKey.currentState?.fields[fkTags]
|
||||||
|
?.didChange((IdsTagsQuery.fromIds(
|
||||||
|
{itemData})));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
// Prevent tags from being hidden by fab
|
// Prevent tags from being hidden by fab
|
||||||
const SizedBox(height: 64),
|
const SizedBox(height: 64),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -194,9 +194,12 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
|
|||||||
|
|
||||||
Widget _buildTagsFormField() {
|
Widget _buildTagsFormField() {
|
||||||
return TagQueryFormField(
|
return TagQueryFormField(
|
||||||
|
allowExclude: false,
|
||||||
options: widget.tags,
|
options: widget.tags,
|
||||||
name: DocumentModel.tagsKey,
|
name: DocumentModel.tagsKey,
|
||||||
initialValue: widget.initialFilter.tags,
|
initialValue: widget.initialFilter.tags,
|
||||||
|
allowOnlySelection: false,
|
||||||
|
allowCreation: false,
|
||||||
);
|
);
|
||||||
return TagFormField(
|
return TagFormField(
|
||||||
name: DocumentModel.tagsKey,
|
name: DocumentModel.tagsKey,
|
||||||
|
|||||||
@@ -1,16 +1,321 @@
|
|||||||
import 'package:flutter/src/widgets/framework.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/src/widgets/placeholder.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/features/edit_label/view/impl/add_tag_page.dart';
|
||||||
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
class FullscreenTagsForm extends StatefulWidget {
|
class FullscreenTagsForm extends StatefulWidget {
|
||||||
const FullscreenTagsForm({super.key});
|
final TagsQuery? initialValue;
|
||||||
|
final Map<int, Tag> options;
|
||||||
|
final void Function({TagsQuery? returnValue}) onSubmit;
|
||||||
|
final bool allowOnlySelection;
|
||||||
|
final bool allowCreation;
|
||||||
|
final bool allowExclude;
|
||||||
|
|
||||||
|
const FullscreenTagsForm({
|
||||||
|
super.key,
|
||||||
|
this.initialValue,
|
||||||
|
required this.options,
|
||||||
|
required this.onSubmit,
|
||||||
|
required this.allowOnlySelection,
|
||||||
|
required this.allowCreation,
|
||||||
|
required this.allowExclude,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<FullscreenTagsForm> createState() => _FullscreenTagsFormState();
|
State<FullscreenTagsForm> createState() => _FullscreenTagsFormState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FullscreenTagsFormState extends State<FullscreenTagsForm> {
|
class _FullscreenTagsFormState extends State<FullscreenTagsForm> {
|
||||||
|
late bool _showClearIcon = false;
|
||||||
|
final _textEditingController = TextEditingController();
|
||||||
|
final _focusNode = FocusNode();
|
||||||
|
late List<Tag> _options;
|
||||||
|
|
||||||
|
List<int> _include = [];
|
||||||
|
List<int> _exclude = [];
|
||||||
|
|
||||||
|
bool _anyAssigned = false;
|
||||||
|
bool _notAssigned = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_options = widget.options.values.toList();
|
||||||
|
final value = widget.initialValue;
|
||||||
|
if (value is IdsTagsQuery) {
|
||||||
|
_include = value.includedIds.toList();
|
||||||
|
_exclude = value.excludedIds.toList();
|
||||||
|
} else if (value is AnyAssignedTagsQuery) {
|
||||||
|
_include = value.tagIds.toList();
|
||||||
|
_anyAssigned = true;
|
||||||
|
} else if (value is OnlyNotAssignedTagsQuery) {
|
||||||
|
_notAssigned = true;
|
||||||
|
}
|
||||||
|
_textEditingController.addListener(() => setState(() {
|
||||||
|
_showClearIcon = _textEditingController.text.isNotEmpty;
|
||||||
|
}));
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||||
|
//Delay keyboard popup to ensure open animation is finished before.
|
||||||
|
Future.delayed(
|
||||||
|
const Duration(milliseconds: 200),
|
||||||
|
() => _focusNode.requestFocus(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const Placeholder();
|
final theme = Theme.of(context);
|
||||||
|
return Scaffold(
|
||||||
|
floatingActionButton: widget.allowCreation
|
||||||
|
? FloatingActionButton(
|
||||||
|
onPressed: _onAddTag,
|
||||||
|
child: Icon(Icons.add),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: theme.colorScheme.surface,
|
||||||
|
toolbarHeight: 72,
|
||||||
|
leading: BackButton(
|
||||||
|
color: theme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
title: TextFormField(
|
||||||
|
focusNode: _focusNode,
|
||||||
|
controller: _textEditingController,
|
||||||
|
autofocus: true,
|
||||||
|
style: theme.textTheme.bodyLarge?.apply(
|
||||||
|
color: theme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
hintStyle: theme.textTheme.bodyLarge?.apply(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
icon: const Icon(Icons.label_outline),
|
||||||
|
hintText: S.of(context)!.startTyping,
|
||||||
|
border: InputBorder.none,
|
||||||
|
),
|
||||||
|
textInputAction: TextInputAction.done,
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
if (_showClearIcon)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.clear),
|
||||||
|
onPressed: () {
|
||||||
|
_textEditingController.clear();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: S.of(context)!.done,
|
||||||
|
icon: const Icon(Icons.done),
|
||||||
|
onPressed: () {
|
||||||
|
if (widget.allowOnlySelection) {
|
||||||
|
widget.onSubmit(returnValue: IdsTagsQuery.included(_include));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
late final TagsQuery query;
|
||||||
|
if (_notAssigned) {
|
||||||
|
query = const OnlyNotAssignedTagsQuery();
|
||||||
|
} else if (_anyAssigned) {
|
||||||
|
query = AnyAssignedTagsQuery(tagIds: _include);
|
||||||
|
} else {
|
||||||
|
query = IdsTagsQuery([
|
||||||
|
for (var id in _include) IncludeTagIdQuery(id),
|
||||||
|
for (var id in _exclude) ExcludeTagIdQuery(id),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
widget.onSubmit(returnValue: query);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
bottom: PreferredSize(
|
||||||
|
preferredSize: !widget.allowOnlySelection
|
||||||
|
? const Size.fromHeight(32)
|
||||||
|
: const Size.fromHeight(1),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Divider(color: theme.colorScheme.outline),
|
||||||
|
if (!widget.allowOnlySelection)
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: SegmentedButton<bool>(
|
||||||
|
segments: [
|
||||||
|
ButtonSegment(
|
||||||
|
enabled: isSegmentedButtonEnabled,
|
||||||
|
value: false,
|
||||||
|
label: const Text("All"), //TODO: INTL
|
||||||
|
),
|
||||||
|
ButtonSegment(
|
||||||
|
enabled: isSegmentedButtonEnabled,
|
||||||
|
value: true,
|
||||||
|
label: Text(S.of(context)!.anyAssigned),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
multiSelectionEnabled: false,
|
||||||
|
emptySelectionAllowed: true,
|
||||||
|
onSelectionChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_anyAssigned = value.first;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
selected: {_anyAssigned},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
final options = _buildOptions(_textEditingController.text);
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: options.length,
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
return options.elementAt(index);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onAddTag() async {
|
||||||
|
final createdTag = await Navigator.of(context).push<Tag?>(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => AddTagPage(
|
||||||
|
initialValue: _textEditingController.text,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_textEditingController.clear();
|
||||||
|
if (createdTag != null) {
|
||||||
|
setState(() {
|
||||||
|
_options.add(createdTag);
|
||||||
|
_toggleSelection(createdTag.id!);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isSegmentedButtonEnabled {
|
||||||
|
return _exclude.isEmpty && _include.length > 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildNotAssignedOption() {
|
||||||
|
return ListTile(
|
||||||
|
title: Text(S.of(context)!.notAssigned),
|
||||||
|
trailing: _notAssigned ? const Icon(Icons.done) : null,
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
_notAssigned = !_notAssigned;
|
||||||
|
_include = [];
|
||||||
|
_exclude = [];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Filters the options passed to this widget by the current [query] and
|
||||||
|
/// adds not-/any assigned options
|
||||||
|
///
|
||||||
|
Iterable<Widget> _buildOptions(String query) sync* {
|
||||||
|
final normalizedQuery = query.trim().toLowerCase();
|
||||||
|
|
||||||
|
if (!widget.allowOnlySelection &&
|
||||||
|
S.of(context)!.notAssigned.toLowerCase().contains(normalizedQuery)) {
|
||||||
|
yield _buildNotAssignedOption();
|
||||||
|
}
|
||||||
|
|
||||||
|
var matches = _options
|
||||||
|
.where((e) => e.name.trim().toLowerCase().contains(normalizedQuery));
|
||||||
|
if (matches.isEmpty && widget.allowCreation) {
|
||||||
|
yield Text(S.of(context)!.noItemsFound);
|
||||||
|
yield TextButton(
|
||||||
|
child: Text(S.of(context)!.addTag),
|
||||||
|
onPressed: _onAddTag,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (final tag in matches) {
|
||||||
|
yield SelectableTagWidget(
|
||||||
|
tag: tag,
|
||||||
|
excluded: _exclude.contains(tag.id),
|
||||||
|
selected: _include.contains(tag.id),
|
||||||
|
onTap: () => _toggleSelection(tag.id!),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _toggleSelection(int id) {
|
||||||
|
if (widget.allowOnlySelection || widget.allowExclude) {
|
||||||
|
if (_include.contains(id)) {
|
||||||
|
setState(() => _include.remove(id));
|
||||||
|
} else {
|
||||||
|
setState(() => _include.add(id));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (_include.contains(id)) {
|
||||||
|
setState(() {
|
||||||
|
_notAssigned = false;
|
||||||
|
_anyAssigned = false;
|
||||||
|
_include.remove(id);
|
||||||
|
_exclude.add(id);
|
||||||
|
});
|
||||||
|
} else if (_exclude.contains(id)) {
|
||||||
|
setState(() {
|
||||||
|
_notAssigned = false;
|
||||||
|
_exclude.remove(id);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_notAssigned = false;
|
||||||
|
_include.add(id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SelectableTagWidget extends StatelessWidget {
|
||||||
|
final Tag tag;
|
||||||
|
final bool selected;
|
||||||
|
final bool excluded;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const SelectableTagWidget({
|
||||||
|
super.key,
|
||||||
|
required this.tag,
|
||||||
|
required this.excluded,
|
||||||
|
required this.selected,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListTile(
|
||||||
|
title: Text(tag.name),
|
||||||
|
trailing: excluded
|
||||||
|
? const Icon(Icons.close)
|
||||||
|
: (selected ? const Icon(Icons.done) : null),
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor: tag.color,
|
||||||
|
child: (tag.isInboxTag ?? false)
|
||||||
|
? Icon(
|
||||||
|
Icons.inbox,
|
||||||
|
color: tag.textColor,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
onTap: onTap,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +1,84 @@
|
|||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
|
||||||
|
import 'package:animations/animations.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/workarounds/colored_chip.dart';
|
import 'package:paperless_mobile/core/workarounds/colored_chip.dart';
|
||||||
|
import 'package:paperless_mobile/features/labels/tags/view/widgets/fullscreen_tags_form.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
class TagQueryFormField extends StatelessWidget {
|
class TagQueryFormField extends StatelessWidget {
|
||||||
final String name;
|
final String name;
|
||||||
final Map<int, Tag> options;
|
final Map<int, Tag> options;
|
||||||
final TagsQuery? initialValue;
|
final TagsQuery? initialValue;
|
||||||
|
final bool allowOnlySelection;
|
||||||
|
final bool allowCreation;
|
||||||
|
final bool allowExclude;
|
||||||
|
|
||||||
const TagQueryFormField({
|
const TagQueryFormField({
|
||||||
super.key,
|
super.key,
|
||||||
required this.options,
|
required this.options,
|
||||||
this.initialValue,
|
this.initialValue,
|
||||||
required this.name,
|
required this.name,
|
||||||
|
required this.allowOnlySelection,
|
||||||
|
required this.allowCreation,
|
||||||
|
required this.allowExclude,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
log(initialValue.toString());
|
|
||||||
|
|
||||||
return FormBuilderField<TagsQuery?>(
|
return FormBuilderField<TagsQuery?>(
|
||||||
initialValue: initialValue,
|
initialValue: initialValue,
|
||||||
builder: (field) {
|
builder: (field) {
|
||||||
|
final values = _generateOptions(context, field.value, field).toList();
|
||||||
final isEmpty = (field.value is IdsTagsQuery &&
|
final isEmpty = (field.value is IdsTagsQuery &&
|
||||||
(field.value as IdsTagsQuery).ids.isEmpty) ||
|
(field.value as IdsTagsQuery).ids.isEmpty) ||
|
||||||
field.value == null;
|
field.value == null;
|
||||||
final values = _generateOptions(context, field.value, field).toList();
|
bool anyAssigned = field.value is AnyAssignedTagsQuery;
|
||||||
return GestureDetector(
|
return OpenContainer<TagsQuery>(
|
||||||
onTap: () {
|
middleColor: Theme.of(context).colorScheme.background,
|
||||||
showDialog(
|
closedColor: Theme.of(context).colorScheme.background,
|
||||||
context: context,
|
openColor: Theme.of(context).colorScheme.background,
|
||||||
builder: (context) => Dialog.fullscreen(
|
closedShape: InputBorder.none,
|
||||||
child: Scaffold(
|
openElevation: 0,
|
||||||
appBar: AppBar(
|
closedElevation: 0,
|
||||||
title: Text("Test"),
|
closedBuilder: (context, openForm) => Container(
|
||||||
|
margin: const EdgeInsets.only(top: 6),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: openForm,
|
||||||
|
child: InputDecorator(
|
||||||
|
isEmpty: isEmpty,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
contentPadding: const EdgeInsets.all(12),
|
||||||
|
labelText:
|
||||||
|
'${S.of(context)!.tags}${anyAssigned ? ' (${S.of(context)!.anyAssigned})' : ''}',
|
||||||
|
prefixIcon: const Icon(Icons.label_outline),
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 32,
|
||||||
|
child: ListView.separated(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
separatorBuilder: (context, index) => SizedBox(width: 4),
|
||||||
|
itemBuilder: (context, index) => values[index],
|
||||||
|
itemCount: values.length,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
)),
|
||||||
);
|
openBuilder: (context, closeForm) => FullscreenTagsForm(
|
||||||
},
|
options: options,
|
||||||
child: InputDecorator(
|
onSubmit: closeForm,
|
||||||
isEmpty: isEmpty,
|
initialValue: field.value,
|
||||||
decoration: InputDecoration(
|
allowOnlySelection: allowOnlySelection,
|
||||||
contentPadding: const EdgeInsets.all(12),
|
allowCreation: allowCreation,
|
||||||
labelText: S.of(context)!.tags,
|
allowExclude: allowExclude,
|
||||||
prefixIcon: const Icon(Icons.label_outline),
|
|
||||||
),
|
|
||||||
child: SizedBox(
|
|
||||||
height: 32,
|
|
||||||
child: ListView.separated(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
separatorBuilder: (context, index) => SizedBox(width: 4),
|
|
||||||
itemBuilder: (context, index) => values[index],
|
|
||||||
itemCount: values.length,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
onClosed: (data) {
|
||||||
|
if (data != null) {
|
||||||
|
field.didChange(data);
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
name: name,
|
name: name,
|
||||||
@@ -80,7 +99,9 @@ class TagQueryFormField extends StatelessWidget {
|
|||||||
} else if (query is OnlyNotAssignedTagsQuery) {
|
} else if (query is OnlyNotAssignedTagsQuery) {
|
||||||
yield _buildNotAssignedTagWidget(context, field);
|
yield _buildNotAssignedTagWidget(context, field);
|
||||||
} else if (query is AnyAssignedTagsQuery) {
|
} else if (query is AnyAssignedTagsQuery) {
|
||||||
yield _buildAnyAssignedTagWidget(context, field);
|
for (final e in query.tagIds) {
|
||||||
|
yield _buildAnyAssignedTagWidget(context, e, field, query);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +115,9 @@ class TagQueryFormField extends StatelessWidget {
|
|||||||
final tag = options[e.id]!;
|
final tag = options[e.id]!;
|
||||||
return QueryTagChip(
|
return QueryTagChip(
|
||||||
onDeleted: () => field.didChange(formValue.withIdsRemoved([e.id])),
|
onDeleted: () => field.didChange(formValue.withIdsRemoved([e.id])),
|
||||||
onSelected: () => field.didChange(formValue.withIdQueryToggled(e.id)),
|
onSelected: allowExclude
|
||||||
|
? () => field.didChange(formValue.withIdQueryToggled(e.id))
|
||||||
|
: null,
|
||||||
exclude: e is ExcludeTagIdQuery,
|
exclude: e is ExcludeTagIdQuery,
|
||||||
backgroundColor: tag.color,
|
backgroundColor: tag.color,
|
||||||
foregroundColor: tag.textColor,
|
foregroundColor: tag.textColor,
|
||||||
@@ -116,13 +139,24 @@ class TagQueryFormField extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAnyAssignedTagWidget(
|
Widget _buildAnyAssignedTagWidget(
|
||||||
BuildContext context, FormFieldState<TagsQuery?> field) {
|
BuildContext context,
|
||||||
|
int e,
|
||||||
|
FormFieldState<TagsQuery?> field,
|
||||||
|
AnyAssignedTagsQuery query,
|
||||||
|
) {
|
||||||
return QueryTagChip(
|
return QueryTagChip(
|
||||||
onDeleted: () => field.didChange(const IdsTagsQuery()),
|
onDeleted: () {
|
||||||
|
final updatedQuery = query.withRemoved([e]);
|
||||||
|
if (updatedQuery.tagIds.isEmpty) {
|
||||||
|
field.didChange(const IdsTagsQuery());
|
||||||
|
} else {
|
||||||
|
field.didChange(updatedQuery);
|
||||||
|
}
|
||||||
|
},
|
||||||
exclude: false,
|
exclude: false,
|
||||||
backgroundColor: Colors.grey,
|
backgroundColor: options[e]!.color,
|
||||||
foregroundColor: Colors.black,
|
foregroundColor: options[e]!.textColor,
|
||||||
labelText: S.of(context)!.anyAssigned,
|
labelText: options[e]!.name,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:animations/animations.dart';
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
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';
|
||||||
@@ -11,8 +9,6 @@ import 'package:paperless_mobile/core/repository/label_repository.dart';
|
|||||||
import 'package:paperless_mobile/core/workarounds/colored_chip.dart';
|
import 'package:paperless_mobile/core/workarounds/colored_chip.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/edit_label/view/impl/add_tag_page.dart';
|
import 'package:paperless_mobile/features/edit_label/view/impl/add_tag_page.dart';
|
||||||
import 'package:paperless_mobile/features/labels/tags/view/widgets/fullscreen_tags_form.dart';
|
|
||||||
import 'package:paperless_mobile/features/labels/tags/view/widgets/tag_widget.dart';
|
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
class TagFormField extends StatefulWidget {
|
class TagFormField extends StatefulWidget {
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ class LabelFormField<T extends Label> extends StatelessWidget {
|
|||||||
openElevation: 0,
|
openElevation: 0,
|
||||||
closedElevation: 0,
|
closedElevation: 0,
|
||||||
closedBuilder: (context, openForm) => Container(
|
closedBuilder: (context, openForm) => Container(
|
||||||
margin: const EdgeInsets.only(top: 4),
|
margin: const EdgeInsets.only(top: 6),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
onTap: openForm,
|
onTap: openForm,
|
||||||
|
|||||||
+11
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
import 'tags_query.dart';
|
import 'tags_query.dart';
|
||||||
@@ -22,6 +23,16 @@ class AnyAssignedTagsQuery extends TagsQuery {
|
|||||||
@override
|
@override
|
||||||
List<Object?> get props => [tagIds];
|
List<Object?> get props => [tagIds];
|
||||||
|
|
||||||
|
AnyAssignedTagsQuery withRemoved(Iterable<int> ids) {
|
||||||
|
return AnyAssignedTagsQuery(
|
||||||
|
tagIds: tagIds.toSet().difference(ids.toSet()).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AnyAssignedTagsQuery withAdded(Iterable<int> ids) {
|
||||||
|
return AnyAssignedTagsQuery(tagIds: [...tagIds, ...ids]);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson() => _$AnyAssignedTagsQueryToJson(this);
|
Map<String, dynamic> toJson() => _$AnyAssignedTagsQueryToJson(this);
|
||||||
|
|
||||||
|
|||||||
Regular → Executable
Reference in New Issue
Block a user