From f2fa4e16dec8bc8a3af8c202083743f900ca2367 Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Mon, 10 Apr 2023 01:00:34 +0200 Subject: [PATCH] feat: Implement new tag form field --- ios/Podfile.lock | 16 +- .../view/document_edit_page.dart | 91 +++-- .../widgets/search/document_filter_form.dart | 3 + .../view/widgets/fullscreen_tags_form.dart | 313 +++++++++++++++++- .../view/widgets/tag_query_form_field.dart | 110 +++--- .../tags/view/widgets/tags_form_field.dart | 4 - .../labels/view/widgets/label_form_field.dart | 2 +- .../tags_query/any_assigned_tags_query.dart | 11 + scripts/install_dependencies.sh | 0 9 files changed, 448 insertions(+), 102 deletions(-) mode change 100644 => 100755 scripts/install_dependencies.sh diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 3ae71c9..0c86e09 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -54,6 +54,8 @@ PODS: - FMDB (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): - Flutter - local_auth_ios (0.0.1): @@ -99,16 +101,17 @@ DEPENDENCIES: - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/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`) - local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`) - open_filex (from `.symlinks/plugins/open_filex/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`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/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`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) @@ -142,6 +145,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_native_splash/ios" fluttertoast: :path: ".symlinks/plugins/fluttertoast/ios" + in_app_review: + :path: ".symlinks/plugins/in_app_review/ios" integration_test: :path: ".symlinks/plugins/integration_test/ios" local_auth_ios: @@ -151,7 +156,7 @@ EXTERNAL SOURCES: package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: - :path: ".symlinks/plugins/path_provider_foundation/darwin" + :path: ".symlinks/plugins/path_provider_foundation/ios" pdfx: :path: ".symlinks/plugins/pdfx/ios" permission_handler_apple: @@ -161,7 +166,7 @@ EXTERNAL SOURCES: share_plus: :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: - :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + :path: ".symlinks/plugins/shared_preferences_foundation/ios" sqflite: :path: ".symlinks/plugins/sqflite/ios" url_launcher_ios: @@ -180,6 +185,7 @@ SPEC CHECKSUMS: flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef fluttertoast: eb263d302cc92e04176c053d2385237e9f43fad0 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a + in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5 local_auth_ios: 0d333dde7780f669e66f19d2ff6005f3ea84008d open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4 @@ -195,7 +201,7 @@ SPEC CHECKSUMS: sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 - url_launcher_ios: ae1517e5e344f5544fb090b079e11f399dfbe4d2 + url_launcher_ios: fb12c43172927bb5cf75aeebd073f883801f1993 WeScan: fed582f6c38014d529afb5aa9ffd1bad38fc72b7 PODFILE CHECKSUM: 7daa35cc908d9fba025075df27cb57a1ba1ebf13 diff --git a/lib/features/document_edit/view/document_edit_page.dart b/lib/features/document_edit/view/document_edit_page.dart index b5e1054..905a051 100644 --- a/lib/features/document_edit/view/document_edit_page.dart +++ b/lib/features/document_edit/view/document_edit_page.dart @@ -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_document_type_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/view/widgets/label_form_field.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; @@ -57,7 +58,6 @@ class _DocumentEditPageState extends State { Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - log("Updated state. correspondents have ${state.correspondents.length} items."); return DefaultTabController( length: 2, child: Scaffold( @@ -207,57 +207,48 @@ class _DocumentEditPageState extends State { ], ).padded(), // Tag form field - TagFormField( - initialValue: IdsTagsQuery.included( - state.document.tags.toList()), - notAssignedSelectable: false, - anyAssignedSelectable: false, - excludeAllowed: false, + TagQueryFormField( + options: state.tags, name: fkTags, - selectableOptions: state.tags, - suggestions: (_filteredSuggestions?.tags.toSet() ?? - {}) - .difference(state.document.tags.toSet()) - .isNotEmpty - ? _buildSuggestionsSkeleton( - 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, + allowOnlySelection: true, + allowCreation: true, + allowExclude: false, + initialValue: IdsTagsQuery.included( + state.document.tags, + ), ).padded(), + if (_filteredSuggestions?.tags + .toSet() + .difference(state.document.tags.toSet()) + .isNotEmpty ?? + false) + _buildSuggestionsSkeleton( + 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 const SizedBox(height: 64), ], diff --git a/lib/features/documents/view/widgets/search/document_filter_form.dart b/lib/features/documents/view/widgets/search/document_filter_form.dart index 8b733df..06d5f52 100644 --- a/lib/features/documents/view/widgets/search/document_filter_form.dart +++ b/lib/features/documents/view/widgets/search/document_filter_form.dart @@ -194,9 +194,12 @@ class _DocumentFilterFormState extends State { Widget _buildTagsFormField() { return TagQueryFormField( + allowExclude: false, options: widget.tags, name: DocumentModel.tagsKey, initialValue: widget.initialFilter.tags, + allowOnlySelection: false, + allowCreation: false, ); return TagFormField( name: DocumentModel.tagsKey, diff --git a/lib/features/labels/tags/view/widgets/fullscreen_tags_form.dart b/lib/features/labels/tags/view/widgets/fullscreen_tags_form.dart index afda28a..42a8d37 100644 --- a/lib/features/labels/tags/view/widgets/fullscreen_tags_form.dart +++ b/lib/features/labels/tags/view/widgets/fullscreen_tags_form.dart @@ -1,16 +1,321 @@ -import 'package:flutter/src/widgets/framework.dart'; -import 'package:flutter/src/widgets/placeholder.dart'; +import 'package:collection/collection.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 { - const FullscreenTagsForm({super.key}); + final TagsQuery? initialValue; + final Map 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 State createState() => _FullscreenTagsFormState(); } class _FullscreenTagsFormState extends State { + late bool _showClearIcon = false; + final _textEditingController = TextEditingController(); + final _focusNode = FocusNode(); + late List _options; + + List _include = []; + List _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 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( + 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( + 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 _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, + ); } } diff --git a/lib/features/labels/tags/view/widgets/tag_query_form_field.dart b/lib/features/labels/tags/view/widgets/tag_query_form_field.dart index 831d533..2aee782 100644 --- a/lib/features/labels/tags/view/widgets/tag_query_form_field.dart +++ b/lib/features/labels/tags/view/widgets/tag_query_form_field.dart @@ -1,65 +1,84 @@ import 'dart:developer'; +import 'package:animations/animations.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:paperless_api/paperless_api.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'; class TagQueryFormField extends StatelessWidget { final String name; final Map options; final TagsQuery? initialValue; + final bool allowOnlySelection; + final bool allowCreation; + final bool allowExclude; const TagQueryFormField({ super.key, required this.options, this.initialValue, required this.name, + required this.allowOnlySelection, + required this.allowCreation, + required this.allowExclude, }); @override Widget build(BuildContext context) { - log(initialValue.toString()); - return FormBuilderField( initialValue: initialValue, builder: (field) { + final values = _generateOptions(context, field.value, field).toList(); final isEmpty = (field.value is IdsTagsQuery && (field.value as IdsTagsQuery).ids.isEmpty) || field.value == null; - final values = _generateOptions(context, field.value, field).toList(); - return GestureDetector( - onTap: () { - showDialog( - context: context, - builder: (context) => Dialog.fullscreen( - child: Scaffold( - appBar: AppBar( - title: Text("Test"), + bool anyAssigned = field.value is AnyAssignedTagsQuery; + return OpenContainer( + middleColor: Theme.of(context).colorScheme.background, + closedColor: Theme.of(context).colorScheme.background, + openColor: Theme.of(context).colorScheme.background, + closedShape: InputBorder.none, + openElevation: 0, + closedElevation: 0, + 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, + ), ), ), - ), - ); - }, - child: InputDecorator( - isEmpty: isEmpty, - decoration: InputDecoration( - contentPadding: const EdgeInsets.all(12), - labelText: S.of(context)!.tags, - 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, + onSubmit: closeForm, + initialValue: field.value, + allowOnlySelection: allowOnlySelection, + allowCreation: allowCreation, + allowExclude: allowExclude, ), + onClosed: (data) { + if (data != null) { + field.didChange(data); + } + }, ); }, name: name, @@ -80,7 +99,9 @@ class TagQueryFormField extends StatelessWidget { } else if (query is OnlyNotAssignedTagsQuery) { yield _buildNotAssignedTagWidget(context, field); } 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]!; return QueryTagChip( 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, backgroundColor: tag.color, foregroundColor: tag.textColor, @@ -116,13 +139,24 @@ class TagQueryFormField extends StatelessWidget { } Widget _buildAnyAssignedTagWidget( - BuildContext context, FormFieldState field) { + BuildContext context, + int e, + FormFieldState field, + AnyAssignedTagsQuery query, + ) { 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, - backgroundColor: Colors.grey, - foregroundColor: Colors.black, - labelText: S.of(context)!.anyAssigned, + backgroundColor: options[e]!.color, + foregroundColor: options[e]!.textColor, + labelText: options[e]!.name, ); } } diff --git a/lib/features/labels/tags/view/widgets/tags_form_field.dart b/lib/features/labels/tags/view/widgets/tags_form_field.dart index 33755aa..6c5e71a 100644 --- a/lib/features/labels/tags/view/widgets/tags_form_field.dart +++ b/lib/features/labels/tags/view/widgets/tags_form_field.dart @@ -1,7 +1,5 @@ import 'dart:developer'; -import 'package:animations/animations.dart'; -import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.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/extensions/flutter_extensions.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'; class TagFormField extends StatefulWidget { diff --git a/lib/features/labels/view/widgets/label_form_field.dart b/lib/features/labels/view/widgets/label_form_field.dart index 1c144ab..6dd01ac 100644 --- a/lib/features/labels/view/widgets/label_form_field.dart +++ b/lib/features/labels/view/widgets/label_form_field.dart @@ -81,7 +81,7 @@ class LabelFormField extends StatelessWidget { openElevation: 0, closedElevation: 0, closedBuilder: (context, openForm) => Container( - margin: const EdgeInsets.only(top: 4), + margin: const EdgeInsets.only(top: 6), child: TextField( controller: controller, onTap: openForm, diff --git a/packages/paperless_api/lib/src/models/query_parameters/tags_query/any_assigned_tags_query.dart b/packages/paperless_api/lib/src/models/query_parameters/tags_query/any_assigned_tags_query.dart index 36b1a03..3bdff23 100644 --- a/packages/paperless_api/lib/src/models/query_parameters/tags_query/any_assigned_tags_query.dart +++ b/packages/paperless_api/lib/src/models/query_parameters/tags_query/any_assigned_tags_query.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'package:json_annotation/json_annotation.dart'; import 'tags_query.dart'; @@ -22,6 +23,16 @@ class AnyAssignedTagsQuery extends TagsQuery { @override List get props => [tagIds]; + AnyAssignedTagsQuery withRemoved(Iterable ids) { + return AnyAssignedTagsQuery( + tagIds: tagIds.toSet().difference(ids.toSet()).toList(), + ); + } + + AnyAssignedTagsQuery withAdded(Iterable ids) { + return AnyAssignedTagsQuery(tagIds: [...tagIds, ...ids]); + } + @override Map toJson() => _$AnyAssignedTagsQueryToJson(this); diff --git a/scripts/install_dependencies.sh b/scripts/install_dependencies.sh old mode 100644 new mode 100755