#601 tag editor: added save button, check modification when leaving, fixed adding placeholder filter
This commit is contained in:
parent
e4b0997f94
commit
6c11fd179e
6 changed files with 264 additions and 151 deletions
|
@ -12,12 +12,14 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
### Changed
|
||||
|
||||
- Info: editing tags now requires explicitly tapping the save button
|
||||
- upgraded Flutter to stable v3.7.12
|
||||
|
||||
### Fixed
|
||||
|
||||
- Video: switching to PiP when going home with gesture navigation
|
||||
- Viewer: multi-page context update when removing burst entries
|
||||
- Info: editing tags with placeholders
|
||||
- prevent editing item when Exif editing changes mime type
|
||||
- parsing videos with skippable boxes in `meta` box
|
||||
|
||||
|
|
|
@ -46,6 +46,7 @@
|
|||
|
||||
"applyButtonLabel": "APPLY",
|
||||
"deleteButtonLabel": "DELETE",
|
||||
"discardButtonLabel": "DISCARD",
|
||||
"nextButtonLabel": "NEXT",
|
||||
"showButtonLabel": "SHOW",
|
||||
"hideButtonLabel": "HIDE",
|
||||
|
|
|
@ -82,26 +82,27 @@ mixin EntryEditorMixin {
|
|||
Future<Map<AvesEntry, Set<String>>?> selectTags(BuildContext context, Set<AvesEntry> entries) async {
|
||||
if (entries.isEmpty) return null;
|
||||
|
||||
final filtersByEntry = Map.fromEntries(entries.map((v) {
|
||||
final oldTagsByEntry = Map.fromEntries(entries.map((v) {
|
||||
return MapEntry(v, v.tags.map(TagFilter.new).toSet());
|
||||
}));
|
||||
await Navigator.maybeOf(context)?.push(
|
||||
final filtersByEntry = await Navigator.maybeOf(context)?.push<Map<AvesEntry, Set<CollectionFilter>>>(
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: TagEditorPage.routeName),
|
||||
builder: (context) => TagEditorPage(
|
||||
filtersByEntry: filtersByEntry,
|
||||
tagsByEntry: oldTagsByEntry,
|
||||
),
|
||||
),
|
||||
);
|
||||
) ??
|
||||
oldTagsByEntry;
|
||||
|
||||
final tagsByEntry = <AvesEntry, Set<String>>{};
|
||||
final newTagsByEntry = <AvesEntry, Set<String>>{};
|
||||
await Future.forEach(filtersByEntry.entries, (kv) async {
|
||||
final entry = kv.key;
|
||||
final filters = kv.value;
|
||||
tagsByEntry[entry] = await getTagsFromFilters(filters, entry);
|
||||
newTagsByEntry[entry] = await getTagsFromFilters(filters, entry);
|
||||
});
|
||||
|
||||
return tagsByEntry;
|
||||
return newTagsByEntry;
|
||||
}
|
||||
|
||||
Future<Set<String>> getTagsFromFilters(Set<CollectionFilter> filters, AvesEntry entry) async {
|
||||
|
|
|
@ -30,8 +30,9 @@ mixin VaultAwareMixin on FeedbackMixin {
|
|||
localizedReason: context.l10n.authenticateToUnlockVault,
|
||||
);
|
||||
} on PlatformException catch (e, stack) {
|
||||
if (e.code != 'auth_in_progress') {
|
||||
if (!{'auth_in_progress', 'NotAvailable'}.contains(e.code)) {
|
||||
// `auth_in_progress`: `Authentication in progress`
|
||||
// `NotAvailable`: `Required security features not enabled`
|
||||
await reportService.recordError(e, stack);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,17 +10,18 @@ import 'package:aves/widgets/common/basic/scaffold.dart';
|
|||
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/dialogs/aves_dialog.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class TagEditorPage extends StatefulWidget {
|
||||
static const routeName = '/info/tag_editor';
|
||||
|
||||
final Map<AvesEntry, Set<CollectionFilter>> filtersByEntry;
|
||||
final Map<AvesEntry, Set<TagFilter>> tagsByEntry;
|
||||
|
||||
const TagEditorPage({
|
||||
super.key,
|
||||
required this.filtersByEntry,
|
||||
required this.tagsByEntry,
|
||||
});
|
||||
|
||||
@override
|
||||
|
@ -31,6 +32,7 @@ class _TagEditorPageState extends State<TagEditorPage> {
|
|||
final TextEditingController _newTagTextController = TextEditingController();
|
||||
final FocusNode _newTagTextFocusNode = FocusNode();
|
||||
final ValueNotifier<String?> _expandedSectionNotifier = ValueNotifier(null);
|
||||
late final Map<AvesEntry, Set<CollectionFilter>> filtersByEntry;
|
||||
late final List<CollectionFilter> _topTags;
|
||||
final List<CollectionFilter> _userAddedFilters = [];
|
||||
|
||||
|
@ -41,11 +43,10 @@ class _TagEditorPageState extends State<TagEditorPage> {
|
|||
PlaceholderFilter.place,
|
||||
];
|
||||
|
||||
Map<AvesEntry, Set<CollectionFilter>> get tagsByEntry => widget.filtersByEntry;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
filtersByEntry = widget.tagsByEntry.map((key, value) => MapEntry(key, value.cast<CollectionFilter>().toSet()));
|
||||
_expandedSectionNotifier.value = settings.tagEditorExpandedSection;
|
||||
_expandedSectionNotifier.addListener(() => settings.tagEditorExpandedSection = _expandedSectionNotifier.value);
|
||||
_initTopTags();
|
||||
|
@ -61,14 +62,35 @@ class _TagEditorPageState extends State<TagEditorPage> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final showCount = tagsByEntry.length > 1;
|
||||
final showCount = filtersByEntry.length > 1;
|
||||
final Map<CollectionFilter, int> entryCountByTag = {};
|
||||
tagsByEntry.entries.forEach((kv) {
|
||||
filtersByEntry.entries.forEach((kv) {
|
||||
kv.value.forEach((tag) => entryCountByTag[tag] = (entryCountByTag[tag] ?? 0) + 1);
|
||||
});
|
||||
List<MapEntry<CollectionFilter, int>> sortedTags = _sortCurrentTags(entryCountByTag);
|
||||
|
||||
return AvesScaffold(
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
if (!_isModified) return true;
|
||||
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AvesDialog(
|
||||
content: Text(context.l10n.genericDangerWarningDialogMessage),
|
||||
actions: [
|
||||
const CancelButton(),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.maybeOf(context)?.pop(true),
|
||||
child: Text(context.l10n.discardButtonLabel),
|
||||
),
|
||||
],
|
||||
),
|
||||
routeSettings: const RouteSettings(name: AvesDialog.warningRouteName),
|
||||
) ??
|
||||
false;
|
||||
return confirmed;
|
||||
},
|
||||
child: AvesScaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(l10n.tagEditorPageTitle),
|
||||
actions: [
|
||||
|
@ -77,6 +99,11 @@ class _TagEditorPageState extends State<TagEditorPage> {
|
|||
onPressed: _reset,
|
||||
tooltip: l10n.resetTooltip,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(AIcons.apply),
|
||||
onPressed: () => Navigator.maybeOf(context)?.pop(filtersByEntry),
|
||||
tooltip: l10n.saveTooltip,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
|
@ -164,9 +191,9 @@ class _TagEditorPageState extends State<TagEditorPage> {
|
|||
)
|
||||
: null,
|
||||
onTap: (filter) {
|
||||
if (tagsByEntry.keys.length > 1) {
|
||||
if (filtersByEntry.keys.length > 1) {
|
||||
// for multiple entries, set tag for all of them
|
||||
tagsByEntry.forEach((entry, filters) => filters.add(filter));
|
||||
filtersByEntry.forEach((entry, filters) => filters.add(filter));
|
||||
setState(() {});
|
||||
} else {
|
||||
// for single entry, remove tag (like pressing on the remove icon)
|
||||
|
@ -206,6 +233,7 @@ class _TagEditorPageState extends State<TagEditorPage> {
|
|||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -239,10 +267,19 @@ class _TagEditorPageState extends State<TagEditorPage> {
|
|||
});
|
||||
}
|
||||
|
||||
bool get _isModified {
|
||||
for (final kv in filtersByEntry.entries) {
|
||||
final oldFilters = kv.key.tags.map(TagFilter.new).toSet();
|
||||
final newFilters = kv.value;
|
||||
if (newFilters.length != oldFilters.length || !newFilters.containsAll(oldFilters)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void _reset() {
|
||||
_userAddedFilters.clear();
|
||||
tagsByEntry.forEach((entry, tags) {
|
||||
final Set<TagFilter> originalFilters = entry.tags.map(TagFilter.new).toSet();
|
||||
filtersByEntry.forEach((entry, tags) {
|
||||
final originalFilters = entry.tags.map(TagFilter.new).toSet();
|
||||
tags
|
||||
..clear()
|
||||
..addAll(originalFilters);
|
||||
|
@ -263,14 +300,14 @@ class _TagEditorPageState extends State<TagEditorPage> {
|
|||
_userAddedFilters
|
||||
..remove(filter)
|
||||
..add(filter);
|
||||
tagsByEntry.forEach((entry, tags) => tags.add(filter));
|
||||
filtersByEntry.forEach((entry, tags) => tags.add(filter));
|
||||
_newTagTextController.clear();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _removeTag(CollectionFilter filter) {
|
||||
_userAddedFilters.remove(filter);
|
||||
tagsByEntry.forEach((entry, filters) => filters.remove(filter));
|
||||
filtersByEntry.forEach((entry, filters) => filters.remove(filter));
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
"timeMinutes",
|
||||
"timeDays",
|
||||
"focalLength",
|
||||
"discardButtonLabel",
|
||||
"pickTooltip",
|
||||
"sourceStateLoading",
|
||||
"sourceStateCataloguing",
|
||||
|
@ -614,6 +615,7 @@
|
|||
],
|
||||
|
||||
"ckb": [
|
||||
"discardButtonLabel",
|
||||
"chipActionGoToPlacePage",
|
||||
"chipActionShowCountryStates",
|
||||
"entryActionRotateCCW",
|
||||
|
@ -1194,7 +1196,28 @@
|
|||
"filePickerUseThisFolder"
|
||||
],
|
||||
|
||||
"cs": [
|
||||
"discardButtonLabel"
|
||||
],
|
||||
|
||||
"de": [
|
||||
"discardButtonLabel"
|
||||
],
|
||||
|
||||
"el": [
|
||||
"discardButtonLabel"
|
||||
],
|
||||
|
||||
"es": [
|
||||
"discardButtonLabel"
|
||||
],
|
||||
|
||||
"eu": [
|
||||
"discardButtonLabel"
|
||||
],
|
||||
|
||||
"fa": [
|
||||
"discardButtonLabel",
|
||||
"clearTooltip",
|
||||
"chipActionGoToPlacePage",
|
||||
"chipActionLock",
|
||||
|
@ -1678,8 +1701,13 @@
|
|||
"filePickerUseThisFolder"
|
||||
],
|
||||
|
||||
"fr": [
|
||||
"discardButtonLabel"
|
||||
],
|
||||
|
||||
"gl": [
|
||||
"columnCount",
|
||||
"discardButtonLabel",
|
||||
"chipActionGoToPlacePage",
|
||||
"chipActionLock",
|
||||
"chipActionShowCountryStates",
|
||||
|
@ -2203,6 +2231,7 @@
|
|||
"focalLength",
|
||||
"applyButtonLabel",
|
||||
"deleteButtonLabel",
|
||||
"discardButtonLabel",
|
||||
"nextButtonLabel",
|
||||
"showButtonLabel",
|
||||
"hideButtonLabel",
|
||||
|
@ -2840,6 +2869,7 @@
|
|||
],
|
||||
|
||||
"hi": [
|
||||
"discardButtonLabel",
|
||||
"resetTooltip",
|
||||
"saveTooltip",
|
||||
"pickTooltip",
|
||||
|
@ -3464,12 +3494,22 @@
|
|||
"filePickerUseThisFolder"
|
||||
],
|
||||
|
||||
"hu": [
|
||||
"discardButtonLabel"
|
||||
],
|
||||
|
||||
"id": [
|
||||
"discardButtonLabel"
|
||||
],
|
||||
|
||||
"it": [
|
||||
"discardButtonLabel",
|
||||
"settingsCollectionBurstPatternsTile"
|
||||
],
|
||||
|
||||
"ja": [
|
||||
"columnCount",
|
||||
"discardButtonLabel",
|
||||
"chipActionShowCountryStates",
|
||||
"chipActionCreateVault",
|
||||
"chipActionConfigureVault",
|
||||
|
@ -3499,8 +3539,13 @@
|
|||
"statsTopStatesSectionTitle"
|
||||
],
|
||||
|
||||
"ko": [
|
||||
"discardButtonLabel"
|
||||
],
|
||||
|
||||
"lt": [
|
||||
"columnCount",
|
||||
"discardButtonLabel",
|
||||
"chipActionGoToPlacePage",
|
||||
"chipActionLock",
|
||||
"chipActionShowCountryStates",
|
||||
|
@ -3556,6 +3601,7 @@
|
|||
],
|
||||
|
||||
"nb": [
|
||||
"discardButtonLabel",
|
||||
"chipActionShowCountryStates",
|
||||
"viewerActionLock",
|
||||
"viewerActionUnlock",
|
||||
|
@ -3576,6 +3622,7 @@
|
|||
|
||||
"nl": [
|
||||
"columnCount",
|
||||
"discardButtonLabel",
|
||||
"chipActionGoToPlacePage",
|
||||
"chipActionLock",
|
||||
"chipActionShowCountryStates",
|
||||
|
@ -3644,6 +3691,7 @@
|
|||
|
||||
"nn": [
|
||||
"columnCount",
|
||||
"discardButtonLabel",
|
||||
"sourceStateCataloguing",
|
||||
"chipActionGoToPlacePage",
|
||||
"chipActionLock",
|
||||
|
@ -3984,6 +4032,7 @@
|
|||
"focalLength",
|
||||
"applyButtonLabel",
|
||||
"deleteButtonLabel",
|
||||
"discardButtonLabel",
|
||||
"nextButtonLabel",
|
||||
"continueButtonLabel",
|
||||
"cancelTooltip",
|
||||
|
@ -4551,7 +4600,20 @@
|
|||
"filePickerUseThisFolder"
|
||||
],
|
||||
|
||||
"pl": [
|
||||
"discardButtonLabel"
|
||||
],
|
||||
|
||||
"pt": [
|
||||
"discardButtonLabel"
|
||||
],
|
||||
|
||||
"ro": [
|
||||
"discardButtonLabel"
|
||||
],
|
||||
|
||||
"ru": [
|
||||
"discardButtonLabel",
|
||||
"chipActionShowCountryStates",
|
||||
"viewerActionLock",
|
||||
"viewerActionUnlock",
|
||||
|
@ -4574,6 +4636,7 @@
|
|||
"itemCount",
|
||||
"columnCount",
|
||||
"timeSeconds",
|
||||
"discardButtonLabel",
|
||||
"chipActionGoToPlacePage",
|
||||
"chipActionLock",
|
||||
"chipActionShowCountryStates",
|
||||
|
@ -5011,6 +5074,7 @@
|
|||
"timeDays",
|
||||
"focalLength",
|
||||
"applyButtonLabel",
|
||||
"discardButtonLabel",
|
||||
"chipActionGoToPlacePage",
|
||||
"chipActionLock",
|
||||
"chipActionShowCountryStates",
|
||||
|
@ -5372,6 +5436,7 @@
|
|||
],
|
||||
|
||||
"tr": [
|
||||
"discardButtonLabel",
|
||||
"chipActionGoToPlacePage",
|
||||
"chipActionLock",
|
||||
"chipActionShowCountryStates",
|
||||
|
@ -5417,7 +5482,12 @@
|
|||
"tagPlaceholderState"
|
||||
],
|
||||
|
||||
"uk": [
|
||||
"discardButtonLabel"
|
||||
],
|
||||
|
||||
"zh": [
|
||||
"discardButtonLabel",
|
||||
"chipActionGoToPlacePage",
|
||||
"viewerActionLock",
|
||||
"viewerActionUnlock",
|
||||
|
@ -5469,6 +5539,7 @@
|
|||
|
||||
"zh_Hant": [
|
||||
"columnCount",
|
||||
"discardButtonLabel",
|
||||
"chipActionGoToPlacePage",
|
||||
"chipActionLock",
|
||||
"chipActionShowCountryStates",
|
||||
|
|
Loading…
Reference in a new issue