#406 quick tag
This commit is contained in:
parent
f57e2306e2
commit
31ea0ba3dc
18 changed files with 227 additions and 69 deletions
|
@ -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)
|
||||
|
||||
|
|
|
@ -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<Collecti
|
|||
return MissingFilter.fromMap(jsonMap);
|
||||
case PathFilter.type:
|
||||
return PathFilter.fromMap(jsonMap);
|
||||
case PlaceholderFilter.type:
|
||||
return PlaceholderFilter.fromMap(jsonMap);
|
||||
case QueryFilter.type:
|
||||
return QueryFilter.fromMap(jsonMap);
|
||||
case RatingFilter.type:
|
||||
|
|
|
@ -29,8 +29,7 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
Settings._private();
|
||||
|
||||
static const int moveDestinationAlbumMax = 3;
|
||||
|
||||
static const int _recentFilterHistoryMax = 10;
|
||||
static const Set<String> _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<int>? newValue) => setAndNotify(topEntryIdsKey, newValue?.map((id) => id.toString()).whereNotNull().toList());
|
||||
|
||||
List<String> get moveDestinationAlbums => getStringList(moveDestinationAlbumsKey) ?? [];
|
||||
List<String> get recentDestinationAlbums => getStringList(recentDestinationAlbumsKey) ?? [];
|
||||
|
||||
set moveDestinationAlbums(List<String> newValue) => setAndNotify(moveDestinationAlbumsKey, newValue.take(Settings.moveDestinationAlbumMax).toList());
|
||||
set recentDestinationAlbums(List<String> newValue) => setAndNotify(recentDestinationAlbumsKey, newValue.take(_recentFilterHistoryMax).toList());
|
||||
|
||||
List<CollectionFilter> get recentTags => (getStringList(recentTagsKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toList();
|
||||
|
||||
set recentTags(List<CollectionFilter> newValue) => setAndNotify(recentTagsKey, newValue.take(_recentFilterHistoryMax).map((filter) => filter.toJson()).toList());
|
||||
|
||||
// display
|
||||
|
||||
|
|
|
@ -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<TagFilter>().map((v) => v.tag).toSet();
|
||||
tagsByEntry[entry] = tags;
|
||||
|
||||
final placeholderTags = await Future.wait(filters.whereType<PlaceholderFilter>().map((v) => v.toTag(entry)));
|
||||
tags.addAll(placeholderTags.whereNotNull().where((v) => v.isNotEmpty));
|
||||
tagsByEntry[entry] = await getTagsFromFilters(filters, entry);
|
||||
});
|
||||
|
||||
return tagsByEntry;
|
||||
}
|
||||
|
||||
Future<Set<String>> getTagsFromFilters(Set<CollectionFilter> filters, AvesEntry entry) async {
|
||||
final tags = filters.whereType<TagFilter>().map((v) => v.tag).toSet();
|
||||
final placeholderTags = await Future.wait(filters.whereType<PlaceholderFilter>().map((v) => v.toTag(entry)));
|
||||
tags.addAll(placeholderTags.whereNotNull().where((v) => v.isNotEmpty));
|
||||
return tags;
|
||||
}
|
||||
|
||||
Future<Set<MetadataType>?> selectMetadataToRemove(BuildContext context, Set<AvesEntry> entries) async {
|
||||
if (entries.isEmpty) return null;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<String> {
|
|||
});
|
||||
|
||||
@override
|
||||
State<MoveButton> createState() => _MoveQuickButtonState();
|
||||
State<MoveButton> createState() => _MoveButtonState();
|
||||
}
|
||||
|
||||
class _MoveQuickButtonState extends ChooserQuickButtonState<MoveButton, String> {
|
||||
class _MoveButtonState extends ChooserQuickButtonState<MoveButton, String> {
|
||||
EntryAction get action => widget.copy ? EntryAction.copy : EntryAction.move;
|
||||
|
||||
@override
|
||||
|
@ -35,13 +36,10 @@ class _MoveQuickButtonState extends ChooserQuickButtonState<MoveButton, String>
|
|||
@override
|
||||
String get tooltip => action.getText(context);
|
||||
|
||||
@override
|
||||
String? get defaultValue => null;
|
||||
|
||||
@override
|
||||
Widget buildChooser(Animation<double> 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<CollectionSource>();
|
||||
final filters = source.rawAlbums.whereNot(options.contains).map((album) => AlbumFilter(album, null)).toSet();
|
||||
|
@ -57,8 +55,8 @@ class _MoveQuickButtonState extends ChooserQuickButtonState<MoveButton, String>
|
|||
scale: animation,
|
||||
child: AlbumQuickChooser(
|
||||
valueNotifier: chooserValueNotifier,
|
||||
pointerGlobalPosition: pointerGlobalPosition,
|
||||
options: widget.chooserPosition == PopupMenuPosition.over ? options.reversed.toList() : options,
|
||||
pointerGlobalPosition: pointerGlobalPosition,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -7,8 +7,8 @@ import 'package:provider/provider.dart';
|
|||
|
||||
abstract class ChooserQuickButton<T> extends StatefulWidget {
|
||||
final PopupMenuPosition? chooserPosition;
|
||||
final ValueSetter<T?>? onChooserValue;
|
||||
final VoidCallback onPressed;
|
||||
final ValueSetter<T>? onChooserValue;
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
const ChooserQuickButton({
|
||||
super.key,
|
||||
|
@ -29,7 +29,7 @@ abstract class ChooserQuickButtonState<T extends ChooserQuickButton<U>, U> exten
|
|||
|
||||
String get tooltip;
|
||||
|
||||
U? get defaultValue;
|
||||
U? get defaultValue => null;
|
||||
|
||||
Duration get animationDuration => context.read<DurationsData>().quickChooserAnimation;
|
||||
|
||||
|
@ -61,7 +61,10 @@ abstract class ChooserQuickButtonState<T extends ChooserQuickButton<U>, U> exten
|
|||
onLongPressEnd: isChooserEnabled
|
||||
? (details) {
|
||||
_clearChooserOverlayEntry();
|
||||
onChooserValue.call(_chooserValueNotifier.value);
|
||||
final selectedValue = _chooserValueNotifier.value;
|
||||
if (selectedValue != null) {
|
||||
onChooserValue(selectedValue);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
onLongPressCancel: _clearChooserOverlayEntry,
|
||||
|
|
|
@ -11,13 +11,15 @@ class FilterQuickChooser<T> extends StatefulWidget {
|
|||
final Stream<Offset> 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<T> options,
|
||||
required this.pointerGlobalPosition,
|
||||
required this.buildFilterChip,
|
||||
});
|
||||
}) : options = options.take(maxOptionCount).toList();
|
||||
|
||||
@override
|
||||
State<FilterQuickChooser<T>> createState() => _FilterQuickChooserState<T>();
|
||||
|
@ -134,8 +136,8 @@ class _FilterQuickChooserState<T> extends State<FilterQuickChooser<T>> {
|
|||
|
||||
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);
|
||||
|
|
32
lib/widgets/common/app_bar/quick_choosers/tag_chooser.dart
Normal file
32
lib/widgets/common/app_bar/quick_choosers/tag_chooser.dart
Normal file
|
@ -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<CollectionFilter?> valueNotifier;
|
||||
final List<CollectionFilter> options;
|
||||
final Stream<Offset> pointerGlobalPosition;
|
||||
|
||||
const TagQuickChooser({
|
||||
super.key,
|
||||
required this.valueNotifier,
|
||||
required this.options,
|
||||
required this.pointerGlobalPosition,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FilterQuickChooser<CollectionFilter>(
|
||||
valueNotifier: valueNotifier,
|
||||
options: options,
|
||||
pointerGlobalPosition: pointerGlobalPosition,
|
||||
buildFilterChip: (context, filter) => AvesFilterChip(
|
||||
filter: filter,
|
||||
showGenericIcon: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -12,10 +12,10 @@ class RateButton extends ChooserQuickButton<int> {
|
|||
});
|
||||
|
||||
@override
|
||||
State<RateButton> createState() => _RateQuickButtonState();
|
||||
State<RateButton> createState() => _RateButtonState();
|
||||
}
|
||||
|
||||
class _RateQuickButtonState extends ChooserQuickButtonState<RateButton, int> {
|
||||
class _RateButtonState extends ChooserQuickButtonState<RateButton, int> {
|
||||
static const _action = EntryAction.editRating;
|
||||
|
||||
@override
|
||||
|
|
62
lib/widgets/common/app_bar/tag_button.dart
Normal file
62
lib/widgets/common/app_bar/tag_button.dart
Normal file
|
@ -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<CollectionFilter> {
|
||||
const TagButton({
|
||||
super.key,
|
||||
super.chooserPosition,
|
||||
super.onChooserValue,
|
||||
required super.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
State<TagButton> createState() => _TagButtonState();
|
||||
}
|
||||
|
||||
class _TagButtonState extends ChooserQuickButtonState<TagButton, CollectionFilter> {
|
||||
EntryAction get action => EntryAction.editTags;
|
||||
|
||||
@override
|
||||
Widget get icon => action.getIcon();
|
||||
|
||||
@override
|
||||
String get tooltip => action.getText(context);
|
||||
|
||||
@override
|
||||
Widget buildChooser(Animation<double> animation) {
|
||||
final options = settings.recentTags;
|
||||
final takeCount = FilterQuickChooser.maxOptionCount - options.length;
|
||||
if (takeCount > 0) {
|
||||
final source = context.read<CollectionSource>();
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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}',
|
||||
|
|
|
@ -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<TagEditorPage> {
|
|||
late final List<CollectionFilter> _topTags;
|
||||
late final List<PlaceholderFilter> _placeholders = [PlaceholderFilter.country, PlaceholderFilter.place];
|
||||
|
||||
static final List<CollectionFilter> _recentTags = [];
|
||||
|
||||
static const Color untaggedColor = Colors.blueGrey;
|
||||
static const int tagHistoryCount = 10;
|
||||
|
||||
Map<AvesEntry, Set<CollectionFilter>> get tagsByEntry => widget.filtersByEntry;
|
||||
|
||||
|
@ -79,7 +75,7 @@ class _TagEditorPageState extends State<TagEditorPage> {
|
|||
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<TagEditorPage> {
|
|||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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<void> _addShortcut(BuildContext context, AvesEntry targetEntry) async {
|
||||
final result = await showDialog<Tuple2<AvesEntry?, String>>(
|
||||
context: context,
|
||||
|
|
|
@ -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<void> _editRating(BuildContext context, AvesEntry targetEntry) async {
|
||||
final rating = await selectRating(context, {targetEntry});
|
||||
if (rating == null) return;
|
||||
|
||||
await quickRate(context, targetEntry, rating);
|
||||
}
|
||||
|
||||
Future<void> quickRate(BuildContext context, AvesEntry targetEntry, int? rating) async {
|
||||
if (rating == null || targetEntry.rating == rating) return;
|
||||
Future<void> quickRate(BuildContext context, AvesEntry targetEntry, int rating) async {
|
||||
if (targetEntry.rating == rating) return;
|
||||
|
||||
await edit(context, targetEntry, () => targetEntry.editRating(rating));
|
||||
}
|
||||
|
||||
Future<void> _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<void> quickTag(BuildContext context, AvesEntry targetEntry, CollectionFilter filter) async {
|
||||
final newTags = {
|
||||
...targetEntry.tags,
|
||||
...await getTagsFromFilters({filter}, targetEntry),
|
||||
};
|
||||
await _applyTags(context, targetEntry, newTags);
|
||||
}
|
||||
|
||||
Future<void> _applyTags(BuildContext context, AvesEntry targetEntry, Set<String> newTags) async {
|
||||
final currentTags = targetEntry.tags;
|
||||
if (newTags.length == currentTags.length && newTags.every(currentTags.contains)) return;
|
||||
|
||||
|
|
|
@ -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<EntryAction?> 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(
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
Loading…
Reference in a new issue