From d621a3bbe7c5e682984f665995213c1271d19eff Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Thu, 13 Apr 2023 22:43:41 +0200 Subject: [PATCH] feat: Finalize bulk edits and reworked form fields --- .../dialog_utils/dialog_confirm_button.dart | 56 +++ lib/core/widgets/error_report_page.dart | 6 +- .../form_builder_color_picker.dart | 27 +- .../fullscreen_selection_form.dart | 58 ++- lib/extensions/dart_extensions.dart | 6 + .../cubit/document_bulk_action_cubit.dart | 4 +- .../view/widgets/bulk_edit_page.dart | 23 - .../widgets/bulk_edit_tags_bottom_sheet.dart | 244 --------- .../confirm_bulk_modify_label_dialog.dart | 38 ++ .../confirm_bulk_modify_tags_dialog.dart | 62 +++ ...t => fullscreen_bulk_edit_label_page.dart} | 50 +- .../fullscreen_bulk_edit_tags_widget.dart | 180 +++++++ .../view/document_edit_page.dart | 4 +- .../view/document_search_page.dart | 4 +- .../view/remove_history_entry_dialog.dart | 11 +- .../document_upload_preparation_page.dart | 11 +- .../delete_document_confirmation_dialog.dart | 19 +- .../widgets/search/document_filter_form.dart | 14 +- .../bulk_delete_confirmation_dialog.dart | 19 +- .../confirm_delete_saved_view_dialog.dart | 16 +- .../document_selection_sliver_app_bar.dart | 81 ++- .../edit_label/view/edit_label_page.dart | 19 +- .../edit_label/view/impl/add_tag_page.dart | 1 + .../edit_label/view/impl/edit_tag_page.dart | 3 +- lib/features/inbox/cubit/inbox_cubit.dart | 4 +- lib/features/inbox/view/pages/inbox_page.dart | 18 +- .../view/widgets/fullscreen_tags_form.dart | 29 +- .../view/widgets/tag_query_form_field.dart | 213 -------- .../tags/view/widgets/tags_form_field.dart | 476 +++++++----------- .../labels/view/pages/labels_page.dart | 2 +- .../view/widgets/fullscreen_label_form.dart | 20 +- .../labels/view/widgets/label_form_field.dart | 2 +- .../view/widgets/radio_settings_dialog.dart | 13 +- lib/l10n/intl_cs.arb | 28 +- lib/l10n/intl_de.arb | 28 +- lib/l10n/intl_en.arb | 28 +- lib/l10n/intl_fr.arb | 28 +- lib/l10n/intl_pl.arb | 28 +- lib/l10n/intl_ru.arb | 28 +- lib/l10n/intl_tr.arb | 28 +- .../lib/src/models/labels/tag_model.dart | 2 +- 41 files changed, 936 insertions(+), 995 deletions(-) create mode 100644 lib/core/widgets/dialog_utils/dialog_confirm_button.dart delete mode 100644 lib/features/document_bulk_action/view/widgets/bulk_edit_page.dart delete mode 100644 lib/features/document_bulk_action/view/widgets/bulk_edit_tags_bottom_sheet.dart create mode 100644 lib/features/document_bulk_action/view/widgets/confirm_bulk_modify_label_dialog.dart create mode 100644 lib/features/document_bulk_action/view/widgets/confirm_bulk_modify_tags_dialog.dart rename lib/features/document_bulk_action/view/widgets/{fullscreen_bulk_edit_label_form_field.dart => fullscreen_bulk_edit_label_page.dart} (74%) create mode 100644 lib/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_tags_widget.dart delete mode 100644 lib/features/labels/tags/view/widgets/tag_query_form_field.dart diff --git a/lib/core/widgets/dialog_utils/dialog_confirm_button.dart b/lib/core/widgets/dialog_utils/dialog_confirm_button.dart new file mode 100644 index 0000000..bb9f02d --- /dev/null +++ b/lib/core/widgets/dialog_utils/dialog_confirm_button.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/framework.dart'; +import 'package:flutter/src/widgets/placeholder.dart'; +import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; + +enum DialogConfirmButtonStyle { + normal, + danger; +} + +class DialogConfirmButton extends StatelessWidget { + final DialogConfirmButtonStyle style; + final String? label; + final T? returnValue; + const DialogConfirmButton({ + super.key, + this.style = DialogConfirmButtonStyle.normal, + this.label, + this.returnValue, + }); + + @override + Widget build(BuildContext context) { + final _normalStyle = ButtonStyle( + backgroundColor: MaterialStatePropertyAll( + Theme.of(context).colorScheme.primaryContainer, + ), + foregroundColor: MaterialStatePropertyAll( + Theme.of(context).colorScheme.onPrimaryContainer, + ), + ); + final _dangerStyle = ButtonStyle( + backgroundColor: MaterialStatePropertyAll( + Theme.of(context).colorScheme.errorContainer, + ), + foregroundColor: MaterialStatePropertyAll( + Theme.of(context).colorScheme.onErrorContainer, + ), + ); + + late final ButtonStyle _style; + switch (style) { + case DialogConfirmButtonStyle.normal: + _style = _normalStyle; + break; + case DialogConfirmButtonStyle.danger: + _style = _dangerStyle; + break; + } + return ElevatedButton( + child: Text(label ?? S.of(context)!.confirm), + style: _style, + onPressed: () => Navigator.of(context).pop(returnValue ?? true), + ); + } +} diff --git a/lib/core/widgets/error_report_page.dart b/lib/core/widgets/error_report_page.dart index c11449c..ab0326b 100644 --- a/lib/core/widgets/error_report_page.dart +++ b/lib/core/widgets/error_report_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:paperless_mobile/core/model/github_error_report.model.dart'; +import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; class ErrorReportPage extends StatefulWidget { @@ -136,10 +137,7 @@ Note: If you have the GitHub Android app installed, the descriptions will not be Navigator.pop(context, true); }, ), - TextButton( - child: const Text('Cancel'), - onPressed: () => Navigator.pop(context, false), - ), + const DialogCancelButton(), ], ), ) ?? diff --git a/lib/core/widgets/form_builder_fields/form_builder_color_picker.dart b/lib/core/widgets/form_builder_fields/form_builder_color_picker.dart index a19c441..893b360 100644 --- a/lib/core/widgets/form_builder_fields/form_builder_color_picker.dart +++ b/lib/core/widgets/form_builder_fields/form_builder_color_picker.dart @@ -4,6 +4,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart'; +import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; extension on Color { @@ -136,11 +138,12 @@ class FormBuilderColorPickerField extends FormBuilderField { : LayoutBuilder( key: ObjectKey(state.value), builder: (context, constraints) { - return Icon( - Icons.circle, - key: ObjectKey(state.value), - size: constraints.minHeight, - color: state.value, + return Padding( + padding: const EdgeInsets.all(8.0), + child: CircleAvatar( + key: ObjectKey(state.value), + backgroundColor: state.value, + ), ); }, ), @@ -218,17 +221,11 @@ class FormBuilderColorPickerFieldState return AlertDialog( // title: null, //const Text('Pick a color!'), - content: SingleChildScrollView( - child: _buildColorPicker(), - ), + content: _buildColorPicker(), actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: Text(materialLocalizations.cancel), - ), - TextButton( - onPressed: () => Navigator.pop(context, true), - child: Text(materialLocalizations.ok), + const DialogCancelButton(), + DialogConfirmButton( + label: S.of(context)!.ok, ), ], ); diff --git a/lib/core/widgets/form_fields/fullscreen_selection_form.dart b/lib/core/widgets/form_fields/fullscreen_selection_form.dart index 7ff9f15..b4d0297 100644 --- a/lib/core/widgets/form_fields/fullscreen_selection_form.dart +++ b/lib/core/widgets/form_fields/fullscreen_selection_form.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; +import 'package:paperless_mobile/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; class FullscreenSelectionForm extends StatefulWidget { final FocusNode? focusNode; @@ -115,31 +117,39 @@ class _FullscreenSelectionFormState extends State { ), ), ), - body: Column( - children: [ - Expanded( - child: ListView.builder( - padding: EdgeInsets.zero, - shrinkWrap: true, - itemCount: widget.selectionCount, - itemBuilder: (BuildContext context, int index) { - final highlight = - AutocompleteHighlightedOption.of(context) == index; - if (highlight) { - SchedulerBinding.instance - .addPostFrameCallback((Duration timeStamp) { - Scrollable.ensureVisible( - context, - alignment: 0, - ); - }); - } - return widget.selectionBuilder(context, index); - }, + body: Builder(builder: (context) { + if (widget.selectionCount == 0) { + return Align( + alignment: Alignment.topCenter, + child: Text(S.of(context)!.noItemsFound).padded(16), + ); + } + return Column( + children: [ + Expanded( + child: ListView.builder( + padding: EdgeInsets.zero, + shrinkWrap: true, + itemCount: widget.selectionCount, + itemBuilder: (BuildContext context, int index) { + final highlight = + AutocompleteHighlightedOption.of(context) == index; + if (highlight) { + SchedulerBinding.instance + .addPostFrameCallback((Duration timeStamp) { + Scrollable.ensureVisible( + context, + alignment: 0, + ); + }); + } + return widget.selectionBuilder(context, index); + }, + ), ), - ), - ], - ), + ], + ); + }), ); } } diff --git a/lib/extensions/dart_extensions.dart b/lib/extensions/dart_extensions.dart index 8e21601..9b23841 100644 --- a/lib/extensions/dart_extensions.dart +++ b/lib/extensions/dart_extensions.dart @@ -36,3 +36,9 @@ extension DateHelpers on DateTime { yesterday.year == year; } } + +extension StringNormalizer on String { + String normalized() { + return trim().toLowerCase(); + } +} diff --git a/lib/features/document_bulk_action/cubit/document_bulk_action_cubit.dart b/lib/features/document_bulk_action/cubit/document_bulk_action_cubit.dart index fb48016..6382c7f 100644 --- a/lib/features/document_bulk_action/cubit/document_bulk_action_cubit.dart +++ b/lib/features/document_bulk_action/cubit/document_bulk_action_cubit.dart @@ -65,7 +65,7 @@ class DocumentBulkActionCubit extends Cubit { final deletedDocuments = state.selection .where((element) => deletedDocumentIds.contains(element.id)); for (final doc in deletedDocuments) { - _notifier.notifyUpdated(doc); + _notifier.notifyDeleted(doc); } } @@ -128,7 +128,7 @@ class DocumentBulkActionCubit extends Cubit { final updatedDocuments = state.selection .where((element) => modifiedDocumentIds.contains(element.id)) .map((doc) => doc.copyWith(tags: [ - ...doc.tags.toSet().difference(addTagIds.toSet()), + ...doc.tags.toSet().difference(removeTagIds.toSet()), ...addTagIds ])); for (final doc in updatedDocuments) { diff --git a/lib/features/document_bulk_action/view/widgets/bulk_edit_page.dart b/lib/features/document_bulk_action/view/widgets/bulk_edit_page.dart deleted file mode 100644 index be63fc4..0000000 --- a/lib/features/document_bulk_action/view/widgets/bulk_edit_page.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:paperless_api/paperless_api.dart'; - -class BulkEditPage extends StatefulWidget { - final bool enableMultipleChoice; - final Map availableOptions; - - const BulkEditPage({ - super.key, - required this.enableMultipleChoice, - required this.availableOptions, - }); - - @override - State createState() => _BulkEditPageState(); -} - -class _BulkEditPageState extends State { - @override - Widget build(BuildContext context) { - return Container(); - } -} diff --git a/lib/features/document_bulk_action/view/widgets/bulk_edit_tags_bottom_sheet.dart b/lib/features/document_bulk_action/view/widgets/bulk_edit_tags_bottom_sheet.dart deleted file mode 100644 index 42ae9aa..0000000 --- a/lib/features/document_bulk_action/view/widgets/bulk_edit_tags_bottom_sheet.dart +++ /dev/null @@ -1,244 +0,0 @@ -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'; -import 'package:flutter_typeahead/flutter_typeahead.dart'; -import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart'; -import 'package:paperless_mobile/extensions/flutter_extensions.dart'; -import 'package:paperless_mobile/features/document_bulk_action/cubit/document_bulk_action_cubit.dart'; -import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; - -class BulkEditTagsBottomSheet extends StatefulWidget { - const BulkEditTagsBottomSheet({super.key}); - - @override - State createState() => - _BulkEditTagsBottomSheetState(); -} - -class _BulkEditTagsBottomSheetState extends State { - final _formKey = GlobalKey(); - final _textEditingController = TextEditingController(); - late Set _sharedTags; - late Set _nonSharedTags; - final Set _sharedTagsToRemove = {}; - final Set _nonSharedTagsToRemove = {}; - final Set _tagsToAdd = {}; - - @override - void initState() { - super.initState(); - final state = context.read().state; - _sharedTags = state.selection - .map((doc) => doc.tags) - .reduce((previousValue, element) => - previousValue.toSet().intersection(element.toSet())) - .toSet(); - print(_sharedTags.map((e) => e).join(", ")); - _nonSharedTags = state.selection - .map((doc) => doc.tags) - .flattened - .toSet() - .difference(_sharedTags) - .toSet(); - print(_nonSharedTags.map((e) => e).join(", ")); - } - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return Padding( - padding: - EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), - child: BlocBuilder( - builder: (context, state) { - print(state); - return Padding( - padding: const EdgeInsets.all(16.0), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - "Bulk modify tags", - style: Theme.of(context).textTheme.titleLarge, - ).paddedOnly(bottom: 24), - TypeAheadFormField( - textFieldConfiguration: TextFieldConfiguration( - controller: _textEditingController, - decoration: const InputDecoration( - labelText: "Tags", - hintText: "Start typing to add tags...", - ), - ), - onSuggestionSelected: (suggestion) { - setState(() { - _tagsToAdd.add(suggestion.id!); - }); - _textEditingController.clear(); - }, - itemBuilder: (context, option) { - return ListTile( - leading: SizedBox( - width: 32, - height: 32, - child: DecoratedBox( - decoration: BoxDecoration( - shape: BoxShape.circle, - color: option.color!, - ), - ), - ), - title: Text(option.name), - ); - }, - suggestionsCallback: (pattern) { - final searchString = pattern.toLowerCase(); - return state.tags.entries - .where( - (tag) => tag.value.name - .toLowerCase() - .contains(searchString), - ) - .map((e) => e.key) - .toSet() - .difference(_sharedTags) - .difference(_nonSharedTags) - .map((e) => state.tags[e]!); - }, - ), - Text("Shared tags"), - Wrap( - children: _sharedTags - .map( - (tag) => RemovableTagWidget( - tag: state.tags[tag]!, - onDeleted: (tag) { - setState(() { - _sharedTagsToRemove.add(tag); - _sharedTags.remove(tag); - }); - }, - ), - ) - .toList(), - ), - const SizedBox(height: 8), - Text("Non-shared tags"), - Wrap( - children: _nonSharedTags - .map( - (tag) => RemovableTagWidget( - tag: state.tags[tag]!, - onDeleted: (tag) { - setState(() { - _nonSharedTagsToRemove.add(tag); - _nonSharedTags.remove(tag); - }); - }, - ), - ) - .toList(), - ), - Text("Remove"), - Wrap( - children: _sharedTagsToRemove.map((tag) { - return RemovableTagWidget( - tag: state.tags[tag]!, - onDeleted: (tag) { - setState(() { - _sharedTagsToRemove.remove(tag); - _sharedTags.add(tag); - }); - }, - ); - }).toList() + - _nonSharedTagsToRemove.map((tag) { - return RemovableTagWidget( - tag: state.tags[tag]!, - onDeleted: (tag) { - setState(() { - _nonSharedTagsToRemove.remove(tag); - _nonSharedTags.add(tag); - }); - }, - ); - }).toList(), - ), - const SizedBox(height: 8), - Text("Add"), - Wrap( - children: _tagsToAdd - .map( - (tag) => RemovableTagWidget( - tag: state.tags[tag]!, - onDeleted: (tag) { - setState(() { - _tagsToAdd.remove(tag); - }); - }), - ) - .toList(), - ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - const DialogCancelButton(), - const SizedBox(width: 16), - FilledButton( - onPressed: () { - if (_formKey.currentState?.saveAndValidate() ?? - false) { - final value = _formKey.currentState - ?.getRawValue('labelFormField') - as IdsTagsQuery; - context - .read() - .bulkModifyTags( - addTagIds: value.includedIds, - ); - } - }, - child: Text(S.of(context)!.apply), - ), - ], - ).padded(8), - ], - ), - ), - ); - }, - ), - ); - }, - ); - } -} - -class RemovableTagWidget extends StatelessWidget { - final Tag tag; - final void Function(int tagId) onDeleted; - const RemovableTagWidget( - {super.key, required this.tag, required this.onDeleted}); - - @override - Widget build(BuildContext context) { - return Chip( - label: Text( - tag.name, - style: TextStyle( - color: tag.textColor, - ), - ), - onDeleted: () => onDeleted(tag.id!), - deleteIcon: Icon(Icons.clear), - backgroundColor: tag.color, - deleteIconColor: tag.textColor, - padding: EdgeInsets.zero, - side: BorderSide.none, - ); - } -} diff --git a/lib/features/document_bulk_action/view/widgets/confirm_bulk_modify_label_dialog.dart b/lib/features/document_bulk_action/view/widgets/confirm_bulk_modify_label_dialog.dart new file mode 100644 index 0000000..0bead34 --- /dev/null +++ b/lib/features/document_bulk_action/view/widgets/confirm_bulk_modify_label_dialog.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart'; +import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart'; +import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; + +class ConfirmBulkModifyLabelDialog extends StatelessWidget { + final int selectionCount; + final String content; + const ConfirmBulkModifyLabelDialog({ + super.key, + required this.selectionCount, + required this.content, + }); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(S.of(context)!.confirmAction), + content: RichText( + text: TextSpan( + text: content, + children: [ + const TextSpan(text: "\n\n"), + TextSpan( + text: S.of(context)!.areYouSureYouWantToContinue, + ), + ], + ), + ), + actions: const [ + DialogCancelButton(), + DialogConfirmButton( + style: DialogConfirmButtonStyle.danger, + ), + ], + ); + } +} diff --git a/lib/features/document_bulk_action/view/widgets/confirm_bulk_modify_tags_dialog.dart b/lib/features/document_bulk_action/view/widgets/confirm_bulk_modify_tags_dialog.dart new file mode 100644 index 0000000..5efa496 --- /dev/null +++ b/lib/features/document_bulk_action/view/widgets/confirm_bulk_modify_tags_dialog.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/framework.dart'; +import 'package:flutter/src/widgets/placeholder.dart'; +import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart'; +import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart'; +import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; + +class ConfirmBulkModifyTagsDialog extends StatelessWidget { + final int selectionCount; + final List removeTags; + final List addTags; + const ConfirmBulkModifyTagsDialog({ + super.key, + required this.removeTags, + required this.addTags, + required this.selectionCount, + }); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(S.of(context)!.confirmAction), + content: RichText( + text: TextSpan( + text: _buildText(context), + children: [ + const TextSpan(text: "\n\n"), + TextSpan( + text: S.of(context)!.areYouSureYouWantToContinue, + ), + ], + ), + ), + actions: const [ + DialogCancelButton(), + DialogConfirmButton( + style: DialogConfirmButtonStyle.danger, + ), + ], + ); + } + + String _buildText(BuildContext context) { + if (removeTags.isNotEmpty && addTags.isNotEmpty) { + return S.of(context)!.bulkEditTagsModifyMessage( + addTags.join(", "), + selectionCount, + removeTags.join(", "), + ); + } else if (removeTags.isNotEmpty) { + return S.of(context)!.bulkEditTagsRemoveMessage( + selectionCount, + removeTags.join(", "), + ); + } else { + return S.of(context)!.bulkEditTagsAddMessage( + selectionCount, + addTags.join(", "), + ); + } + } +} diff --git a/lib/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_label_form_field.dart b/lib/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_label_page.dart similarity index 74% rename from lib/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_label_form_field.dart rename to lib/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_label_page.dart index 5c4c494..1f82d2e 100644 --- a/lib/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_label_form_field.dart +++ b/lib/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_label_page.dart @@ -1,21 +1,23 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart'; +import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart'; import 'package:paperless_mobile/core/widgets/form_fields/fullscreen_selection_form.dart'; -import 'package:paperless_mobile/features/document_bulk_action/cubit/document_bulk_action_cubit.dart'; +import 'package:paperless_mobile/extensions/dart_extensions.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; -class FullscreenBulkEditLabelFormField extends StatefulWidget { +class FullscreenBulkEditLabelPage extends StatefulWidget { final String hintText; final Map options; final List selection; final int? Function(DocumentModel document) labelMapper; final Widget leadingIcon; final void Function(int? id) onSubmit; + final Function(String name) Function(int count) removeStringFnBuilder; + final String Function(String name) Function(int count) assignStringFnBuilder; - FullscreenBulkEditLabelFormField({ + FullscreenBulkEditLabelPage({ super.key, required this.options, required this.selection, @@ -23,20 +25,27 @@ class FullscreenBulkEditLabelFormField extends StatefulWidget { required this.leadingIcon, required this.hintText, required this.onSubmit, + required this.removeStringFnBuilder, + required this.assignStringFnBuilder, }) : assert(selection.isNotEmpty); @override - State createState() => - _FullscreenBulkEditLabelFormFieldState(); + State createState() => + _FullscreenBulkEditLabelPageState(); } -class _FullscreenBulkEditLabelFormFieldState - extends State { +class _FullscreenBulkEditLabelPageState + extends State { + final _controller = TextEditingController(); + LabelSelection? _selection; @override void initState() { super.initState(); + _controller.addListener(() { + setState(() {}); + }); if (_initialValues.length == 1 && _initialValues.first != null) { _selection = LabelSelection(_initialValues.first); } @@ -46,13 +55,19 @@ class _FullscreenBulkEditLabelFormFieldState widget.selection.map(widget.labelMapper).toSet().toList(); Iterable _generateOrderedLabels() sync* { - for (var label in _initialValues) { + final _availableValues = widget.options.values + .where( + (e) => e.name.normalized().contains(_controller.text.normalized())) + .map((e) => e.id!) + .toSet(); + for (var label + in _initialValues.toSet().intersection(_availableValues.toSet())) { if (label != null) { yield label; } } for (final id - in widget.options.keys.whereNot((e) => _initialValues.contains(e))) { + in _availableValues.whereNot((e) => _initialValues.contains(e))) { yield id; } } @@ -64,6 +79,7 @@ class _FullscreenBulkEditLabelFormFieldState (_initialValues.length == 1 && _selection?.label == _initialValues.first); return FullscreenSelectionForm( + controller: _controller, hintText: widget.hintText, leadingIcon: widget.leadingIcon, selectionBuilder: (context, index) => @@ -101,16 +117,10 @@ class _FullscreenBulkEditLabelFormFieldState content: Text( S.of(context)!.areYouSureYouWantToContinue, ), - actions: [ - const DialogCancelButton(), - TextButton( - onPressed: () => Navigator.of(context).pop(true), - child: Text( - S.of(context)!.confirm, - style: TextStyle( - color: Theme.of(context).colorScheme.error, - ), - ), + actions: const [ + DialogCancelButton(), + DialogConfirmButton( + style: DialogConfirmButtonStyle.danger, ), ], ); diff --git a/lib/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_tags_widget.dart b/lib/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_tags_widget.dart new file mode 100644 index 0000000..7b0623f --- /dev/null +++ b/lib/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_tags_widget.dart @@ -0,0 +1,180 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/widgets/form_fields/fullscreen_selection_form.dart'; +import 'package:paperless_mobile/extensions/dart_extensions.dart'; +import 'package:paperless_mobile/features/document_bulk_action/cubit/document_bulk_action_cubit.dart'; +import 'package:paperless_mobile/features/document_bulk_action/view/widgets/confirm_bulk_modify_tags_dialog.dart'; +import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; + +class FullscreenBulkEditTagsWidget extends StatefulWidget { + const FullscreenBulkEditTagsWidget({super.key}); + + @override + State createState() => + _FullscreenBulkEditTagsWidgetState(); +} + +class _FullscreenBulkEditTagsWidgetState + extends State { + final TextEditingController _controller = TextEditingController(); + + /// Tags shared by all documents + late final List _sharedTags; + + /// Tags not assigned to at least one document in the selection + late final List _nonSharedTags; + + List _addTags = []; + List _removeTags = []; + late List _filteredTags; + + @override + void initState() { + super.initState(); + final state = context.read().state; + _sharedTags = state.selection + .map((e) => e.tags) + .map((e) => e.toSet()) + .fold( + state.tags.values.map((e) => e.id!).toSet(), + (previousValue, element) => previousValue.intersection(element), + ) + .toList(); + _nonSharedTags = state.selection + .map((e) => e.tags) + .flattened + .toSet() + .difference(_sharedTags.toSet()) + .toList(); + _filteredTags = state.tags.keys.toList(); + _controller.addListener(() { + setState(() { + _filteredTags = context + .read() + .state + .tags + .values + .where((e) => + e.name.normalized().contains(_controller.text.normalized())) + .map((e) => e.id!) + .toList(); + }); + }); + } + + List get _assignedTags => [..._sharedTags, ..._nonSharedTags]; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return FullscreenSelectionForm( + controller: _controller, + floatingActionButton: _addTags.isNotEmpty || _removeTags.isNotEmpty + ? FloatingActionButton.extended( + label: Text(S.of(context)!.apply), + icon: const Icon(Icons.done), + onPressed: _submit, + ) + : null, + hintText: S.of(context)!.startTyping, + leadingIcon: const Icon(Icons.label_outline), + selectionBuilder: (context, index) { + return _buildTagOption( + _filteredTags[index], + state.tags, + ); + }, + selectionCount: _filteredTags.length, + ); + }, + ); + } + + Widget _buildTagOption(int id, Map options) { + Widget? icon; + if (_sharedTags.contains(id) && !_removeTags.contains(id)) { + // Tag is assigned to all documents and not marked for removal + // => will remain assigned + icon = const Icon(Icons.done); + } else if (_addTags.contains(id)) { + // tag is marked to be added + icon = const Icon(Icons.done); + } else if (_nonSharedTags.contains(id) && !_removeTags.contains(id)) { + // Tag is neither shared among all documents, nor marked to be removed or + // added but assigned to at least one document + icon = const Icon(Icons.remove); + } + + return ListTile( + title: Text(options[id]!.name), + trailing: icon, + leading: CircleAvatar( + backgroundColor: options[id]!.color, + foregroundColor: options[id]!.textColor, + child: options[id]!.isInboxTag ? const Icon(Icons.inbox) : null, + ), + onTap: () { + if (_addTags.contains(id)) { + setState(() { + _addTags.remove(id); + }); + if (_assignedTags.contains(id)) { + setState(() { + _removeTags.add(id); + }); + } + } else if (_removeTags.contains(id)) { + setState(() { + _removeTags.remove(id); + }); + if (!_sharedTags.contains(id)) { + setState(() { + _addTags.add(id); + }); + } + } else { + if (_sharedTags.contains(id)) { + setState(() { + _removeTags.add(id); + }); + } else { + setState(() { + _addTags.add(id); + }); + } + } + }, + ); + } + + void _submit() async { + if (_addTags.isNotEmpty || _removeTags.isNotEmpty) { + final bloc = context.read(); + final addNames = _addTags + .map((value) => "\"${bloc.state.tags[value]!.name}\"") + .toList(); + final removeNames = _removeTags + .map((value) => "\"${bloc.state.tags[value]!.name}\"") + .toList(); + final shouldPerformAction = await showDialog( + context: context, + builder: (context) => ConfirmBulkModifyTagsDialog( + selectionCount: bloc.state.selection.length, + addTags: addNames, + removeTags: removeNames, + ), + ) ?? + false; + if (shouldPerformAction) { + bloc.bulkModifyTags( + removeTagIds: _removeTags, + addTagIds: _addTags, + ); + Navigator.pop(context); + } + } + } +} diff --git a/lib/features/document_edit/view/document_edit_page.dart b/lib/features/document_edit/view/document_edit_page.dart index 905a051..b139786 100644 --- a/lib/features/document_edit/view/document_edit_page.dart +++ b/lib/features/document_edit/view/document_edit_page.dart @@ -15,7 +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/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'; @@ -207,7 +207,7 @@ class _DocumentEditPageState extends State { ], ).padded(), // Tag form field - TagQueryFormField( + TagsFormField( options: state.tags, name: fkTags, allowOnlySelection: true, diff --git a/lib/features/document_search/view/document_search_page.dart b/lib/features/document_search/view/document_search_page.dart index 4f0ad28..292548c 100644 --- a/lib/features/document_search/view/document_search_page.dart +++ b/lib/features/document_search/view/document_search_page.dart @@ -8,7 +8,6 @@ import 'package:paperless_mobile/features/document_search/cubit/document_search_ import 'package:paperless_mobile/features/document_search/view/remove_history_entry_dialog.dart'; import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart'; import 'package:paperless_mobile/features/documents/view/widgets/selection/view_type_selection_widget.dart'; -import 'package:paperless_mobile/features/settings/model/view_type.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/routes/document_details_route.dart'; @@ -70,13 +69,14 @@ class _DocumentSearchPageState extends State { controller: _queryController, onChanged: (query) { _debounceTimer?.cancel(); - _debounceTimer = Timer(const Duration(milliseconds: 700), () { + _debounceTimer = Timer(const Duration(milliseconds: 500), () { context.read().suggest(query); }); }, textInputAction: TextInputAction.search, onSubmitted: (query) { FocusScope.of(context).unfocus(); + _debounceTimer?.cancel(); context.read().search(query); }, ), diff --git a/lib/features/document_search/view/remove_history_entry_dialog.dart b/lib/features/document_search/view/remove_history_entry_dialog.dart index cdfd371..03ba3bc 100644 --- a/lib/features/document_search/view/remove_history_entry_dialog.dart +++ b/lib/features/document_search/view/remove_history_entry_dialog.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart'; +import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; class RemoveHistoryEntryDialog extends StatelessWidget { @@ -13,12 +14,10 @@ class RemoveHistoryEntryDialog extends StatelessWidget { content: Text(S.of(context)!.removeQueryFromSearchHistory), actions: [ const DialogCancelButton(), - TextButton( - child: Text(S.of(context)!.remove), - onPressed: () { - Navigator.pop(context, true); - }, - ), + DialogConfirmButton( + style: DialogConfirmButtonStyle.danger, + label: S.of(context)!.remove, + ) ], ); } diff --git a/lib/features/document_upload/view/document_upload_preparation_page.dart b/lib/features/document_upload/view/document_upload_preparation_page.dart index 7f08530..0ccfd69 100644 --- a/lib/features/document_upload/view/document_upload_preparation_page.dart +++ b/lib/features/document_upload/view/document_upload_preparation_page.dart @@ -222,13 +222,12 @@ class _DocumentUploadPreparationPageState options: state.documentTypes, prefixIcon: const Icon(Icons.description_outlined), ), - TagFormField( + TagsFormField( name: DocumentModel.tagsKey, - notAssignedSelectable: false, - anyAssignedSelectable: false, - excludeAllowed: false, - selectableOptions: state.tags, - //Label: "Tags" + " *", + allowCreation: true, + allowExclude: false, + allowOnlySelection: true, + options: state.tags, ), Text( "* " + S.of(context)!.uploadInferValuesHint, diff --git a/lib/features/documents/view/widgets/delete_document_confirmation_dialog.dart b/lib/features/documents/view/widgets/delete_document_confirmation_dialog.dart index 0405141..ea9c854 100644 --- a/lib/features/documents/view/widgets/delete_document_confirmation_dialog.dart +++ b/lib/features/documents/view/widgets/delete_document_confirmation_dialog.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart'; +import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; class DeleteDocumentConfirmationDialog extends StatelessWidget { @@ -30,19 +32,10 @@ class DeleteDocumentConfirmationDialog extends StatelessWidget { ], ), actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: Text(S.of(context)!.cancel), - ), - TextButton( - style: ButtonStyle( - foregroundColor: - MaterialStateProperty.all(Theme.of(context).colorScheme.error), - ), - onPressed: () { - Navigator.pop(context, true); - }, - child: Text(S.of(context)!.delete), + const DialogCancelButton(), + DialogConfirmButton( + label: S.of(context)!.delete, + style: DialogConfirmButtonStyle.danger, ), ], ); 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 06d5f52..34f037d 100644 --- a/lib/features/documents/view/widgets/search/document_filter_form.dart +++ b/lib/features/documents/view/widgets/search/document_filter_form.dart @@ -4,7 +4,7 @@ import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/widgets/form_builder_fields/extended_date_range_form_field/form_builder_extended_date_range_picker.dart'; import 'package:paperless_mobile/features/labels/cubit/label_cubit.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/generated/l10n/app_localizations.dart'; @@ -193,19 +193,13 @@ class _DocumentFilterFormState extends State { } Widget _buildTagsFormField() { - return TagQueryFormField( - allowExclude: false, - options: widget.tags, + return TagsFormField( name: DocumentModel.tagsKey, initialValue: widget.initialFilter.tags, + options: widget.tags, + allowExclude: false, allowOnlySelection: false, allowCreation: false, ); - return TagFormField( - name: DocumentModel.tagsKey, - initialValue: widget.initialFilter.tags, - allowCreation: false, - selectableOptions: widget.tags, - ); } } diff --git a/lib/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart b/lib/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart index e59f429..d956c2a 100644 --- a/lib/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart +++ b/lib/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart'; +import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart'; import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; @@ -29,19 +31,10 @@ class BulkDeleteConfirmationDialog extends StatelessWidget { ], ), actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: Text(S.of(context)!.cancel), - ), - TextButton( - style: ButtonStyle( - foregroundColor: - MaterialStateProperty.all(Theme.of(context).colorScheme.error), - ), - onPressed: () { - Navigator.pop(context, true); - }, - child: Text(S.of(context)!.delete), + const DialogCancelButton(), + DialogConfirmButton( + label: S.of(context)!.delete, + style: DialogConfirmButtonStyle.danger, ), ], ); diff --git a/lib/features/documents/view/widgets/selection/confirm_delete_saved_view_dialog.dart b/lib/features/documents/view/widgets/selection/confirm_delete_saved_view_dialog.dart index 5721277..7aa31c9 100644 --- a/lib/features/documents/view/widgets/selection/confirm_delete_saved_view_dialog.dart +++ b/lib/features/documents/view/widgets/selection/confirm_delete_saved_view_dialog.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart'; +import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; class ConfirmDeleteSavedViewDialog extends StatelessWidget { @@ -19,16 +21,10 @@ class ConfirmDeleteSavedViewDialog extends StatelessWidget { ), content: Text(S.of(context)!.doYouReallyWantToDeleteThisView), actions: [ - TextButton( - child: Text(S.of(context)!.cancel), - onPressed: () => Navigator.pop(context, false), - ), - TextButton( - child: Text( - S.of(context)!.delete, - style: TextStyle(color: Theme.of(context).colorScheme.error), - ), - onPressed: () => Navigator.pop(context, true), + const DialogCancelButton(), + DialogConfirmButton( + label: S.of(context)!.delete, + style: DialogConfirmButtonStyle.danger, ), ], ); diff --git a/lib/features/documents/view/widgets/selection/document_selection_sliver_app_bar.dart b/lib/features/documents/view/widgets/selection/document_selection_sliver_app_bar.dart index 01cd603..c6c1d86 100644 --- a/lib/features/documents/view/widgets/selection/document_selection_sliver_app_bar.dart +++ b/lib/features/documents/view/widgets/selection/document_selection_sliver_app_bar.dart @@ -1,13 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/document_bulk_action/cubit/document_bulk_action_cubit.dart'; -import 'package:paperless_mobile/features/document_bulk_action/view/widgets/bulk_edit_label_bottom_sheet.dart'; -import 'package:paperless_mobile/features/document_bulk_action/view/widgets/bulk_edit_tags_bottom_sheet.dart'; -import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_label_form_field.dart'; +import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_label_page.dart'; +import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_tags_widget.dart'; import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; @@ -82,7 +79,7 @@ class DocumentSelectionSliverAppBar extends StatelessWidget { child: BlocBuilder( builder: (context, state) { - return FullscreenBulkEditLabelFormField( + return FullscreenBulkEditLabelPage( options: state.correspondents, selection: state.selection, labelMapper: (document) => document.correspondent, @@ -91,6 +88,22 @@ class DocumentSelectionSliverAppBar extends StatelessWidget { onSubmit: context .read() .bulkModifyCorrespondent, + assignStringFnBuilder: (int count) { + return (String name) => S + .of(context)! + .bulkEditCorrespondentAssignMessage( + name, + count, + ); + }, + removeStringFnBuilder: (int count) { + return (String name) => S + .of(context)! + .bulkEditCorrespondentRemoveMessage( + name, + count, + ); + }, ); }, ), @@ -115,7 +128,7 @@ class DocumentSelectionSliverAppBar extends StatelessWidget { child: BlocBuilder( builder: (context, state) { - return FullscreenBulkEditLabelFormField( + return FullscreenBulkEditLabelPage( options: state.documentTypes, selection: state.selection, labelMapper: (document) => document.documentType, @@ -125,6 +138,22 @@ class DocumentSelectionSliverAppBar extends StatelessWidget { onSubmit: context .read() .bulkModifyDocumentType, + assignStringFnBuilder: (int count) { + return (String name) => S + .of(context)! + .bulkEditDocumentTypeAssignMessage( + count, + name, + ); + }, + removeStringFnBuilder: (int count) { + return (String name) => S + .of(context)! + .bulkEditDocumentTypeRemoveMessage( + count, + name, + ); + }, ); }, ), @@ -149,7 +178,7 @@ class DocumentSelectionSliverAppBar extends StatelessWidget { child: BlocBuilder( builder: (context, state) { - return FullscreenBulkEditLabelFormField( + return FullscreenBulkEditLabelPage( options: state.storagePaths, selection: state.selection, labelMapper: (document) => document.storagePath, @@ -158,6 +187,22 @@ class DocumentSelectionSliverAppBar extends StatelessWidget { onSubmit: context .read() .bulkModifyStoragePath, + assignStringFnBuilder: (int count) { + return (String name) => S + .of(context)! + .bulkEditStoragePathAssignMessage( + count, + name, + ); + }, + removeStringFnBuilder: (int count) { + return (String name) => S + .of(context)! + .bulkEditStoragePathRemoveMessage( + count, + name, + ); + }, ); }, ), @@ -179,17 +224,9 @@ class DocumentSelectionSliverAppBar extends StatelessWidget { label: Text(S.of(context)!.tags), avatar: const Icon(Icons.edit), onPressed: () { - showModalBottomSheet( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - ), - ), - isScrollControlled: true, - context: context, - builder: (_) { - return BlocProvider( + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => BlocProvider( create: (context) => DocumentBulkActionCubit( context.read(), context.read(), @@ -197,10 +234,10 @@ class DocumentSelectionSliverAppBar extends StatelessWidget { selection: state.selection, ), child: Builder(builder: (context) { - return const BulkEditTagsBottomSheet(); + return const FullscreenBulkEditTagsWidget(); }), - ); - }, + ), + ), ); }, ); diff --git a/lib/features/edit_label/view/edit_label_page.dart b/lib/features/edit_label/view/edit_label_page.dart index 88536e7..e090727 100644 --- a/lib/features/edit_label/view/edit_label_page.dart +++ b/lib/features/edit_label/view/edit_label_page.dart @@ -4,6 +4,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart'; +import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart'; import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart'; import 'package:paperless_mobile/features/edit_label/view/label_form.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; @@ -95,19 +97,10 @@ class EditLabelForm extends StatelessWidget { S.of(context)!.deleteLabelWarningText, ), actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: Text(S.of(context)!.cancel), - ), - TextButton( - onPressed: () { - Navigator.pop(context, true); - }, - child: Text( - S.of(context)!.delete, - style: - TextStyle(color: Theme.of(context).colorScheme.error), - ), + const DialogCancelButton(), + DialogConfirmButton( + label: S.of(context)!.delete, + style: DialogConfirmButtonStyle.danger, ), ], ), diff --git a/lib/features/edit_label/view/impl/add_tag_page.dart b/lib/features/edit_label/view/impl/add_tag_page.dart index 9c26d10..3a13501 100644 --- a/lib/features/edit_label/view/impl/add_tag_page.dart +++ b/lib/features/edit_label/view/impl/add_tag_page.dart @@ -35,6 +35,7 @@ class AddTagPage extends StatelessWidget { colorPickerType: ColorPickerType.materialPicker, initialValue: Color((Random().nextDouble() * 0xFFFFFF).toInt()) .withOpacity(1.0), + readOnly: true, ), FormBuilderCheckbox( name: Tag.isInboxTagKey, diff --git a/lib/features/edit_label/view/impl/edit_tag_page.dart b/lib/features/edit_label/view/impl/edit_tag_page.dart index 6abd5c9..9c2d79e 100644 --- a/lib/features/edit_label/view/impl/edit_tag_page.dart +++ b/lib/features/edit_label/view/impl/edit_tag_page.dart @@ -32,7 +32,8 @@ class EditTagPage extends StatelessWidget { decoration: InputDecoration( label: Text(S.of(context)!.color), ), - colorPickerType: ColorPickerType.blockPicker, + colorPickerType: ColorPickerType.materialPicker, + readOnly: true, ), FormBuilderCheckbox( initialValue: tag.isInboxTag, diff --git a/lib/features/inbox/cubit/inbox_cubit.dart b/lib/features/inbox/cubit/inbox_cubit.dart index 82492e5..69efa7a 100644 --- a/lib/features/inbox/cubit/inbox_cubit.dart +++ b/lib/features/inbox/cubit/inbox_cubit.dart @@ -78,7 +78,7 @@ class InboxCubit extends HydratedCubit Future loadInbox() async { debugPrint("Initializing inbox..."); final inboxTags = await _labelRepository.findAllTags().then( - (tags) => tags.where((t) => t.isInboxTag ?? false).map((t) => t.id!), + (tags) => tags.where((t) => t.isInboxTag).map((t) => t.id!), ); if (inboxTags.isEmpty) { @@ -106,7 +106,7 @@ class InboxCubit extends HydratedCubit Future reloadInbox() async { emit(state.copyWith(hasLoaded: false, isLoading: true)); final inboxTags = await _labelRepository.findAllTags().then( - (tags) => tags.where((t) => t.isInboxTag ?? false).map((t) => t.id!), + (tags) => tags.where((t) => t.isInboxTag).map((t) => t.id!), ); if (inboxTags.isEmpty) { diff --git a/lib/features/inbox/view/pages/inbox_page.dart b/lib/features/inbox/view/pages/inbox_page.dart index 9c16095..0bed511 100644 --- a/lib/features/inbox/view/pages/inbox_page.dart +++ b/lib/features/inbox/view/pages/inbox_page.dart @@ -3,6 +3,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart'; +import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart'; import 'package:paperless_mobile/core/widgets/hint_card.dart'; import 'package:paperless_mobile/extensions/dart_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; @@ -197,18 +199,10 @@ class _InboxPageState extends State S.of(context)!.areYouSureYouWantToMarkAllDocumentsAsSeen, ), actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: Text( - S.of(context)!.cancel, - style: TextStyle( - color: Theme.of(context).colorScheme.error, - ), - ), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(true), - child: Text(S.of(context)!.ok), + const DialogCancelButton(), + DialogConfirmButton( + label: S.of(context)!.markAsSeen, + style: DialogConfirmButtonStyle.danger, ), ], ), 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 42a8d37..2305558 100644 --- a/lib/features/labels/tags/view/widgets/fullscreen_tags_form.dart +++ b/lib/features/labels/tags/view/widgets/fullscreen_tags_form.dart @@ -1,6 +1,4 @@ -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'; @@ -12,7 +10,7 @@ class FullscreenTagsForm extends StatefulWidget { final bool allowOnlySelection; final bool allowCreation; final bool allowExclude; - + final bool autofocus; const FullscreenTagsForm({ super.key, this.initialValue, @@ -21,6 +19,7 @@ class FullscreenTagsForm extends StatefulWidget { required this.allowOnlySelection, required this.allowCreation, required this.allowExclude, + this.autofocus = true, }); @override @@ -56,13 +55,15 @@ class _FullscreenTagsFormState extends State { _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(), - ); - }); + if (widget.autofocus) { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + //Delay keyboard popup to ensure open animation is finished before. + Future.delayed( + const Duration(milliseconds: 200), + () => _focusNode.requestFocus(), + ); + }); + } } @override @@ -72,7 +73,7 @@ class _FullscreenTagsFormState extends State { floatingActionButton: widget.allowCreation ? FloatingActionButton( onPressed: _onAddTag, - child: Icon(Icons.add), + child: const Icon(Icons.add), ) : null, appBar: AppBar( @@ -145,12 +146,12 @@ class _FullscreenTagsFormState extends State { ButtonSegment( enabled: isSegmentedButtonEnabled, value: false, - label: const Text("All"), //TODO: INTL + label: Text(S.of(context)!.allTags), ), ButtonSegment( enabled: isSegmentedButtonEnabled, value: true, - label: Text(S.of(context)!.anyAssigned), + label: Text(S.of(context)!.anyTag), ), ], multiSelectionEnabled: false, @@ -308,7 +309,7 @@ class SelectableTagWidget extends StatelessWidget { : (selected ? const Icon(Icons.done) : null), leading: CircleAvatar( backgroundColor: tag.color, - child: (tag.isInboxTag ?? false) + child: (tag.isInboxTag) ? Icon( Icons.inbox, color: tag.textColor, 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 deleted file mode 100644 index 2aee782..0000000 --- a/lib/features/labels/tags/view/widgets/tag_query_form_field.dart +++ /dev/null @@ -1,213 +0,0 @@ -import 'dart:developer'; - -import 'package:animations/animations.dart'; -import 'package:flutter/material.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) { - 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; - 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, - ), - ), - ), - )), - 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, - ); - } - - Iterable _generateOptions( - BuildContext context, - TagsQuery? query, - FormFieldState field, - ) sync* { - if (query == null) { - yield Container(); - } else if (query is IdsTagsQuery) { - for (final e in query.queries) { - yield _buildTagIdQueryWidget(context, e, field); - } - } else if (query is OnlyNotAssignedTagsQuery) { - yield _buildNotAssignedTagWidget(context, field); - } else if (query is AnyAssignedTagsQuery) { - for (final e in query.tagIds) { - yield _buildAnyAssignedTagWidget(context, e, field, query); - } - } - } - - Widget _buildTagIdQueryWidget( - BuildContext context, - TagIdQuery e, - FormFieldState field, - ) { - assert(field.value is IdsTagsQuery); - final formValue = field.value as IdsTagsQuery; - final tag = options[e.id]!; - return QueryTagChip( - onDeleted: () => field.didChange(formValue.withIdsRemoved([e.id])), - onSelected: allowExclude - ? () => field.didChange(formValue.withIdQueryToggled(e.id)) - : null, - exclude: e is ExcludeTagIdQuery, - backgroundColor: tag.color, - foregroundColor: tag.textColor, - labelText: tag.name, - ); - } - - Widget _buildNotAssignedTagWidget( - BuildContext context, - FormFieldState field, - ) { - return QueryTagChip( - onDeleted: () => field.didChange(null), - exclude: false, - backgroundColor: Colors.grey, - foregroundColor: Colors.black, - labelText: S.of(context)!.notAssigned, - ); - } - - Widget _buildAnyAssignedTagWidget( - BuildContext context, - int e, - FormFieldState field, - AnyAssignedTagsQuery query, - ) { - return QueryTagChip( - onDeleted: () { - final updatedQuery = query.withRemoved([e]); - if (updatedQuery.tagIds.isEmpty) { - field.didChange(const IdsTagsQuery()); - } else { - field.didChange(updatedQuery); - } - }, - exclude: false, - backgroundColor: options[e]!.color, - foregroundColor: options[e]!.textColor, - labelText: options[e]!.name, - ); - } -} - -typedef TagQueryCallback = void Function(Tag tag); - -class QueryTagChip extends StatelessWidget { - final VoidCallback onDeleted; - final VoidCallback? onSelected; - final bool exclude; - final Color? backgroundColor; - final Color? foregroundColor; - final String labelText; - - const QueryTagChip({ - super.key, - required this.onDeleted, - this.onSelected, - required this.exclude, - this.backgroundColor, - this.foregroundColor, - required this.labelText, - }); - - @override - Widget build(BuildContext context) { - return ColoredChipWrapper( - child: InputChip( - labelPadding: const EdgeInsets.symmetric( - horizontal: 4, - vertical: 2, - ), - padding: const EdgeInsets.all(4), - selectedColor: backgroundColor, - visualDensity: const VisualDensity(vertical: -2), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - label: Text( - labelText, - style: TextStyle( - color: foregroundColor, - decorationColor: foregroundColor, - decoration: exclude ? TextDecoration.lineThrough : null, - ), - ), - onDeleted: onDeleted, - onPressed: onSelected, - deleteIconColor: foregroundColor, - checkmarkColor: foregroundColor, - backgroundColor: backgroundColor, - side: BorderSide.none, - ), - ); - } -} 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 6c5e71a..f70fd69 100644 --- a/lib/features/labels/tags/view/widgets/tags_form_field.dart +++ b/lib/features/labels/tags/view/widgets/tags_form_field.dart @@ -1,335 +1,213 @@ import 'dart:developer'; +import 'package:animations/animations.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; -import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:paperless_api/paperless_api.dart'; -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/generated/l10n/app_localizations.dart'; -class TagFormField extends StatefulWidget { - final TagsQuery? initialValue; +class TagsFormField extends StatelessWidget { final String name; + final Map options; + final TagsQuery? initialValue; + final bool allowOnlySelection; final bool allowCreation; - final bool notAssignedSelectable; - final bool anyAssignedSelectable; - final bool excludeAllowed; - final Map selectableOptions; - final Widget? suggestions; - final String? labelText; - final String? hintText; + final bool allowExclude; - const TagFormField({ + const TagsFormField({ super.key, - required this.name, + required this.options, this.initialValue, - this.allowCreation = true, - this.notAssignedSelectable = true, - this.anyAssignedSelectable = true, - this.excludeAllowed = true, - required this.selectableOptions, - this.suggestions, - this.labelText, - this.hintText, + required this.name, + required this.allowOnlySelection, + required this.allowCreation, + required this.allowExclude, }); @override - State createState() => _TagFormFieldState(); + Widget build(BuildContext context) { + 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; + 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) => + const 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, + ); + } + + Iterable _generateOptions( + BuildContext context, + TagsQuery? query, + FormFieldState field, + ) sync* { + if (query == null) { + yield Container(); + } else if (query is IdsTagsQuery) { + for (final e in query.queries) { + yield _buildTagIdQueryWidget(context, e, field); + } + } else if (query is OnlyNotAssignedTagsQuery) { + yield _buildNotAssignedTagWidget(context, field); + } else if (query is AnyAssignedTagsQuery) { + for (final e in query.tagIds) { + yield _buildAnyAssignedTagWidget(context, e, field, query); + } + } + } + + Widget _buildTagIdQueryWidget( + BuildContext context, + TagIdQuery e, + FormFieldState field, + ) { + assert(field.value is IdsTagsQuery); + final formValue = field.value as IdsTagsQuery; + final tag = options[e.id]!; + return QueryTagChip( + onDeleted: () => field.didChange(formValue.withIdsRemoved([e.id])), + onSelected: allowExclude + ? () => field.didChange(formValue.withIdQueryToggled(e.id)) + : null, + exclude: e is ExcludeTagIdQuery, + backgroundColor: tag.color, + foregroundColor: tag.textColor, + labelText: tag.name, + ); + } + + Widget _buildNotAssignedTagWidget( + BuildContext context, + FormFieldState field, + ) { + return QueryTagChip( + onDeleted: () => field.didChange(null), + exclude: false, + backgroundColor: Colors.grey, + foregroundColor: Colors.black, + labelText: S.of(context)!.notAssigned, + ); + } + + Widget _buildAnyAssignedTagWidget( + BuildContext context, + int e, + FormFieldState field, + AnyAssignedTagsQuery query, + ) { + return QueryTagChip( + onDeleted: () { + final updatedQuery = query.withRemoved([e]); + if (updatedQuery.tagIds.isEmpty) { + field.didChange(const IdsTagsQuery()); + } else { + field.didChange(updatedQuery); + } + }, + exclude: false, + backgroundColor: options[e]!.color, + foregroundColor: options[e]!.textColor, + labelText: options[e]!.name, + ); + } } -class _TagFormFieldState extends State { - static const _onlyNotAssignedId = -1; - static const _anyAssignedId = -2; +typedef TagQueryCallback = void Function(Tag tag); - late final TextEditingController _textEditingController; - bool _showCreationSuffixIcon = false; - bool _showClearSuffixIcon = false; +class QueryTagChip extends StatelessWidget { + final VoidCallback onDeleted; + final VoidCallback? onSelected; + final bool exclude; + final Color? backgroundColor; + final Color? foregroundColor; + final String labelText; - @override - void initState() { - super.initState(); - _textEditingController = TextEditingController() - ..addListener(() { - setState(() { - _showCreationSuffixIcon = widget.selectableOptions.values.where( - (item) { - log(item.name - .toLowerCase() - .startsWith( - _textEditingController.text.toLowerCase(), - ) - .toString()); - return item.name.toLowerCase().startsWith( - _textEditingController.text.toLowerCase(), - ); - }, - ).isEmpty; - }); - setState( - () => _showClearSuffixIcon = _textEditingController.text.isNotEmpty, - ); - }); - } + const QueryTagChip({ + super.key, + required this.onDeleted, + this.onSelected, + required this.exclude, + this.backgroundColor, + this.foregroundColor, + required this.labelText, + }); @override Widget build(BuildContext context) { - final isEnabled = widget.selectableOptions.values.fold( - false, - (previousValue, element) => - previousValue || (element.documentCount ?? 0) > 0) || - widget.allowCreation; - - return FormBuilderField( - enabled: isEnabled, - builder: (field) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TypeAheadField( - textFieldConfiguration: TextFieldConfiguration( - enabled: isEnabled, - decoration: InputDecoration( - prefixIcon: const Icon( - Icons.label_outline, - ), - suffixIcon: _buildSuffixIcon(context, field), - labelText: widget.labelText ?? S.of(context)!.tags, - hintText: widget.hintText ?? S.of(context)!.filterTags, - ), - controller: _textEditingController, - ), - suggestionsBoxDecoration: SuggestionsBoxDecoration( - elevation: 4.0, - shadowColor: Theme.of(context).colorScheme.primary, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - suggestionsCallback: (query) { - final suggestions = widget.selectableOptions.entries - .where( - (entry) => entry.value.name - .toLowerCase() - .startsWith(query.toLowerCase()), - ) - .where((entry) => - widget.allowCreation || - (entry.value.documentCount ?? 0) > 0) - .map((entry) => entry.key) - .toList(); - if (field.value is IdsTagsQuery) { - suggestions.removeWhere((element) => - (field.value as IdsTagsQuery).ids.contains(element)); - } - if (widget.notAssignedSelectable && - field.value is! OnlyNotAssignedTagsQuery) { - suggestions.insert(0, _onlyNotAssignedId); - } - if (widget.anyAssignedSelectable && - field.value is! AnyAssignedTagsQuery) { - suggestions.insert(0, _anyAssignedId); - } - return suggestions; - }, - getImmediateSuggestions: true, - animationStart: 1, - itemBuilder: (context, data) { - late String? title; - switch (data) { - case _onlyNotAssignedId: - title = S.of(context)!.notAssigned; - break; - case _anyAssignedId: - title = S.of(context)!.anyAssigned; - break; - default: - title = widget.selectableOptions[data]?.name; - } - - final tag = widget.selectableOptions[data]; - return ListTile( - dense: true, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - style: ListTileStyle.list, - leading: data != _onlyNotAssignedId && data != _anyAssignedId - ? Icon( - Icons.circle, - color: tag?.color, - ) - : null, - title: Text( - title ?? '', - style: TextStyle( - color: Theme.of(context).colorScheme.onBackground), - ), - ); - }, - onSuggestionSelected: (id) { - if (id == _onlyNotAssignedId) { - //Not assigned tag - field.didChange(const OnlyNotAssignedTagsQuery()); - return; - } else if (id == _anyAssignedId) { - field.didChange(const AnyAssignedTagsQuery()); - } else { - final tagsQuery = field.value is IdsTagsQuery - ? field.value as IdsTagsQuery - : const IdsTagsQuery(); - field.didChange( - tagsQuery.withIdQueriesAdded([IncludeTagIdQuery(id)])); - } - _textEditingController.clear(); - }, - direction: AxisDirection.up, - ), - if (field.value is OnlyNotAssignedTagsQuery) ...[ - _buildNotAssignedTag(field).padded() - ] else if (field.value is AnyAssignedTagsQuery) ...[ - _buildAnyAssignedTag(field).padded() - ] else ...[ - if (widget.suggestions != null) widget.suggestions!, - // field.value is IdsTagsQuery - Wrap( - alignment: WrapAlignment.start, - runAlignment: WrapAlignment.start, - spacing: 4.0, - runSpacing: 4.0, - children: ((field.value as IdsTagsQuery).queries) - .map( - (query) => _buildTag( - field, - query, - widget.selectableOptions[query.id], - ), - ) - .toList(), - ).padded(), - ] - ], - ); - }, - initialValue: widget.initialValue ?? const IdsTagsQuery(), - name: widget.name, - ); - } - - Widget? _buildSuffixIcon( - BuildContext context, - FormFieldState 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 field) async { - final Tag? tag = await Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => RepositoryProvider.value( - value: context.read(), - child: AddTagPage(initialValue: _textEditingController.text), - ), - ), - ); - if (tag != null) { - final tagsQuery = field.value is IdsTagsQuery - ? field.value as IdsTagsQuery - : const IdsTagsQuery(); - field.didChange( - tagsQuery.withIdQueriesAdded([IncludeTagIdQuery(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 field) { return ColoredChipWrapper( child: InputChip( - labelPadding: const EdgeInsets.symmetric(horizontal: 2), - padding: const EdgeInsets.all(4), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - side: BorderSide.none, - label: Text( - S.of(context)!.notAssigned, + labelPadding: const EdgeInsets.symmetric( + horizontal: 4, + vertical: 2, ), - backgroundColor: - Theme.of(context).colorScheme.onSurface.withOpacity(0.12), - onDeleted: () => field.didChange(const IdsTagsQuery()), - ), - ); - } - - Widget _buildTag( - FormFieldState field, - TagIdQuery query, - Tag? tag, - ) { - final currentQuery = field.value as IdsTagsQuery; - final isIncludedTag = currentQuery.includedIds.contains(query.id); - if (tag == null) { - return Container(); - } - return ColoredChipWrapper( - child: InputChip( - labelPadding: const EdgeInsets.symmetric(horizontal: 2), padding: const EdgeInsets.all(4), + selectedColor: backgroundColor, + visualDensity: const VisualDensity(vertical: -2), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - side: BorderSide.none, label: Text( - tag.name, + labelText, style: TextStyle( - color: tag.textColor, - decorationColor: tag.textColor, - decoration: !isIncludedTag ? TextDecoration.lineThrough : null, - decorationThickness: 2.0, + color: foregroundColor, + decorationColor: foregroundColor, + decoration: exclude ? TextDecoration.lineThrough : null, ), ), - onPressed: widget.excludeAllowed - ? () => field.didChange(currentQuery.withIdQueryToggled(tag.id!)) - : null, - backgroundColor: tag.color, - deleteIconColor: tag.textColor, - onDeleted: () => field.didChange( - (field.value as IdsTagsQuery).withIdsRemoved([tag.id!]), - ), - ), - ); - } - - Widget _buildAnyAssignedTag(FormFieldState field) { - return ColoredChipWrapper( - child: InputChip( - labelPadding: const EdgeInsets.symmetric(horizontal: 2), - padding: const EdgeInsets.all(4), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + onDeleted: onDeleted, + onPressed: onSelected, + deleteIconColor: foregroundColor, + checkmarkColor: foregroundColor, + backgroundColor: backgroundColor, side: BorderSide.none, - label: Text(S.of(context)!.anyAssigned), - backgroundColor: - Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.12), - onDeleted: () => field.didChange(const IdsTagsQuery()), ), ); } diff --git a/lib/features/labels/view/pages/labels_page.dart b/lib/features/labels/view/pages/labels_page.dart index b65435a..769b20a 100644 --- a/lib/features/labels/view/pages/labels_page.dart +++ b/lib/features/labels/view/pages/labels_page.dart @@ -220,7 +220,7 @@ class _LabelsPageState extends State onEdit: _openEditTagPage, leadingBuilder: (t) => CircleAvatar( backgroundColor: t.color, - child: t.isInboxTag ?? false + child: t.isInboxTag ? Icon( Icons.inbox, color: t.textColor, diff --git a/lib/features/labels/view/widgets/fullscreen_label_form.dart b/lib/features/labels/view/widgets/fullscreen_label_form.dart index d40fa00..fe55559 100644 --- a/lib/features/labels/view/widgets/fullscreen_label_form.dart +++ b/lib/features/labels/view/widgets/fullscreen_label_form.dart @@ -14,6 +14,7 @@ class FullscreenLabelForm extends StatefulWidget { final void Function({IdQueryParameter returnValue}) onSubmit; final Widget leadingIcon; final String? addNewLabelText; + final bool autofocus; FullscreenLabelForm({ super.key, @@ -25,6 +26,7 @@ class FullscreenLabelForm extends StatefulWidget { required this.onSubmit, required this.leadingIcon, this.addNewLabelText, + this.autofocus = true, }) : assert( !(initialValue?.onlyAssigned ?? false) || showAnyAssignedOption, ), @@ -49,13 +51,15 @@ class _FullscreenLabelFormState _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(), - ); - }); + if (widget.autofocus) { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + //Delay keyboard popup to ensure open animation is finished before. + Future.delayed( + const Duration(milliseconds: 200), + () => _focusNode.requestFocus(), + ); + }); + } } @override @@ -215,7 +219,7 @@ class _FullscreenLabelFormState String? _buildHintText() { if (widget.initialValue?.isSet ?? false) { - return widget.options[widget.initialValue!.id]?.name ?? 'undefined'; + return widget.options[widget.initialValue!.id]!.name; } if (widget.initialValue?.onlyNotAssigned ?? false) { return S.of(context)!.notAssigned; diff --git a/lib/features/labels/view/widgets/label_form_field.dart b/lib/features/labels/view/widgets/label_form_field.dart index 6dd01ac..4851fe2 100644 --- a/lib/features/labels/view/widgets/label_form_field.dart +++ b/lib/features/labels/view/widgets/label_form_field.dart @@ -46,7 +46,7 @@ class LabelFormField extends StatelessWidget { String _buildText(BuildContext context, IdQueryParameter? value) { if (value?.isSet ?? false) { - return options[value!.id]?.name ?? 'undefined'; + return options[value!.id]!.name; } else if (value?.onlyNotAssigned ?? false) { return S.of(context)!.notAssigned; } else if (value?.onlyAssigned ?? false) { diff --git a/lib/features/settings/view/widgets/radio_settings_dialog.dart b/lib/features/settings/view/widgets/radio_settings_dialog.dart index 035901c..350147c 100644 --- a/lib/features/settings/view/widgets/radio_settings_dialog.dart +++ b/lib/features/settings/view/widgets/radio_settings_dialog.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart'; +import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; class RadioSettingsDialog extends StatefulWidget { @@ -38,14 +40,11 @@ class _RadioSettingsDialogState extends State> { Widget build(BuildContext context) { return AlertDialog( actions: [ + const DialogCancelButton(), widget.confirmButton ?? - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(S.of(context)!.cancel)), - widget.confirmButton ?? - TextButton( - onPressed: () => Navigator.pop(context, _groupValue), - child: Text(S.of(context)!.ok)), + DialogConfirmButton( + returnValue: _groupValue, + ), ], title: widget.titleText != null ? Text(widget.titleText!) : null, content: Column( diff --git a/lib/l10n/intl_cs.arb b/lib/l10n/intl_cs.arb index 57b6289..376c0ec 100644 --- a/lib/l10n/intl_cs.arb +++ b/lib/l10n/intl_cs.arb @@ -703,5 +703,31 @@ "@confirmAction": { "description": "Typically used as a title to confirm a previously selected action" }, - "areYouSureYouWantToContinue": "Are you sure you want to continue?" + "areYouSureYouWantToContinue": "Are you sure you want to continue?", + "bulkEditTagsAddMessage": "{count, plural, one{This operation will add the tags {tags} to the selected document.} other{This operation will add the tags {tags} to {count} selected documents.}}", + "@bulkEditTagsAddMessage": { + "description": "Message of the confirmation dialog when bulk adding tags." + }, + "bulkEditTagsRemoveMessage": "{count, plural, one{This operation will remove the tags {tags} from the selected document.} other{This operation will remove the tags {tags} from {count} selected documents.}}", + "@bulkEditTagsRemoveMessage": { + "description": "Message of the confirmation dialog when bulk removing tags." + }, + "bulkEditTagsModifyMessage": "{count, plural, one{This operation will add the tags {addTags} and remove the tags {removeTags} from the selected document.} other{This operation will add the tags {addTags} and remove the tags {removeTags} from {count} selected documents.}}", + "@bulkEditTagsModifyMessage": { + "description": "Message of the confirmation dialog when both adding and removing tags." + }, + "bulkEditCorrespondentAssignMessage": "{count, plural, one{This operation will assign the correspondent {correspondent} to the selected document.} other{This operation will assign the correspondent {correspondent} to {count} selected documents.}}", + "bulkEditDocumentTypeAssignMessage": "{count, plural, one{This operation will assign the document type {docType} to the selected document.} other{This operation will assign the documentType {docType} to {count} selected documents.}}", + "bulkEditStoragePathAssignMessage": "{count, plural, one{This operation will assign the storage path {path} to the selected document.} other{This operation will assign the storage path {path} to {count} selected documents.}}", + "bulkEditCorrespondentRemoveMessage": "{count, plural, one{This operation will remove the correspondent {correspondent} from the selected document.} other{This operation will remove the correspondent {correspondent} from {count} selected documents.}}", + "bulkEditDocumentTypeRemoveMessage": "{count, plural, one{This operation will remove the document type {docType} from the selected document.} other{This operation will remove the document type {docType} from {count} selected documents.}}", + "bulkEditStoragePathRemoveMessage": "{count, plural, one{This operation will remove the storage path {path} from the selected document.} other{This operation will remove the storage path {path} from {count} selected documents.}}", + "anyTag": "Any", + "@anyTag": { + "description": "Label shown when any tag should be filtered" + }, + "allTags": "All", + "@allTags": { + "description": "Label shown when a document has to be assigned to all selected tags" + } } \ No newline at end of file diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 877b114..90a78f4 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -703,5 +703,31 @@ "@confirmAction": { "description": "Typically used as a title to confirm a previously selected action" }, - "areYouSureYouWantToContinue": "Bist Du sicher, dass Du fortfahren möchtest?" + "areYouSureYouWantToContinue": "Bist Du sicher, dass Du fortfahren möchtest?", + "bulkEditTagsAddMessage": "{count, plural, one{Diese Operation wird die Tags {tags} dem ausgewählten Dokument hinzufügen.} other{Diese Operation wird die Tags {tags} den {count} ausgewählten Dokumenten hinzufügen.}}", + "@bulkEditTagsAddMessage": { + "description": "Message of the confirmation dialog when bulk adding tags." + }, + "bulkEditTagsRemoveMessage": "{count, plural, one{Diese Operation wird die Tags {tags} vom ausgewählten Dokument entfernen.} other{Diese Operation wird die Tags {tags} von den {count} ausgewählten Dokumenten entfernen.}}", + "@bulkEditTagsRemoveMessage": { + "description": "Message of the confirmation dialog when bulk removing tags." + }, + "bulkEditTagsModifyMessage": "{count, plural, one{Diese Operation wird die Tags {addTags} hinzufügen und die Tags {removeTags} von dem ausgewählten Dokument entfernen.} other{Diese Operation wird die Tags {addTags} hinzufügen und die Tags {removeTags} von {count} ausgewählten Dokumenten entfernen.}}", + "@bulkEditTagsModifyMessage": { + "description": "Message of the confirmation dialog when both adding and removing tags." + }, + "bulkEditCorrespondentAssignMessage": "{count, plural, one{Diese Operation wird den Korrespondent {correspondent} dem ausgewählten Dokument zuweisen.} other{Diese Operation wird den Korrespondent {correspondent} den {count} ausgewählten Dokumenten zuweisen.}}", + "bulkEditDocumentTypeAssignMessage": "{count, plural, one{Diese Operation wird den Dokumenttyp {docType} dem ausgewählten Dokument zuweisen.} other{Diese Operation wird den Dokumenttyp {docType} den {count} ausgewählten Dokumenten zuweisen.}}", + "bulkEditStoragePathAssignMessage": "{count, plural, one{Diese Operation wird den Speicherpfad {path} dem ausgewählten Dokument zuweisen.} other{Diese Operation wird den Speicherpfad {path} den {count} ausgewählten Dokumenten zuweisen.}}", + "bulkEditCorrespondentRemoveMessage": "{count, plural, one{Diese Operation wird den Korrespondent {correspondent} vom ausgewählten Dokument entfernen.} other{Diese Operation wird den Korrespondenten {correspondent} von {count} ausgewählten Dokumenten entfernen.}}", + "bulkEditDocumentTypeRemoveMessage": "{count, plural, one{Diese Operation wird den Dokumenttyp {docType} vom ausgewählten Dokument entfernen.} other{Diese Operation wird den Dokumenttyp {docType} von {count} ausgewählten Dokumenten entfernen.}}", + "bulkEditStoragePathRemoveMessage": "{count, plural, one{Diese Operation wird den Speicherpfad {path} vom ausgewählten Dokument entfernen.} other{Diese Operation wird den Speicherpfad {path} von {count} ausgewählten Dokumenten entfernen.}}", + "anyTag": "Irgendeines", + "@anyTag": { + "description": "Label shown when any tag should be filtered" + }, + "allTags": "Alle", + "@allTags": { + "description": "Label shown when a document has to be assigned to all selected tags" + } } \ No newline at end of file diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 874b175..5e903de 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -703,5 +703,31 @@ "@confirmAction": { "description": "Typically used as a title to confirm a previously selected action" }, - "areYouSureYouWantToContinue": "Are you sure you want to continue?" + "areYouSureYouWantToContinue": "Are you sure you want to continue?", + "bulkEditTagsAddMessage": "{count, plural, one{This operation will add the tags {tags} to the selected document.} other{This operation will add the tags {tags} to {count} selected documents.}}", + "@bulkEditTagsAddMessage": { + "description": "Message of the confirmation dialog when bulk adding tags." + }, + "bulkEditTagsRemoveMessage": "{count, plural, one{This operation will remove the tags {tags} from the selected document.} other{This operation will remove the tags {tags} from {count} selected documents.}}", + "@bulkEditTagsRemoveMessage": { + "description": "Message of the confirmation dialog when bulk removing tags." + }, + "bulkEditTagsModifyMessage": "{count, plural, one{This operation will add the tags {addTags} and remove the tags {removeTags} from the selected document.} other{This operation will add the tags {addTags} and remove the tags {removeTags} from {count} selected documents.}}", + "@bulkEditTagsModifyMessage": { + "description": "Message of the confirmation dialog when both adding and removing tags." + }, + "bulkEditCorrespondentAssignMessage": "{count, plural, one{This operation will assign the correspondent {correspondent} to the selected document.} other{This operation will assign the correspondent {correspondent} to {count} selected documents.}}", + "bulkEditDocumentTypeAssignMessage": "{count, plural, one{This operation will assign the document type {docType} to the selected document.} other{This operation will assign the documentType {docType} to {count} selected documents.}}", + "bulkEditStoragePathAssignMessage": "{count, plural, one{This operation will assign the storage path {path} to the selected document.} other{This operation will assign the storage path {path} to {count} selected documents.}}", + "bulkEditCorrespondentRemoveMessage": "{count, plural, one{This operation will remove the correspondent {correspondent} from the selected document.} other{This operation will remove the correspondent {correspondent} from {count} selected documents.}}", + "bulkEditDocumentTypeRemoveMessage": "{count, plural, one{This operation will remove the document type {docType} from the selected document.} other{This operation will remove the document type {docType} from {count} selected documents.}}", + "bulkEditStoragePathRemoveMessage": "{count, plural, one{This operation will remove the storage path {path} from the selected document.} other{This operation will remove the storage path {path} from {count} selected documents.}}", + "anyTag": "Any", + "@anyTag": { + "description": "Label shown when any tag should be filtered" + }, + "allTags": "All", + "@allTags": { + "description": "Label shown when a document has to be assigned to all selected tags" + } } \ No newline at end of file diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 718cbc0..cbf2e5a 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -703,5 +703,31 @@ "@confirmAction": { "description": "Typically used as a title to confirm a previously selected action" }, - "areYouSureYouWantToContinue": "Are you sure you want to continue?" + "areYouSureYouWantToContinue": "Are you sure you want to continue?", + "bulkEditTagsAddMessage": "{count, plural, one{This operation will add the tags {tags} to the selected document.} other{This operation will add the tags {tags} to {count} selected documents.}}", + "@bulkEditTagsAddMessage": { + "description": "Message of the confirmation dialog when bulk adding tags." + }, + "bulkEditTagsRemoveMessage": "{count, plural, one{This operation will remove the tags {tags} from the selected document.} other{This operation will remove the tags {tags} from {count} selected documents.}}", + "@bulkEditTagsRemoveMessage": { + "description": "Message of the confirmation dialog when bulk removing tags." + }, + "bulkEditTagsModifyMessage": "{count, plural, one{This operation will add the tags {addTags} and remove the tags {removeTags} from the selected document.} other{This operation will add the tags {addTags} and remove the tags {removeTags} from {count} selected documents.}}", + "@bulkEditTagsModifyMessage": { + "description": "Message of the confirmation dialog when both adding and removing tags." + }, + "bulkEditCorrespondentAssignMessage": "{count, plural, one{This operation will assign the correspondent {correspondent} to the selected document.} other{This operation will assign the correspondent {correspondent} to {count} selected documents.}}", + "bulkEditDocumentTypeAssignMessage": "{count, plural, one{This operation will assign the document type {docType} to the selected document.} other{This operation will assign the documentType {docType} to {count} selected documents.}}", + "bulkEditStoragePathAssignMessage": "{count, plural, one{This operation will assign the storage path {path} to the selected document.} other{This operation will assign the storage path {path} to {count} selected documents.}}", + "bulkEditCorrespondentRemoveMessage": "{count, plural, one{This operation will remove the correspondent {correspondent} from the selected document.} other{This operation will remove the correspondent {correspondent} from {count} selected documents.}}", + "bulkEditDocumentTypeRemoveMessage": "{count, plural, one{This operation will remove the document type {docType} from the selected document.} other{This operation will remove the document type {docType} from {count} selected documents.}}", + "bulkEditStoragePathRemoveMessage": "{count, plural, one{This operation will remove the storage path {path} from the selected document.} other{This operation will remove the storage path {path} from {count} selected documents.}}", + "anyTag": "Any", + "@anyTag": { + "description": "Label shown when any tag should be filtered" + }, + "allTags": "All", + "@allTags": { + "description": "Label shown when a document has to be assigned to all selected tags" + } } \ No newline at end of file diff --git a/lib/l10n/intl_pl.arb b/lib/l10n/intl_pl.arb index 14629b4..c3d09e3 100644 --- a/lib/l10n/intl_pl.arb +++ b/lib/l10n/intl_pl.arb @@ -703,5 +703,31 @@ "@confirmAction": { "description": "Typically used as a title to confirm a previously selected action" }, - "areYouSureYouWantToContinue": "Are you sure you want to continue?" + "areYouSureYouWantToContinue": "Are you sure you want to continue?", + "bulkEditTagsAddMessage": "{count, plural, one{This operation will add the tags {tags} to the selected document.} other{This operation will add the tags {tags} to {count} selected documents.}}", + "@bulkEditTagsAddMessage": { + "description": "Message of the confirmation dialog when bulk adding tags." + }, + "bulkEditTagsRemoveMessage": "{count, plural, one{This operation will remove the tags {tags} from the selected document.} other{This operation will remove the tags {tags} from {count} selected documents.}}", + "@bulkEditTagsRemoveMessage": { + "description": "Message of the confirmation dialog when bulk removing tags." + }, + "bulkEditTagsModifyMessage": "{count, plural, one{This operation will add the tags {addTags} and remove the tags {removeTags} from the selected document.} other{This operation will add the tags {addTags} and remove the tags {removeTags} from {count} selected documents.}}", + "@bulkEditTagsModifyMessage": { + "description": "Message of the confirmation dialog when both adding and removing tags." + }, + "bulkEditCorrespondentAssignMessage": "{count, plural, one{This operation will assign the correspondent {correspondent} to the selected document.} other{This operation will assign the correspondent {correspondent} to {count} selected documents.}}", + "bulkEditDocumentTypeAssignMessage": "{count, plural, one{This operation will assign the document type {docType} to the selected document.} other{This operation will assign the documentType {docType} to {count} selected documents.}}", + "bulkEditStoragePathAssignMessage": "{count, plural, one{This operation will assign the storage path {path} to the selected document.} other{This operation will assign the storage path {path} to {count} selected documents.}}", + "bulkEditCorrespondentRemoveMessage": "{count, plural, one{This operation will remove the correspondent {correspondent} from the selected document.} other{This operation will remove the correspondent {correspondent} from {count} selected documents.}}", + "bulkEditDocumentTypeRemoveMessage": "{count, plural, one{This operation will remove the document type {docType} from the selected document.} other{This operation will remove the document type {docType} from {count} selected documents.}}", + "bulkEditStoragePathRemoveMessage": "{count, plural, one{This operation will remove the storage path {path} from the selected document.} other{This operation will remove the storage path {path} from {count} selected documents.}}", + "anyTag": "Any", + "@anyTag": { + "description": "Label shown when any tag should be filtered" + }, + "allTags": "All", + "@allTags": { + "description": "Label shown when a document has to be assigned to all selected tags" + } } \ No newline at end of file diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 34be213..730e6ca 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -703,5 +703,31 @@ "@confirmAction": { "description": "Typically used as a title to confirm a previously selected action" }, - "areYouSureYouWantToContinue": "Are you sure you want to continue?" + "areYouSureYouWantToContinue": "Are you sure you want to continue?", + "bulkEditTagsAddMessage": "{count, plural, one{This operation will add the tags {tags} to the selected document.} other{This operation will add the tags {tags} to {count} selected documents.}}", + "@bulkEditTagsAddMessage": { + "description": "Message of the confirmation dialog when bulk adding tags." + }, + "bulkEditTagsRemoveMessage": "{count, plural, one{This operation will remove the tags {tags} from the selected document.} other{This operation will remove the tags {tags} from {count} selected documents.}}", + "@bulkEditTagsRemoveMessage": { + "description": "Message of the confirmation dialog when bulk removing tags." + }, + "bulkEditTagsModifyMessage": "{count, plural, one{This operation will add the tags {addTags} and remove the tags {removeTags} from the selected document.} other{This operation will add the tags {addTags} and remove the tags {removeTags} from {count} selected documents.}}", + "@bulkEditTagsModifyMessage": { + "description": "Message of the confirmation dialog when both adding and removing tags." + }, + "bulkEditCorrespondentAssignMessage": "{count, plural, one{This operation will assign the correspondent {correspondent} to the selected document.} other{This operation will assign the correspondent {correspondent} to {count} selected documents.}}", + "bulkEditDocumentTypeAssignMessage": "{count, plural, one{This operation will assign the document type {docType} to the selected document.} other{This operation will assign the documentType {docType} to {count} selected documents.}}", + "bulkEditStoragePathAssignMessage": "{count, plural, one{This operation will assign the storage path {path} to the selected document.} other{This operation will assign the storage path {path} to {count} selected documents.}}", + "bulkEditCorrespondentRemoveMessage": "{count, plural, one{This operation will remove the correspondent {correspondent} from the selected document.} other{This operation will remove the correspondent {correspondent} from {count} selected documents.}}", + "bulkEditDocumentTypeRemoveMessage": "{count, plural, one{This operation will remove the document type {docType} from the selected document.} other{This operation will remove the document type {docType} from {count} selected documents.}}", + "bulkEditStoragePathRemoveMessage": "{count, plural, one{This operation will remove the storage path {path} from the selected document.} other{This operation will remove the storage path {path} from {count} selected documents.}}", + "anyTag": "Any", + "@anyTag": { + "description": "Label shown when any tag should be filtered" + }, + "allTags": "All", + "@allTags": { + "description": "Label shown when a document has to be assigned to all selected tags" + } } \ No newline at end of file diff --git a/lib/l10n/intl_tr.arb b/lib/l10n/intl_tr.arb index 883a5cf..fd3d289 100644 --- a/lib/l10n/intl_tr.arb +++ b/lib/l10n/intl_tr.arb @@ -703,5 +703,31 @@ "@confirmAction": { "description": "Typically used as a title to confirm a previously selected action" }, - "areYouSureYouWantToContinue": "Are you sure you want to continue?" + "areYouSureYouWantToContinue": "Are you sure you want to continue?", + "bulkEditTagsAddMessage": "{count, plural, one{This operation will add the tags {tags} to the selected document.} other{This operation will add the tags {tags} to {count} selected documents.}}", + "@bulkEditTagsAddMessage": { + "description": "Message of the confirmation dialog when bulk adding tags." + }, + "bulkEditTagsRemoveMessage": "{count, plural, one{This operation will remove the tags {tags} from the selected document.} other{This operation will remove the tags {tags} from {count} selected documents.}}", + "@bulkEditTagsRemoveMessage": { + "description": "Message of the confirmation dialog when bulk removing tags." + }, + "bulkEditTagsModifyMessage": "{count, plural, one{This operation will add the tags {addTags} and remove the tags {removeTags} from the selected document.} other{This operation will add the tags {addTags} and remove the tags {removeTags} from {count} selected documents.}}", + "@bulkEditTagsModifyMessage": { + "description": "Message of the confirmation dialog when both adding and removing tags." + }, + "bulkEditCorrespondentAssignMessage": "{count, plural, one{This operation will assign the correspondent {correspondent} to the selected document.} other{This operation will assign the correspondent {correspondent} to {count} selected documents.}}", + "bulkEditDocumentTypeAssignMessage": "{count, plural, one{This operation will assign the document type {docType} to the selected document.} other{This operation will assign the documentType {docType} to {count} selected documents.}}", + "bulkEditStoragePathAssignMessage": "{count, plural, one{This operation will assign the storage path {path} to the selected document.} other{This operation will assign the storage path {path} to {count} selected documents.}}", + "bulkEditCorrespondentRemoveMessage": "{count, plural, one{This operation will remove the correspondent {correspondent} from the selected document.} other{This operation will remove the correspondent {correspondent} from {count} selected documents.}}", + "bulkEditDocumentTypeRemoveMessage": "{count, plural, one{This operation will remove the document type {docType} from the selected document.} other{This operation will remove the document type {docType} from {count} selected documents.}}", + "bulkEditStoragePathRemoveMessage": "{count, plural, one{This operation will remove the storage path {path} from the selected document.} other{This operation will remove the storage path {path} from {count} selected documents.}}", + "anyTag": "Any", + "@anyTag": { + "description": "Label shown when any tag should be filtered" + }, + "allTags": "All", + "@allTags": { + "description": "Label shown when a document has to be assigned to all selected tags" + } } \ No newline at end of file diff --git a/packages/paperless_api/lib/src/models/labels/tag_model.dart b/packages/paperless_api/lib/src/models/labels/tag_model.dart index 8f22e05..414ea79 100644 --- a/packages/paperless_api/lib/src/models/labels/tag_model.dart +++ b/packages/paperless_api/lib/src/models/labels/tag_model.dart @@ -20,7 +20,7 @@ class Tag extends Label { final Color? textColor; - final bool? isInboxTag; + final bool isInboxTag; @protected @JsonKey(name: colorKey)