From 8ed8787c248872e05f6b996f8be33fc80e3aac53 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 26 Oct 2022 12:11:30 +0200 Subject: [PATCH] #242 info: edit tags with dynamic placeholders for country / place --- CHANGELOG.md | 1 + lib/l10n/app_en.arb | 4 + lib/model/filters/placeholder.dart | 95 +++++++++++++++++++ .../common/action_mixins/entry_editor.dart | 22 ++++- .../entry_editors/edit_tags_dialog.dart | 81 +++++++++------- untranslated.json | 79 ++++++++++++--- 6 files changed, 232 insertions(+), 50 deletions(-) create mode 100644 lib/model/filters/placeholder.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index b9caf9a0e..44c05057f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file. - Info: edit MP4 metadata (date / location / title / description / rating / tags / rotation) - Info: edit location by copying from other item +- Info: edit tags with dynamic placeholders for country / place - Widget: option to open collection on tap ### Changed diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index b550afe67..47e0028f8 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -868,6 +868,10 @@ "tagEditorPageNewTagFieldLabel": "New tag", "tagEditorPageAddTagTooltip": "Add tag", "tagEditorSectionRecent": "Recent", + "tagEditorSectionPlaceholders": "Placeholders", + + "tagPlaceholderCountry": "Country", + "tagPlaceholderPlace": "Place", "panoramaEnableSensorControl": "Enable sensor control", "panoramaDisableSensorControl": "Disable sensor control", diff --git a/lib/model/filters/placeholder.dart b/lib/model/filters/placeholder.dart new file mode 100644 index 000000000..a53ff307d --- /dev/null +++ b/lib/model/filters/placeholder.dart @@ -0,0 +1,95 @@ +import 'package:aves/model/entry.dart'; +import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:flutter/widgets.dart'; + +class PlaceholderFilter extends CollectionFilter { + static const type = 'placeholder'; + + static const _country = 'country'; + static const _place = 'place'; + + final String placeholder; + late final IconData _icon; + + static final country = PlaceholderFilter._private(_country); + static final place = PlaceholderFilter._private(_place); + + @override + List get props => [placeholder]; + + PlaceholderFilter._private(this.placeholder) : super(reversed: false) { + switch (placeholder) { + case _country: + case _place: + _icon = AIcons.location; + break; + } + } + + factory PlaceholderFilter.fromMap(Map json) { + return PlaceholderFilter._private( + json['placeholder'], + ); + } + + @override + Map toMap() => { + 'type': type, + 'placeholder': placeholder, + }; + + Future toTag(AvesEntry entry) async { + switch (placeholder) { + case _country: + case _place: + if (!entry.isCatalogued) { + await entry.catalog(background: false, force: false, persist: true); + } + if (!entry.hasGps) return null; + + if (!entry.hasFineAddress) { + await entry.locate(background: false, force: false, geocoderLocale: settings.appliedLocale); + } + final address = entry.addressDetails; + if (address == null) return null; + + if (placeholder == _country) return address.countryName; + if (placeholder == _place) return address.place; + break; + } + return null; + } + + @override + EntryFilter get positiveTest => (entry) => throw Exception('this is not a test'); + + @override + bool get exclusiveProp => false; + + @override + String get universalLabel => placeholder; + + @override + String getLabel(BuildContext context) { + switch (placeholder) { + case _country: + return context.l10n.tagPlaceholderCountry; + case _place: + return context.l10n.tagPlaceholderPlace; + default: + return placeholder; + } + } + + @override + Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(_icon, size: size); + + @override + String get category => type; + + @override + String get key => '$type-$placeholder'; +} diff --git a/lib/widgets/common/action_mixins/entry_editor.dart b/lib/widgets/common/action_mixins/entry_editor.dart index 612b0cacf..ec36e7f91 100644 --- a/lib/widgets/common/action_mixins/entry_editor.dart +++ b/lib/widgets/common/action_mixins/entry_editor.dart @@ -1,5 +1,8 @@ import 'package:aves/model/entry.dart'; import 'package:aves/model/entry_metadata_edition.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/metadata/date_modifier.dart'; import 'package:aves/model/metadata/enums/enums.dart'; import 'package:aves/model/source/collection_lens.dart'; @@ -74,17 +77,32 @@ mixin EntryEditorMixin { Future>?> selectTags(BuildContext context, Set entries) async { if (entries.isEmpty) return null; - final tagsByEntry = Map.fromEntries(entries.map((v) => MapEntry(v, v.tags.toSet()))); + final filtersByEntry = Map.fromEntries(entries.map((v) { + // use `{...}` instead of `toSet()` to circumvent an implicit typing issue, as of Dart v2.18.2 + final filters = {...v.tags.map(TagFilter.new)}; + return MapEntry(v, filters); + })); await Navigator.push( context, MaterialPageRoute( settings: const RouteSettings(name: TagEditorPage.routeName), builder: (context) => TagEditorPage( - tagsByEntry: tagsByEntry, + filtersByEntry: filtersByEntry, ), ), ); + final tagsByEntry = >{}; + 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)); + }); + return tagsByEntry; } diff --git a/lib/widgets/dialogs/entry_editors/edit_tags_dialog.dart b/lib/widgets/dialogs/entry_editors/edit_tags_dialog.dart index 7ce92e34c..6f91fadf4 100644 --- a/lib/widgets/dialogs/entry_editors/edit_tags_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/edit_tags_dialog.dart @@ -1,6 +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/source/collection_source.dart'; import 'package:aves/theme/durations.dart'; @@ -9,18 +11,17 @@ import 'package:aves/widgets/common/expandable_filter_row.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; -import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class TagEditorPage extends StatefulWidget { static const routeName = '/info/tag_editor'; - final Map> tagsByEntry; + final Map> filtersByEntry; const TagEditorPage({ super.key, - required this.tagsByEntry, + required this.filtersByEntry, }); @override @@ -31,14 +32,15 @@ class _TagEditorPageState extends State { final TextEditingController _newTagTextController = TextEditingController(); final FocusNode _newTagTextFocusNode = FocusNode(); final ValueNotifier _expandedSectionNotifier = ValueNotifier(null); - late final List _topTags; + late final List _topTags; + late final List _placeholders = [PlaceholderFilter.country, PlaceholderFilter.place]; - static final List _recentTags = []; + static final List _recentTags = []; static const Color untaggedColor = Colors.blueGrey; static const int tagHistoryCount = 10; - Map> get tagsByEntry => widget.tagsByEntry; + Map> get tagsByEntry => widget.filtersByEntry; @override void initState() { @@ -50,11 +52,11 @@ class _TagEditorPageState extends State { Widget build(BuildContext context) { final l10n = context.l10n; final showCount = tagsByEntry.length > 1; - final Map entryCountByTag = {}; + final Map entryCountByTag = {}; tagsByEntry.entries.forEach((kv) { kv.value.forEach((tag) => entryCountByTag[tag] = (entryCountByTag[tag] ?? 0) + 1); }); - List> sortedTags = _sortEntryCountByTag(entryCountByTag); + List> sortedTags = _sortEntryCountByTag(entryCountByTag); return MediaQueryDataProvider( child: Scaffold( @@ -76,9 +78,10 @@ class _TagEditorPageState extends State { valueListenable: _newTagTextController, builder: (context, value, child) { final upQuery = value.text.trim().toUpperCase(); - bool containQuery(String s) => s.toUpperCase().contains(upQuery); - final recentFilters = _recentTags.where(containQuery).map(TagFilter.new).toList(); - final topTagFilters = _topTags.where(containQuery).map(TagFilter.new).toList(); + bool containQuery(CollectionFilter v) => v.getLabel(context).toUpperCase().contains(upQuery); + final recentFilters = _recentTags.where(containQuery).toList(); + final topTagFilters = _topTags.where(containQuery).toList(); + final placeholderFilters = _placeholders.where(containQuery).toList(); return ListView( children: [ Padding( @@ -95,7 +98,7 @@ class _TagEditorPageState extends State { ), autofocus: true, onSubmitted: (newTag) { - _addTag(newTag); + _addCustomTag(newTag); _newTagTextFocusNode.requestFocus(); }, ), @@ -105,7 +108,7 @@ class _TagEditorPageState extends State { builder: (context, value, child) { return IconButton( icon: const Icon(AIcons.add), - onPressed: value.text.isEmpty ? null : () => _addTag(_newTagTextController.text), + onPressed: value.text.isEmpty ? null : () => _addCustomTag(_newTagTextController.text), tooltip: l10n.tagEditorPageAddTagTooltip, ); }, @@ -138,13 +141,12 @@ class _TagEditorPageState extends State { spacing: 8, runSpacing: 8, children: sortedTags.map((kv) { - final tag = kv.key; return AvesFilterChip( - filter: TagFilter(tag), + filter: kv.key, removable: true, showGenericIcon: false, leadingOverride: showCount ? _TagCount(count: kv.value) : null, - onTap: (filter) => _removeTag(tag), + onTap: _removeTag, onLongPress: null, ); }).toList(), @@ -167,6 +169,12 @@ class _TagEditorPageState extends State { expandedNotifier: _expandedSectionNotifier, onTap: _addTag, ), + _FilterRow( + title: l10n.tagEditorSectionPlaceholders, + filters: placeholderFilters, + expandedNotifier: _expandedSectionNotifier, + onTap: _addTag, + ), ], ); }, @@ -184,49 +192,54 @@ class _TagEditorPageState extends State { visibleEntries?.forEach((entry) { entry.tags.forEach((tag) => entryCountByTag[tag] = (entryCountByTag[tag] ?? 0) + 1); }); - List> sortedTopTags = _sortEntryCountByTag(entryCountByTag); + List> sortedTopTags = _sortEntryCountByTag(entryCountByTag.map((key, value) => MapEntry(TagFilter(key), value))); _topTags = sortedTopTags.map((kv) => kv.key).toList(); } - List> _sortEntryCountByTag(Map entryCountByTag) { + List> _sortEntryCountByTag(Map entryCountByTag) { return entryCountByTag.entries.toList() ..sort((kv1, kv2) { final c = kv2.value.compareTo(kv1.value); - return c != 0 ? c : compareAsciiUpperCaseNatural(kv1.key, kv2.key); + return c != 0 ? c : kv1.key.compareTo(kv2.key); }); } void _reset() { setState(() => tagsByEntry.forEach((entry, tags) { + final Set originalFilters = entry.tags.map(TagFilter.new).toSet(); tags ..clear() - ..addAll(entry.tags); + ..addAll(originalFilters); })); } - void _addTag(String newTag) { + void _addCustomTag(String newTag) { if (newTag.isNotEmpty) { - setState(() { - _recentTags - ..remove(newTag) - ..insert(0, newTag) - ..removeRange(min(tagHistoryCount, _recentTags.length), _recentTags.length); - tagsByEntry.forEach((entry, tags) => tags.add(newTag)); - }); - _newTagTextController.clear(); + _addTag(TagFilter(newTag)); } } - void _removeTag(String tag) { - setState(() => tagsByEntry.forEach((entry, tags) => tags.remove(tag))); + 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)); + }); + _newTagTextController.clear(); + } + + void _removeTag(CollectionFilter filter) { + setState(() => tagsByEntry.forEach((entry, filters) => filters.remove(filter))); } } class _FilterRow extends StatelessWidget { final String title; - final List filters; + final List filters; final ValueNotifier expandedNotifier; - final void Function(String tag) onTap; + final void Function(CollectionFilter filter) onTap; const _FilterRow({ required this.title, @@ -244,7 +257,7 @@ class _FilterRow extends StatelessWidget { filters: filters, expandedNotifier: expandedNotifier, showGenericIcon: false, - onTap: (filter) => onTap((filter as TagFilter).tag), + onTap: onTap, onLongPress: null, ); } diff --git a/untranslated.json b/untranslated.json index e00e4cc87..1e893420e 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,17 +1,26 @@ { "de": [ - "editEntryLocationDialogSetCustom" + "editEntryLocationDialogSetCustom", + "tagEditorSectionPlaceholders", + "tagPlaceholderCountry", + "tagPlaceholderPlace" ], "el": [ - "editEntryLocationDialogSetCustom" + "editEntryLocationDialogSetCustom", + "tagEditorSectionPlaceholders", + "tagPlaceholderCountry", + "tagPlaceholderPlace" ], "es": [ "widgetOpenPageHome", "widgetOpenPageCollection", "widgetOpenPageViewer", - "editEntryLocationDialogSetCustom" + "editEntryLocationDialogSetCustom", + "tagEditorSectionPlaceholders", + "tagPlaceholderCountry", + "tagPlaceholderPlace" ], "fa": [ @@ -582,6 +591,9 @@ "tagEditorPageNewTagFieldLabel", "tagEditorPageAddTagTooltip", "tagEditorSectionRecent", + "tagEditorSectionPlaceholders", + "tagPlaceholderCountry", + "tagPlaceholderPlace", "panoramaEnableSensorControl", "panoramaDisableSensorControl", "sourceViewerPageTitle", @@ -593,7 +605,10 @@ ], "fr": [ - "editEntryLocationDialogSetCustom" + "editEntryLocationDialogSetCustom", + "tagEditorSectionPlaceholders", + "tagPlaceholderCountry", + "tagPlaceholderPlace" ], "gl": [ @@ -1031,6 +1046,9 @@ "tagEditorPageNewTagFieldLabel", "tagEditorPageAddTagTooltip", "tagEditorSectionRecent", + "tagEditorSectionPlaceholders", + "tagPlaceholderCountry", + "tagPlaceholderPlace", "panoramaEnableSensorControl", "panoramaDisableSensorControl", "sourceViewerPageTitle", @@ -1057,11 +1075,17 @@ "settingsSlideshowAnimatedZoomEffect", "settingsWidgetOpenPage", "statsTopAlbumsSectionTitle", - "wallpaperUseScrollEffect" + "wallpaperUseScrollEffect", + "tagEditorSectionPlaceholders", + "tagPlaceholderCountry", + "tagPlaceholderPlace" ], "it": [ - "editEntryLocationDialogSetCustom" + "editEntryLocationDialogSetCustom", + "tagEditorSectionPlaceholders", + "tagPlaceholderCountry", + "tagPlaceholderPlace" ], "ja": [ @@ -1097,11 +1121,17 @@ "settingsWidgetOpenPage", "statsTopAlbumsSectionTitle", "viewerInfoLabelDescription", - "wallpaperUseScrollEffect" + "wallpaperUseScrollEffect", + "tagEditorSectionPlaceholders", + "tagPlaceholderCountry", + "tagPlaceholderPlace" ], "ko": [ - "editEntryLocationDialogSetCustom" + "editEntryLocationDialogSetCustom", + "tagEditorSectionPlaceholders", + "tagPlaceholderCountry", + "tagPlaceholderPlace" ], "nb": [ @@ -1210,7 +1240,10 @@ "mapEmptyRegion", "viewerInfoOpenEmbeddedFailureFeedback", "viewerInfoSearchEmpty", - "wallpaperUseScrollEffect" + "wallpaperUseScrollEffect", + "tagEditorSectionPlaceholders", + "tagPlaceholderCountry", + "tagPlaceholderPlace" ], "nl": [ @@ -1219,7 +1252,10 @@ "durationDialogSeconds", "editEntryLocationDialogSetCustom", "aboutLinkPolicy", - "policyPageTitle" + "policyPageTitle", + "tagEditorSectionPlaceholders", + "tagPlaceholderCountry", + "tagPlaceholderPlace" ], "pl": [ @@ -1697,6 +1733,9 @@ "tagEditorPageNewTagFieldLabel", "tagEditorPageAddTagTooltip", "tagEditorSectionRecent", + "tagEditorSectionPlaceholders", + "tagPlaceholderCountry", + "tagPlaceholderPlace", "panoramaEnableSensorControl", "panoramaDisableSensorControl", "sourceViewerPageTitle", @@ -1708,11 +1747,17 @@ ], "pt": [ - "editEntryLocationDialogSetCustom" + "editEntryLocationDialogSetCustom", + "tagEditorSectionPlaceholders", + "tagPlaceholderCountry", + "tagPlaceholderPlace" ], "ru": [ - "editEntryLocationDialogSetCustom" + "editEntryLocationDialogSetCustom", + "tagEditorSectionPlaceholders", + "tagPlaceholderCountry", + "tagPlaceholderPlace" ], "tr": [ @@ -1774,10 +1819,16 @@ "statsTopAlbumsSectionTitle", "viewerSetWallpaperButtonLabel", "viewerInfoLabelDescription", - "wallpaperUseScrollEffect" + "wallpaperUseScrollEffect", + "tagEditorSectionPlaceholders", + "tagPlaceholderCountry", + "tagPlaceholderPlace" ], "zh": [ - "editEntryLocationDialogSetCustom" + "editEntryLocationDialogSetCustom", + "tagEditorSectionPlaceholders", + "tagPlaceholderCountry", + "tagPlaceholderPlace" ] }