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 90cf129..fb48016 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 @@ -127,14 +127,10 @@ class DocumentBulkActionCubit extends Cubit { ); final updatedDocuments = state.selection .where((element) => modifiedDocumentIds.contains(element.id)) - .map( - (doc) => doc.copyWith( - tags: [ + .map((doc) => doc.copyWith(tags: [ ...doc.tags.toSet().difference(addTagIds.toSet()), ...addTagIds - ], - ), - ); + ])); for (final doc in updatedDocuments) { _notifier.notifyUpdated(doc); } diff --git a/lib/features/document_bulk_action/view/widgets/bulk_edit_label_bottom_sheet.dart b/lib/features/document_bulk_action/view/widgets/bulk_edit_label_bottom_sheet.dart index 70e26db..f450ea1 100644 --- a/lib/features/document_bulk_action/view/widgets/bulk_edit_label_bottom_sheet.dart +++ b/lib/features/document_bulk_action/view/widgets/bulk_edit_label_bottom_sheet.dart @@ -1,6 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/src/widgets/framework.dart'; -import 'package:flutter/src/widgets/placeholder.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:paperless_api/paperless_api.dart'; @@ -10,14 +8,17 @@ import 'package:paperless_mobile/features/document_bulk_action/cubit/document_bu import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; +typedef LabelOptionsSelector = Map Function( + DocumentBulkActionState state); + class BulkEditLabelBottomSheet extends StatefulWidget { final String title; final String formFieldLabel; final Widget formFieldPrefixIcon; - final Map Function(DocumentBulkActionState state) - availableOptionsSelector; + final LabelOptionsSelector availableOptionsSelector; final void Function(int? selectedId) onSubmit; final int? initialValue; + const BulkEditLabelBottomSheet({ super.key, required this.title, 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 2ff6d41..8b733df 100644 --- a/lib/features/documents/view/widgets/search/document_filter_form.dart +++ b/lib/features/documents/view/widgets/search/document_filter_form.dart @@ -4,6 +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/view/widgets/label_form_field.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; @@ -192,6 +193,11 @@ class _DocumentFilterFormState extends State { } Widget _buildTagsFormField() { + return TagQueryFormField( + options: widget.tags, + name: DocumentModel.tagsKey, + initialValue: widget.initialFilter.tags, + ); return TagFormField( name: DocumentModel.tagsKey, initialValue: widget.initialFilter.tags, diff --git a/lib/features/labels/tags/view/widgets/fullscreen_tags_form.dart b/lib/features/labels/tags/view/widgets/fullscreen_tags_form.dart new file mode 100644 index 0000000..afda28a --- /dev/null +++ b/lib/features/labels/tags/view/widgets/fullscreen_tags_form.dart @@ -0,0 +1,16 @@ +import 'package:flutter/src/widgets/framework.dart'; +import 'package:flutter/src/widgets/placeholder.dart'; + +class FullscreenTagsForm extends StatefulWidget { + const FullscreenTagsForm({super.key}); + + @override + State createState() => _FullscreenTagsFormState(); +} + +class _FullscreenTagsFormState extends State { + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} diff --git a/lib/features/labels/tags/view/widgets/tag_form_field.dart b/lib/features/labels/tags/view/widgets/tag_form_field.dart new file mode 100644 index 0000000..a35dc1d --- /dev/null +++ b/lib/features/labels/tags/view/widgets/tag_form_field.dart @@ -0,0 +1,149 @@ +// import 'dart:developer'; + +// import 'package:animations/animations.dart'; +// import 'package:collection/collection.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/widgets/material/chips_input.dart'; +// import 'package:paperless_mobile/core/workarounds/colored_chip.dart'; +// import 'package:paperless_mobile/extensions/flutter_extensions.dart'; +// import 'package:paperless_mobile/features/labels/view/widgets/fullscreen_label_form.dart'; +// import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; + +// /// +// /// Form field allowing to select labels (i.e. correspondent, documentType) +// /// [T] is the label type (e.g. [DocumentType], [Correspondent], ...) +// /// +// class TagsFormField extends StatelessWidget { +// final Widget prefixIcon; +// final Map options; +// final IdQueryParameter? initialValue; +// final String name; +// final String labelText; +// final FormFieldValidator? validator; +// final Widget Function(String? initialName)? addLabelPageBuilder; +// final void Function(IdQueryParameter?)? onChanged; +// final bool showNotAssignedOption; +// final bool showAnyAssignedOption; +// final List suggestions; +// final String? addLabelText; + +// const TagsFormField({ +// Key? key, +// required this.name, +// required this.options, +// required this.labelText, +// required this.prefixIcon, +// this.initialValue, +// this.validator, +// this.addLabelPageBuilder, +// this.onChanged, +// this.showNotAssignedOption = true, +// this.showAnyAssignedOption = true, +// this.suggestions = const [], +// this.addLabelText, +// }) : super(key: key); + +// String _buildText(BuildContext context, IdQueryParameter? value) { +// if (value?.isSet ?? false) { +// return options[value!.id]?.name ?? 'undefined'; +// } else if (value?.onlyNotAssigned ?? false) { +// return S.of(context)!.notAssigned; +// } else if (value?.onlyAssigned ?? false) { +// return S.of(context)!.anyAssigned; +// } +// return ''; +// } + +// @override +// Widget build(BuildContext context) { +// final isEnabled = options.values.any((e) => (e.documentCount ?? 0) > 0) || +// addLabelPageBuilder != null; +// return FormBuilderField( +// name: name, +// initialValue: initialValue, +// onChanged: onChanged, +// enabled: isEnabled, +// builder: (field) { +// final controller = TextEditingController( +// text: _buildText(context, field.value), +// ); +// final displayedSuggestions = +// suggestions.whereNot((e) => e.id == field.value?.id).toList(); + +// return Column( +// children: [ +// 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: 4), +// child: +// ), +// openBuilder: (context, closeForm) => FullscreenLabelForm( +// addNewLabelText: addLabelText, +// leadingIcon: prefixIcon, +// onCreateNewLabel: addLabelPageBuilder != null +// ? (initialName) { +// return Navigator.of(context).push( +// MaterialPageRoute( +// builder: (context) => +// addLabelPageBuilder!(initialName), +// ), +// ); +// } +// : null, +// options: options, +// onSubmit: closeForm, +// initialValue: field.value, +// showAnyAssignedOption: showAnyAssignedOption, +// showNotAssignedOption: showNotAssignedOption, +// ), +// onClosed: (data) { +// if (data != null) { +// field.didChange(data); +// } +// }, +// ), +// if (displayedSuggestions.isNotEmpty) +// Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// Text( +// S.of(context)!.suggestions, +// style: Theme.of(context).textTheme.bodySmall, +// ), +// SizedBox( +// height: 48, +// child: ListView.separated( +// scrollDirection: Axis.horizontal, +// itemCount: displayedSuggestions.length, +// itemBuilder: (context, index) { +// final suggestion = +// displayedSuggestions.elementAt(index); +// return ColoredChipWrapper( +// child: ActionChip( +// label: Text(suggestion.name), +// onPressed: () => field.didChange( +// IdQueryParameter.fromId(suggestion.id), +// ), +// ), +// ); +// }, +// separatorBuilder: (BuildContext context, int index) => +// const SizedBox(width: 4.0), +// ), +// ), +// ], +// ).padded(), +// ], +// ); +// }, +// ); +// } +// } 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 new file mode 100644 index 0000000..831d533 --- /dev/null +++ b/lib/features/labels/tags/view/widgets/tag_query_form_field.dart @@ -0,0 +1,179 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/workarounds/colored_chip.dart'; +import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; + +class TagQueryFormField extends StatelessWidget { + final String name; + final Map options; + final TagsQuery? initialValue; + + const TagQueryFormField({ + super.key, + required this.options, + this.initialValue, + required this.name, + }); + + @override + Widget build(BuildContext context) { + log(initialValue.toString()); + + return FormBuilderField( + initialValue: initialValue, + builder: (field) { + final isEmpty = (field.value is IdsTagsQuery && + (field.value as IdsTagsQuery).ids.isEmpty) || + field.value == null; + final values = _generateOptions(context, field.value, field).toList(); + return GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (context) => Dialog.fullscreen( + child: Scaffold( + appBar: AppBar( + title: Text("Test"), + ), + ), + ), + ); + }, + child: InputDecorator( + isEmpty: isEmpty, + decoration: InputDecoration( + contentPadding: const EdgeInsets.all(12), + labelText: S.of(context)!.tags, + prefixIcon: const Icon(Icons.label_outline), + ), + child: SizedBox( + height: 32, + child: ListView.separated( + scrollDirection: Axis.horizontal, + separatorBuilder: (context, index) => SizedBox(width: 4), + itemBuilder: (context, index) => values[index], + itemCount: values.length, + ), + ), + ), + ); + }, + 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) { + yield _buildAnyAssignedTagWidget(context, field); + } + } + + 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: () => field.didChange(formValue.withIdQueryToggled(e.id)), + 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, FormFieldState field) { + return QueryTagChip( + onDeleted: () => field.didChange(const IdsTagsQuery()), + exclude: false, + backgroundColor: Colors.grey, + foregroundColor: Colors.black, + labelText: S.of(context)!.anyAssigned, + ); + } +} + +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..33755aa 100644 --- a/lib/features/labels/tags/view/widgets/tags_form_field.dart +++ b/lib/features/labels/tags/view/widgets/tags_form_field.dart @@ -1,5 +1,7 @@ import 'dart:developer'; +import 'package:animations/animations.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; @@ -9,6 +11,8 @@ import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/workarounds/colored_chip.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/edit_label/view/impl/add_tag_page.dart'; +import 'package:paperless_mobile/features/labels/tags/view/widgets/fullscreen_tags_form.dart'; +import 'package:paperless_mobile/features/labels/tags/view/widgets/tag_widget.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; class TagFormField extends StatefulWidget { diff --git a/packages/paperless_api/lib/src/models/query_parameters/id_query_parameter.dart b/packages/paperless_api/lib/src/models/query_parameters/id_query_parameter.dart index 21e1884..3b3dc63 100644 --- a/packages/paperless_api/lib/src/models/query_parameters/id_query_parameter.dart +++ b/packages/paperless_api/lib/src/models/query_parameters/id_query_parameter.dart @@ -57,3 +57,45 @@ class IdQueryParameter extends Equatable { factory IdQueryParameter.fromJson(Map json) => _$IdQueryParameterFromJson(json); } +// @freezed +// class IdQueryParameter with _$IdQueryParameter { +// const IdQueryParameter._(); +// const factory IdQueryParameter.unset() = _UnsetIdQueryParameter; +// const factory IdQueryParameter.notAssigned() = _NotAssignedIdQueryParameter; +// const factory IdQueryParameter.anyAssigned() = _AnyAssignedIdQueryParameter; +// const factory IdQueryParameter.id(int id) = _SetIdQueryParameter; + +// Map toQueryParameter(String field) { +// return when( +// unset: () => {}, +// notAssigned: () => { +// '${field}__isnull': '1', +// }, +// anyAssigned: () => { +// '${field}__isnull': '0', +// }, +// id: (id) => { +// '${field}_id': '$id', +// }, +// ); +// } + +// bool get onlyNotAssigned => this is _NotAssignedIdQueryParameter; + +// bool get onlyAssigned => this is _AnyAssignedIdQueryParameter; + +// bool get isSet => this is _SetIdQueryParameter; + +// bool get isUnset => this is _UnsetIdQueryParameter; +// bool matches(int? id) { +// return when( +// unset: () => true, +// notAssigned: () => id == null, +// anyAssigned: () => id != null, +// id: (id_) => id == id_, +// ); +// } + +// factory IdQueryParameter.fromJson(Map json) => +// _$IdQueryParameterFromJson(json); +// } diff --git a/packages/paperless_api/pubspec.yaml b/packages/paperless_api/pubspec.yaml index 654907e..d0fc02d 100644 --- a/packages/paperless_api/pubspec.yaml +++ b/packages/paperless_api/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: dio: ^5.0.0 collection: ^1.17.0 jiffy: ^5.0.0 + freezed_annotation: ^2.2.0 dev_dependencies: flutter_test: @@ -27,6 +28,7 @@ dev_dependencies: flutter_lints: ^2.0.0 json_serializable: ^6.5.4 build_runner: ^2.3.2 + freezed: ^2.3.2 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec