From 31ea0ba3dc10bb43caba78f7ebedd9a2b080c727 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 30 Nov 2022 16:53:42 +0100 Subject: [PATCH] #406 quick tag --- CHANGELOG.md | 2 +- lib/model/filters/filters.dart | 3 + lib/model/settings/settings.dart | 17 +++-- .../common/action_mixins/entry_editor.dart | 13 ++-- .../common/action_mixins/entry_storage.dart | 2 +- lib/widgets/common/app_bar/move_button.dart | 14 ++--- .../quick_choosers/chooser_button.dart | 11 ++-- .../quick_choosers/filter_chooser.dart | 12 ++-- .../app_bar/quick_choosers/tag_chooser.dart | 32 ++++++++++ lib/widgets/common/app_bar/rate_button.dart | 4 +- lib/widgets/common/app_bar/tag_button.dart | 62 +++++++++++++++++++ lib/widgets/debug/settings.dart | 2 + .../entry_editors/edit_tags_dialog.dart | 19 ++---- .../viewer/action/entry_action_delegate.dart | 14 +++-- .../action/entry_info_action_delegate.dart | 25 ++++++-- lib/widgets/viewer/info/basic_section.dart | 35 +++++++++-- .../viewer/overlay/viewer_buttons.dart | 25 +++++--- test/model/filters_test.dart | 4 ++ 18 files changed, 227 insertions(+), 69 deletions(-) create mode 100644 lib/widgets/common/app_bar/quick_choosers/tag_chooser.dart create mode 100644 lib/widgets/common/app_bar/tag_button.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index c3adeacd9..336147aea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ All notable changes to this project will be documented in this file. ### Added - Viewer: optionally show rating & tags on overlay -- Viewer: long press on copy/move/rating quick action for quicker action +- Viewer: long press on copy/move/rating/tag quick action for quicker action - Search: missing address, portrait, landscape filters - Lithuanian translation (thanks Gediminas Murauskas) diff --git a/lib/model/filters/filters.dart b/lib/model/filters/filters.dart index 8770c7149..a043214b8 100644 --- a/lib/model/filters/filters.dart +++ b/lib/model/filters/filters.dart @@ -11,6 +11,7 @@ import 'package:aves/model/filters/location.dart'; import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/missing.dart'; import 'package:aves/model/filters/path.dart'; +import 'package:aves/model/filters/placeholder.dart'; import 'package:aves/model/filters/query.dart'; import 'package:aves/model/filters/rating.dart'; import 'package:aves/model/filters/recent.dart'; @@ -69,6 +70,8 @@ abstract class CollectionFilter extends Equatable implements Comparable _internalKeys = { hasAcceptedTermsKey, catalogTimeZoneKey, @@ -39,7 +38,8 @@ class Settings extends ChangeNotifier { platformAccelerometerRotationKey, platformTransitionAnimationScaleKey, topEntryIdsKey, - moveDestinationAlbumsKey, + recentDestinationAlbumsKey, + recentTagsKey, }; static const _widgetKeyPrefix = 'widget_'; @@ -54,7 +54,8 @@ class Settings extends ChangeNotifier { static const tileLayoutPrefixKey = 'tile_layout_'; static const entryRenamingPatternKey = 'entry_renaming_pattern'; static const topEntryIdsKey = 'top_entry_ids'; - static const moveDestinationAlbumsKey = 'move_destination_albums'; + static const recentDestinationAlbumsKey = 'recent_destination_albums'; + static const recentTagsKey = 'recent_tags'; // display static const displayRefreshRateModeKey = 'display_refresh_rate_mode'; @@ -318,9 +319,13 @@ class Settings extends ChangeNotifier { set topEntryIds(List? newValue) => setAndNotify(topEntryIdsKey, newValue?.map((id) => id.toString()).whereNotNull().toList()); - List get moveDestinationAlbums => getStringList(moveDestinationAlbumsKey) ?? []; + List get recentDestinationAlbums => getStringList(recentDestinationAlbumsKey) ?? []; - set moveDestinationAlbums(List newValue) => setAndNotify(moveDestinationAlbumsKey, newValue.take(Settings.moveDestinationAlbumMax).toList()); + set recentDestinationAlbums(List newValue) => setAndNotify(recentDestinationAlbumsKey, newValue.take(_recentFilterHistoryMax).toList()); + + List get recentTags => (getStringList(recentTagsKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toList(); + + set recentTags(List newValue) => setAndNotify(recentTagsKey, newValue.take(_recentFilterHistoryMax).map((filter) => filter.toJson()).toList()); // display diff --git a/lib/widgets/common/action_mixins/entry_editor.dart b/lib/widgets/common/action_mixins/entry_editor.dart index ec36e7f91..66e7e1321 100644 --- a/lib/widgets/common/action_mixins/entry_editor.dart +++ b/lib/widgets/common/action_mixins/entry_editor.dart @@ -96,16 +96,19 @@ mixin EntryEditorMixin { await Future.forEach(filtersByEntry.entries, (kv) async { final entry = kv.key; final filters = kv.value; - final tags = filters.whereType().map((v) => v.tag).toSet(); - tagsByEntry[entry] = tags; - - final placeholderTags = await Future.wait(filters.whereType().map((v) => v.toTag(entry))); - tags.addAll(placeholderTags.whereNotNull().where((v) => v.isNotEmpty)); + tagsByEntry[entry] = await getTagsFromFilters(filters, entry); }); return tagsByEntry; } + Future> getTagsFromFilters(Set filters, AvesEntry entry) async { + final tags = filters.whereType().map((v) => v.tag).toSet(); + final placeholderTags = await Future.wait(filters.whereType().map((v) => v.toTag(entry))); + tags.addAll(placeholderTags.whereNotNull().where((v) => v.isNotEmpty)); + return tags; + } + Future?> selectMetadataToRemove(BuildContext context, Set entries) async { if (entries.isEmpty) return null; diff --git a/lib/widgets/common/action_mixins/entry_storage.dart b/lib/widgets/common/action_mixins/entry_storage.dart index 69e041d82..e99c52296 100644 --- a/lib/widgets/common/action_mixins/entry_storage.dart +++ b/lib/widgets/common/action_mixins/entry_storage.dart @@ -209,7 +209,7 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { final destinationAlbum = await pickAlbum(context: context, moveType: moveType); if (destinationAlbum == null) return; - settings.moveDestinationAlbums = settings.moveDestinationAlbums + settings.recentDestinationAlbums = settings.recentDestinationAlbums ..remove(destinationAlbum) ..insert(0, destinationAlbum); entriesByDestination[destinationAlbum] = entries; diff --git a/lib/widgets/common/app_bar/move_button.dart b/lib/widgets/common/app_bar/move_button.dart index c70870001..edaec5843 100644 --- a/lib/widgets/common/app_bar/move_button.dart +++ b/lib/widgets/common/app_bar/move_button.dart @@ -5,6 +5,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/widgets/common/app_bar/quick_choosers/album_chooser.dart'; import 'package:aves/widgets/common/app_bar/quick_choosers/chooser_button.dart'; +import 'package:aves/widgets/common/app_bar/quick_choosers/filter_chooser.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart'; import 'package:collection/collection.dart'; @@ -23,10 +24,10 @@ class MoveButton extends ChooserQuickButton { }); @override - State createState() => _MoveQuickButtonState(); + State createState() => _MoveButtonState(); } -class _MoveQuickButtonState extends ChooserQuickButtonState { +class _MoveButtonState extends ChooserQuickButtonState { EntryAction get action => widget.copy ? EntryAction.copy : EntryAction.move; @override @@ -35,13 +36,10 @@ class _MoveQuickButtonState extends ChooserQuickButtonState @override String get tooltip => action.getText(context); - @override - String? get defaultValue => null; - @override Widget buildChooser(Animation animation) { - final options = settings.moveDestinationAlbums; - final takeCount = Settings.moveDestinationAlbumMax - options.length; + final options = settings.recentDestinationAlbums; + final takeCount = FilterQuickChooser.maxOptionCount - options.length; if (takeCount > 0) { final source = context.read(); final filters = source.rawAlbums.whereNot(options.contains).map((album) => AlbumFilter(album, null)).toSet(); @@ -57,8 +55,8 @@ class _MoveQuickButtonState extends ChooserQuickButtonState scale: animation, child: AlbumQuickChooser( valueNotifier: chooserValueNotifier, - pointerGlobalPosition: pointerGlobalPosition, options: widget.chooserPosition == PopupMenuPosition.over ? options.reversed.toList() : options, + pointerGlobalPosition: pointerGlobalPosition, ), ), ), diff --git a/lib/widgets/common/app_bar/quick_choosers/chooser_button.dart b/lib/widgets/common/app_bar/quick_choosers/chooser_button.dart index a1c2b4acb..b3f71c1b7 100644 --- a/lib/widgets/common/app_bar/quick_choosers/chooser_button.dart +++ b/lib/widgets/common/app_bar/quick_choosers/chooser_button.dart @@ -7,8 +7,8 @@ import 'package:provider/provider.dart'; abstract class ChooserQuickButton extends StatefulWidget { final PopupMenuPosition? chooserPosition; - final ValueSetter? onChooserValue; - final VoidCallback onPressed; + final ValueSetter? onChooserValue; + final VoidCallback? onPressed; const ChooserQuickButton({ super.key, @@ -29,7 +29,7 @@ abstract class ChooserQuickButtonState, U> exten String get tooltip; - U? get defaultValue; + U? get defaultValue => null; Duration get animationDuration => context.read().quickChooserAnimation; @@ -61,7 +61,10 @@ abstract class ChooserQuickButtonState, U> exten onLongPressEnd: isChooserEnabled ? (details) { _clearChooserOverlayEntry(); - onChooserValue.call(_chooserValueNotifier.value); + final selectedValue = _chooserValueNotifier.value; + if (selectedValue != null) { + onChooserValue(selectedValue); + } } : null, onLongPressCancel: _clearChooserOverlayEntry, diff --git a/lib/widgets/common/app_bar/quick_choosers/filter_chooser.dart b/lib/widgets/common/app_bar/quick_choosers/filter_chooser.dart index 5c49ddfee..1b51b3a4a 100644 --- a/lib/widgets/common/app_bar/quick_choosers/filter_chooser.dart +++ b/lib/widgets/common/app_bar/quick_choosers/filter_chooser.dart @@ -11,13 +11,15 @@ class FilterQuickChooser extends StatefulWidget { final Stream pointerGlobalPosition; final Widget Function(BuildContext context, T album) buildFilterChip; - const FilterQuickChooser({ + static const int maxOptionCount = 3; + + FilterQuickChooser({ super.key, required this.valueNotifier, - required this.options, + required List options, required this.pointerGlobalPosition, required this.buildFilterChip, - }); + }) : options = options.take(maxOptionCount).toList(); @override State> createState() => _FilterQuickChooserState(); @@ -134,8 +136,8 @@ class _FilterQuickChooserState extends State> { T? selectedValue; if (0 < dx && dx < contentWidth && 0 < dy && dy < contentHeight) { - final index = (options.length * dy / contentHeight).floor(); - if (0 <= index && index < options.length) { + final index = (optionCount * dy / contentHeight).floor(); + if (0 <= index && index < optionCount) { selectedValue = options[index]; final top = index * (itemHeight + intraPadding); _selectedRowRect.value = Rect.fromLTWH(0, top, contentWidth, itemHeight); diff --git a/lib/widgets/common/app_bar/quick_choosers/tag_chooser.dart b/lib/widgets/common/app_bar/quick_choosers/tag_chooser.dart new file mode 100644 index 000000000..c546eb624 --- /dev/null +++ b/lib/widgets/common/app_bar/quick_choosers/tag_chooser.dart @@ -0,0 +1,32 @@ +import 'dart:async'; + +import 'package:aves/model/filters/filters.dart'; +import 'package:aves/widgets/common/app_bar/quick_choosers/filter_chooser.dart'; +import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; +import 'package:flutter/material.dart'; + +class TagQuickChooser extends StatelessWidget { + final ValueNotifier valueNotifier; + final List options; + final Stream pointerGlobalPosition; + + const TagQuickChooser({ + super.key, + required this.valueNotifier, + required this.options, + required this.pointerGlobalPosition, + }); + + @override + Widget build(BuildContext context) { + return FilterQuickChooser( + valueNotifier: valueNotifier, + options: options, + pointerGlobalPosition: pointerGlobalPosition, + buildFilterChip: (context, filter) => AvesFilterChip( + filter: filter, + showGenericIcon: false, + ), + ); + } +} diff --git a/lib/widgets/common/app_bar/rate_button.dart b/lib/widgets/common/app_bar/rate_button.dart index 64fcb2dc4..2cee25b45 100644 --- a/lib/widgets/common/app_bar/rate_button.dart +++ b/lib/widgets/common/app_bar/rate_button.dart @@ -12,10 +12,10 @@ class RateButton extends ChooserQuickButton { }); @override - State createState() => _RateQuickButtonState(); + State createState() => _RateButtonState(); } -class _RateQuickButtonState extends ChooserQuickButtonState { +class _RateButtonState extends ChooserQuickButtonState { static const _action = EntryAction.editRating; @override diff --git a/lib/widgets/common/app_bar/tag_button.dart b/lib/widgets/common/app_bar/tag_button.dart new file mode 100644 index 000000000..eda816098 --- /dev/null +++ b/lib/widgets/common/app_bar/tag_button.dart @@ -0,0 +1,62 @@ +import 'package:aves/model/actions/entry_actions.dart'; +import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/filters/tag.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/widgets/common/app_bar/quick_choosers/chooser_button.dart'; +import 'package:aves/widgets/common/app_bar/quick_choosers/filter_chooser.dart'; +import 'package:aves/widgets/common/app_bar/quick_choosers/tag_chooser.dart'; +import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; +import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class TagButton extends ChooserQuickButton { + const TagButton({ + super.key, + super.chooserPosition, + super.onChooserValue, + required super.onPressed, + }); + + @override + State createState() => _TagButtonState(); +} + +class _TagButtonState extends ChooserQuickButtonState { + EntryAction get action => EntryAction.editTags; + + @override + Widget get icon => action.getIcon(); + + @override + String get tooltip => action.getText(context); + + @override + Widget buildChooser(Animation animation) { + final options = settings.recentTags; + final takeCount = FilterQuickChooser.maxOptionCount - options.length; + if (takeCount > 0) { + final source = context.read(); + final filters = source.sortedTags.map(TagFilter.new).whereNot(options.contains).toSet(); + final allMapEntries = filters.map((filter) => FilterGridItem(filter, source.recentEntry(filter))).toList(); + allMapEntries.sort(FilterNavigationPage.compareFiltersByDate); + options.addAll(allMapEntries.take(takeCount).map((v) => v.filter)); + } + + return MediaQueryDataProvider( + child: FadeTransition( + opacity: animation, + child: ScaleTransition( + scale: animation, + child: TagQuickChooser( + valueNotifier: chooserValueNotifier, + options: widget.chooserPosition == PopupMenuPosition.over ? options.reversed.toList() : options, + pointerGlobalPosition: pointerGlobalPosition, + ), + ), + ), + ); + } +} diff --git a/lib/widgets/debug/settings.dart b/lib/widgets/debug/settings.dart index f84b1c897..a4bbe7c98 100644 --- a/lib/widgets/debug/settings.dart +++ b/lib/widgets/debug/settings.dart @@ -65,6 +65,8 @@ class DebugSettingsSection extends StatelessWidget { 'pinnedFilters': toMultiline(settings.pinnedFilters), 'hiddenFilters': toMultiline(settings.hiddenFilters), 'searchHistory': toMultiline(settings.searchHistory), + 'recentDestinationAlbums': toMultiline(settings.recentDestinationAlbums), + 'recentTags': toMultiline(settings.recentTags), 'locale': '${settings.locale}', 'systemLocales': '${WidgetsBinding.instance.window.locales}', 'topEntryIds': '${settings.topEntryIds}', diff --git a/lib/widgets/dialogs/entry_editors/edit_tags_dialog.dart b/lib/widgets/dialogs/entry_editors/edit_tags_dialog.dart index 6f91fadf4..bed1fe719 100644 --- a/lib/widgets/dialogs/entry_editors/edit_tags_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/edit_tags_dialog.dart @@ -1,9 +1,8 @@ -import 'dart:math'; - import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/placeholder.dart'; import 'package:aves/model/filters/tag.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; @@ -35,10 +34,7 @@ class _TagEditorPageState extends State { late final List _topTags; late final List _placeholders = [PlaceholderFilter.country, PlaceholderFilter.place]; - static final List _recentTags = []; - static const Color untaggedColor = Colors.blueGrey; - static const int tagHistoryCount = 10; Map> get tagsByEntry => widget.filtersByEntry; @@ -79,7 +75,7 @@ class _TagEditorPageState extends State { builder: (context, value, child) { final upQuery = value.text.trim().toUpperCase(); bool containQuery(CollectionFilter v) => v.getLabel(context).toUpperCase().contains(upQuery); - final recentFilters = _recentTags.where(containQuery).toList(); + final recentFilters = settings.recentTags.where(containQuery).toList(); final topTagFilters = _topTags.where(containQuery).toList(); final placeholderFilters = _placeholders.where(containQuery).toList(); return ListView( @@ -220,13 +216,10 @@ class _TagEditorPageState extends State { } void _addTag(CollectionFilter newTag) { - setState(() { - _recentTags - ..remove(newTag) - ..insert(0, newTag) - ..removeRange(min(tagHistoryCount, _recentTags.length), _recentTags.length); - tagsByEntry.forEach((entry, tags) => tags.add(newTag)); - }); + settings.recentTags = settings.recentTags + ..remove(newTag) + ..insert(0, newTag); + setState(() => tagsByEntry.forEach((entry, tags) => tags.add(newTag))); _newTagTextController.clear(); } diff --git a/lib/widgets/viewer/action/entry_action_delegate.dart b/lib/widgets/viewer/action/entry_action_delegate.dart index d56ea8c8d..c2f6b33cb 100644 --- a/lib/widgets/viewer/action/entry_action_delegate.dart +++ b/lib/widgets/viewer/action/entry_action_delegate.dart @@ -8,6 +8,7 @@ import 'package:aves/model/device.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/entry_metadata_edition.dart'; import 'package:aves/model/filters/album.dart'; +import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; @@ -280,9 +281,9 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix } } - void quickMove(BuildContext context, String? album, {required bool copy}) { - final targetEntry = _getTargetEntry(context, EntryAction.editRating); - if (album == null || (!copy && targetEntry.directory == album)) return; + void quickMove(BuildContext context, String album, {required bool copy}) { + final targetEntry = _getTargetEntry(context, copy ? EntryAction.copy : EntryAction.move); + if (!copy && targetEntry.directory == album) return; doQuickMove( context, @@ -293,11 +294,16 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix ); } - void quickRate(BuildContext context, int? rating) { + void quickRate(BuildContext context, int rating) { final targetEntry = _getTargetEntry(context, EntryAction.editRating); _metadataActionDelegate.quickRate(context, targetEntry, rating); } + void quickTag(BuildContext context, CollectionFilter filter) { + final targetEntry = _getTargetEntry(context, EntryAction.editTags); + _metadataActionDelegate.quickTag(context, targetEntry, filter); + } + Future _addShortcut(BuildContext context, AvesEntry targetEntry) async { final result = await showDialog>( context: context, diff --git a/lib/widgets/viewer/action/entry_info_action_delegate.dart b/lib/widgets/viewer/action/entry_info_action_delegate.dart index 29829fe32..0d458be95 100644 --- a/lib/widgets/viewer/action/entry_info_action_delegate.dart +++ b/lib/widgets/viewer/action/entry_info_action_delegate.dart @@ -6,6 +6,7 @@ import 'package:aves/model/actions/events.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/entry_info.dart'; import 'package:aves/model/entry_metadata_edition.dart'; +import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/geotiff.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/ref/mime_types.dart'; @@ -144,20 +145,34 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi Future _editRating(BuildContext context, AvesEntry targetEntry) async { final rating = await selectRating(context, {targetEntry}); + if (rating == null) return; + await quickRate(context, targetEntry, rating); } - Future quickRate(BuildContext context, AvesEntry targetEntry, int? rating) async { - if (rating == null || targetEntry.rating == rating) return; + Future quickRate(BuildContext context, AvesEntry targetEntry, int rating) async { + if (targetEntry.rating == rating) return; await edit(context, targetEntry, () => targetEntry.editRating(rating)); } Future _editTags(BuildContext context, AvesEntry targetEntry) async { - final newTagsByEntry = await selectTags(context, {targetEntry}); - if (newTagsByEntry == null) return; + final tagsByEntry = await selectTags(context, {targetEntry}); + if (tagsByEntry == null) return; - final newTags = newTagsByEntry[targetEntry] ?? targetEntry.tags; + final newTags = tagsByEntry[targetEntry] ?? targetEntry.tags; + await _applyTags(context, targetEntry, newTags); + } + + Future quickTag(BuildContext context, AvesEntry targetEntry, CollectionFilter filter) async { + final newTags = { + ...targetEntry.tags, + ...await getTagsFromFilters({filter}, targetEntry), + }; + await _applyTags(context, targetEntry, newTags); + } + + Future _applyTags(BuildContext context, AvesEntry targetEntry, Set newTags) async { final currentTags = targetEntry.tags; if (newTags.length == currentTags.length && newTags.every(currentTags.contains)) return; diff --git a/lib/widgets/viewer/info/basic_section.dart b/lib/widgets/viewer/info/basic_section.dart index 5f9e672c0..ae2f6ce06 100644 --- a/lib/widgets/viewer/info/basic_section.dart +++ b/lib/widgets/viewer/info/basic_section.dart @@ -16,6 +16,8 @@ import 'package:aves/theme/colors.dart'; import 'package:aves/theme/format.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/file_utils.dart'; +import 'package:aves/widgets/common/app_bar/rate_button.dart'; +import 'package:aves/widgets/common/app_bar/tag_button.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/viewer/action/entry_info_action_delegate.dart'; @@ -32,6 +34,8 @@ class BasicSection extends StatelessWidget { final ValueNotifier isEditingMetadataNotifier; final FilterCallback onFilter; + static const quickChooserPosition = PopupMenuPosition.over; + const BasicSection({ super.key, required this.entry, @@ -126,6 +130,31 @@ class BasicSection extends StatelessWidget { valueListenable: isEditingMetadataNotifier, builder: (context, editingAction, child) { final isEditing = editingAction != null; + final onPressed = isEditing ? null : () => actionDelegate.onActionSelected(context, entry, collection, action); + Widget button; + switch (action) { + case EntryAction.editRating: + button = RateButton( + chooserPosition: quickChooserPosition, + onChooserValue: (rating) => actionDelegate.quickRate(context, entry, rating), + onPressed: onPressed, + ); + break; + case EntryAction.editTags: + button = TagButton( + chooserPosition: quickChooserPosition, + onChooserValue: (filter) => actionDelegate.quickTag(context, entry, filter), + onPressed: onPressed, + ); + break; + default: + button = IconButton( + icon: action.getIcon(), + onPressed: onPressed, + tooltip: action.getText(context), + ); + break; + } return Stack( children: [ DecoratedBox( @@ -136,11 +165,7 @@ class BasicSection extends StatelessWidget { )), borderRadius: const BorderRadius.all(Radius.circular(AvesFilterChip.defaultRadius)), ), - child: IconButton( - icon: action.getIcon(), - onPressed: isEditing ? null : () => actionDelegate.onActionSelected(context, entry, collection, action), - tooltip: action.getText(context), - ), + child: button, ), Positioned.fill( child: Visibility( diff --git a/lib/widgets/viewer/overlay/viewer_buttons.dart b/lib/widgets/viewer/overlay/viewer_buttons.dart index 0b808be2d..21539d339 100644 --- a/lib/widgets/viewer/overlay/viewer_buttons.dart +++ b/lib/widgets/viewer/overlay/viewer_buttons.dart @@ -7,6 +7,7 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/app_bar/favourite_toggler.dart'; import 'package:aves/widgets/common/app_bar/move_button.dart'; import 'package:aves/widgets/common/app_bar/rate_button.dart'; +import 'package:aves/widgets/common/app_bar/tag_button.dart'; import 'package:aves/widgets/common/basic/menu.dart'; import 'package:aves/widgets/common/basic/popup_menu_button.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -90,6 +91,7 @@ class ViewerButtonRowContent extends StatelessWidget { AvesEntry get favouriteTargetEntry => mainEntry.isBurst ? pageEntry : mainEntry; static const double padding = 8; + static const quickChooserPosition = PopupMenuPosition.over; ViewerButtonRowContent({ super.key, @@ -208,16 +210,16 @@ class ViewerButtonRowContent extends StatelessWidget { case EntryAction.copy: child = MoveButton( copy: true, - chooserPosition: PopupMenuPosition.over, - onChooserValue: (album) => _quickMove(context, album, copy: true), + chooserPosition: quickChooserPosition, + onChooserValue: (album) => _entryActionDelegate.quickMove(context, album, copy: true), onPressed: onPressed, ); break; case EntryAction.move: child = MoveButton( copy: false, - chooserPosition: PopupMenuPosition.over, - onChooserValue: (album) => _quickMove(context, album, copy: false), + chooserPosition: quickChooserPosition, + onChooserValue: (album) => _entryActionDelegate.quickMove(context, album, copy: false), onPressed: onPressed, ); break; @@ -250,8 +252,15 @@ class ViewerButtonRowContent extends StatelessWidget { break; case EntryAction.editRating: child = RateButton( - chooserPosition: PopupMenuPosition.over, - onChooserValue: (rating) => _quickRate(context, rating), + chooserPosition: quickChooserPosition, + onChooserValue: (rating) => _entryActionDelegate.quickRate(context, rating), + onPressed: onPressed, + ); + break; + case EntryAction.editTags: + child = TagButton( + chooserPosition: quickChooserPosition, + onChooserValue: (filter) => _entryActionDelegate.quickTag(context, filter), onPressed: onPressed, ); break; @@ -381,8 +390,4 @@ class ViewerButtonRowContent extends StatelessWidget { EntryActionDelegate get _entryActionDelegate => EntryActionDelegate(mainEntry, pageEntry, collection); void _onActionSelected(BuildContext context, EntryAction action) => _entryActionDelegate.onActionSelected(context, action); - - void _quickMove(BuildContext context, String? album, {required bool copy}) => _entryActionDelegate.quickMove(context, album, copy: copy); - - void _quickRate(BuildContext context, int? rating) => _entryActionDelegate.quickRate(context, rating); } diff --git a/test/model/filters_test.dart b/test/model/filters_test.dart index 2f77c5162..fe50208fe 100644 --- a/test/model/filters_test.dart +++ b/test/model/filters_test.dart @@ -8,6 +8,7 @@ import 'package:aves/model/filters/location.dart'; import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/missing.dart'; import 'package:aves/model/filters/path.dart'; +import 'package:aves/model/filters/placeholder.dart'; import 'package:aves/model/filters/query.dart'; import 'package:aves/model/filters/rating.dart'; import 'package:aves/model/filters/recent.dart'; @@ -64,6 +65,9 @@ void main() { final path = PathFilter('/some/path/'); expect(path, jsonRoundTrip(path)); + final placeholder = PlaceholderFilter.country; + expect(placeholder, jsonRoundTrip(placeholder)); + final query = QueryFilter('some query'); expect(query, jsonRoundTrip(query));