#601 tag editor: added save button, check modification when leaving, fixed adding placeholder filter

This commit is contained in:
Thibault Deckers 2023-04-28 19:35:38 +02:00
parent e4b0997f94
commit 6c11fd179e
6 changed files with 264 additions and 151 deletions

View file

@ -12,12 +12,14 @@ All notable changes to this project will be documented in this file.
### Changed ### Changed
- Info: editing tags now requires explicitly tapping the save button
- upgraded Flutter to stable v3.7.12 - upgraded Flutter to stable v3.7.12
### Fixed ### Fixed
- Video: switching to PiP when going home with gesture navigation - Video: switching to PiP when going home with gesture navigation
- Viewer: multi-page context update when removing burst entries - Viewer: multi-page context update when removing burst entries
- Info: editing tags with placeholders
- prevent editing item when Exif editing changes mime type - prevent editing item when Exif editing changes mime type
- parsing videos with skippable boxes in `meta` box - parsing videos with skippable boxes in `meta` box

View file

@ -46,6 +46,7 @@
"applyButtonLabel": "APPLY", "applyButtonLabel": "APPLY",
"deleteButtonLabel": "DELETE", "deleteButtonLabel": "DELETE",
"discardButtonLabel": "DISCARD",
"nextButtonLabel": "NEXT", "nextButtonLabel": "NEXT",
"showButtonLabel": "SHOW", "showButtonLabel": "SHOW",
"hideButtonLabel": "HIDE", "hideButtonLabel": "HIDE",

View file

@ -82,26 +82,27 @@ mixin EntryEditorMixin {
Future<Map<AvesEntry, Set<String>>?> selectTags(BuildContext context, Set<AvesEntry> entries) async { Future<Map<AvesEntry, Set<String>>?> selectTags(BuildContext context, Set<AvesEntry> entries) async {
if (entries.isEmpty) return null; 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()); 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( MaterialPageRoute(
settings: const RouteSettings(name: TagEditorPage.routeName), settings: const RouteSettings(name: TagEditorPage.routeName),
builder: (context) => TagEditorPage( 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 { await Future.forEach(filtersByEntry.entries, (kv) async {
final entry = kv.key; final entry = kv.key;
final filters = kv.value; 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 { Future<Set<String>> getTagsFromFilters(Set<CollectionFilter> filters, AvesEntry entry) async {

View file

@ -30,8 +30,9 @@ mixin VaultAwareMixin on FeedbackMixin {
localizedReason: context.l10n.authenticateToUnlockVault, localizedReason: context.l10n.authenticateToUnlockVault,
); );
} on PlatformException catch (e, stack) { } 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` // `auth_in_progress`: `Authentication in progress`
// `NotAvailable`: `Required security features not enabled`
await reportService.recordError(e, stack); await reportService.recordError(e, stack);
} }
} }

View file

@ -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/expandable_filter_row.dart';
import 'package:aves/widgets/common/extensions/build_context.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/identity/aves_filter_chip.dart';
import 'package:aves/widgets/dialogs/aves_dialog.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class TagEditorPage extends StatefulWidget { class TagEditorPage extends StatefulWidget {
static const routeName = '/info/tag_editor'; static const routeName = '/info/tag_editor';
final Map<AvesEntry, Set<CollectionFilter>> filtersByEntry; final Map<AvesEntry, Set<TagFilter>> tagsByEntry;
const TagEditorPage({ const TagEditorPage({
super.key, super.key,
required this.filtersByEntry, required this.tagsByEntry,
}); });
@override @override
@ -31,6 +32,7 @@ class _TagEditorPageState extends State<TagEditorPage> {
final TextEditingController _newTagTextController = TextEditingController(); final TextEditingController _newTagTextController = TextEditingController();
final FocusNode _newTagTextFocusNode = FocusNode(); final FocusNode _newTagTextFocusNode = FocusNode();
final ValueNotifier<String?> _expandedSectionNotifier = ValueNotifier(null); final ValueNotifier<String?> _expandedSectionNotifier = ValueNotifier(null);
late final Map<AvesEntry, Set<CollectionFilter>> filtersByEntry;
late final List<CollectionFilter> _topTags; late final List<CollectionFilter> _topTags;
final List<CollectionFilter> _userAddedFilters = []; final List<CollectionFilter> _userAddedFilters = [];
@ -41,11 +43,10 @@ class _TagEditorPageState extends State<TagEditorPage> {
PlaceholderFilter.place, PlaceholderFilter.place,
]; ];
Map<AvesEntry, Set<CollectionFilter>> get tagsByEntry => widget.filtersByEntry;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
filtersByEntry = widget.tagsByEntry.map((key, value) => MapEntry(key, value.cast<CollectionFilter>().toSet()));
_expandedSectionNotifier.value = settings.tagEditorExpandedSection; _expandedSectionNotifier.value = settings.tagEditorExpandedSection;
_expandedSectionNotifier.addListener(() => settings.tagEditorExpandedSection = _expandedSectionNotifier.value); _expandedSectionNotifier.addListener(() => settings.tagEditorExpandedSection = _expandedSectionNotifier.value);
_initTopTags(); _initTopTags();
@ -61,149 +62,176 @@ class _TagEditorPageState extends State<TagEditorPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = context.l10n; final l10n = context.l10n;
final showCount = tagsByEntry.length > 1; final showCount = filtersByEntry.length > 1;
final Map<CollectionFilter, int> entryCountByTag = {}; final Map<CollectionFilter, int> entryCountByTag = {};
tagsByEntry.entries.forEach((kv) { filtersByEntry.entries.forEach((kv) {
kv.value.forEach((tag) => entryCountByTag[tag] = (entryCountByTag[tag] ?? 0) + 1); kv.value.forEach((tag) => entryCountByTag[tag] = (entryCountByTag[tag] ?? 0) + 1);
}); });
List<MapEntry<CollectionFilter, int>> sortedTags = _sortCurrentTags(entryCountByTag); List<MapEntry<CollectionFilter, int>> sortedTags = _sortCurrentTags(entryCountByTag);
return AvesScaffold( return WillPopScope(
appBar: AppBar( onWillPop: () async {
title: Text(l10n.tagEditorPageTitle), if (!_isModified) return true;
actions: [
IconButton( final confirmed = await showDialog<bool>(
icon: const Icon(AIcons.reset), context: context,
onPressed: _reset, builder: (context) => AvesDialog(
tooltip: l10n.resetTooltip, content: Text(context.l10n.genericDangerWarningDialogMessage),
), actions: [
], const CancelButton(),
), TextButton(
body: SafeArea( onPressed: () => Navigator.maybeOf(context)?.pop(true),
child: ValueListenableBuilder<String?>( child: Text(context.l10n.discardButtonLabel),
valueListenable: _expandedSectionNotifier, ),
builder: (context, expandedSection, child) { ],
return ValueListenableBuilder<TextEditingValue>( ),
valueListenable: _newTagTextController, routeSettings: const RouteSettings(name: AvesDialog.warningRouteName),
builder: (context, value, child) { ) ??
final upQuery = value.text.trim().toUpperCase(); false;
bool containQuery(CollectionFilter v) => v.getLabel(context).toUpperCase().contains(upQuery); return confirmed;
final recentFilters = settings.recentTags.where(containQuery).toList(); },
final topTagFilters = _topTags.where(containQuery).toList(); child: AvesScaffold(
final placeholderFilters = _placeholders.where(containQuery).toList(); appBar: AppBar(
return ListView( title: Text(l10n.tagEditorPageTitle),
children: [ actions: [
Padding( IconButton(
padding: const EdgeInsetsDirectional.only(start: 8, end: 16), icon: const Icon(AIcons.reset),
child: Row( onPressed: _reset,
crossAxisAlignment: CrossAxisAlignment.end, tooltip: l10n.resetTooltip,
children: [ ),
Expanded( IconButton(
child: TextField( icon: const Icon(AIcons.apply),
controller: _newTagTextController, onPressed: () => Navigator.maybeOf(context)?.pop(filtersByEntry),
focusNode: _newTagTextFocusNode, tooltip: l10n.saveTooltip,
decoration: InputDecoration( ),
labelText: l10n.tagEditorPageNewTagFieldLabel, ],
),
body: SafeArea(
child: ValueListenableBuilder<String?>(
valueListenable: _expandedSectionNotifier,
builder: (context, expandedSection, child) {
return ValueListenableBuilder<TextEditingValue>(
valueListenable: _newTagTextController,
builder: (context, value, child) {
final upQuery = value.text.trim().toUpperCase();
bool containQuery(CollectionFilter v) => v.getLabel(context).toUpperCase().contains(upQuery);
final recentFilters = settings.recentTags.where(containQuery).toList();
final topTagFilters = _topTags.where(containQuery).toList();
final placeholderFilters = _placeholders.where(containQuery).toList();
return ListView(
children: [
Padding(
padding: const EdgeInsetsDirectional.only(start: 8, end: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: TextField(
controller: _newTagTextController,
focusNode: _newTagTextFocusNode,
decoration: InputDecoration(
labelText: l10n.tagEditorPageNewTagFieldLabel,
),
autofocus: true,
onSubmitted: (newTag) {
_addCustomTag(newTag);
_newTagTextFocusNode.requestFocus();
},
), ),
autofocus: true, ),
onSubmitted: (newTag) { ValueListenableBuilder<TextEditingValue>(
_addCustomTag(newTag); valueListenable: _newTagTextController,
_newTagTextFocusNode.requestFocus(); builder: (context, value, child) {
return IconButton(
icon: const Icon(AIcons.add),
onPressed: value.text.isEmpty ? null : () => _addCustomTag(_newTagTextController.text),
tooltip: l10n.tagEditorPageAddTagTooltip,
);
}, },
), ),
), Selector<Settings, bool>(
ValueListenableBuilder<TextEditingValue>( selector: (context, s) => s.tagEditorCurrentFilterSectionExpanded,
valueListenable: _newTagTextController, builder: (context, isExpanded, child) {
builder: (context, value, child) { return IconButton(
return IconButton( icon: Icon(isExpanded ? AIcons.collapse : AIcons.expand),
icon: const Icon(AIcons.add), onPressed: sortedTags.isEmpty ? null : () => settings.tagEditorCurrentFilterSectionExpanded = !isExpanded,
onPressed: value.text.isEmpty ? null : () => _addCustomTag(_newTagTextController.text), tooltip: isExpanded ? MaterialLocalizations.of(context).expandedIconTapHint : MaterialLocalizations.of(context).collapsedIconTapHint,
tooltip: l10n.tagEditorPageAddTagTooltip, );
); },
}, ),
), ],
Selector<Settings, bool>( ),
selector: (context, s) => s.tagEditorCurrentFilterSectionExpanded,
builder: (context, isExpanded, child) {
return IconButton(
icon: Icon(isExpanded ? AIcons.collapse : AIcons.expand),
onPressed: sortedTags.isEmpty ? null : () => settings.tagEditorCurrentFilterSectionExpanded = !isExpanded,
tooltip: isExpanded ? MaterialLocalizations.of(context).expandedIconTapHint : MaterialLocalizations.of(context).collapsedIconTapHint,
);
},
),
],
), ),
), Padding(
Padding( padding: const EdgeInsets.symmetric(vertical: 16),
padding: const EdgeInsets.symmetric(vertical: 16), child: AnimatedCrossFade(
child: AnimatedCrossFade( firstChild: ConstrainedBox(
firstChild: ConstrainedBox( constraints: const BoxConstraints(minHeight: AvesFilterChip.minChipHeight),
constraints: const BoxConstraints(minHeight: AvesFilterChip.minChipHeight), child: Center(
child: Center( child: Row(
child: Row( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, children: [
children: [ const Icon(AIcons.tagUntagged, color: untaggedColor),
const Icon(AIcons.tagUntagged, color: untaggedColor), const SizedBox(width: 8),
const SizedBox(width: 8), Text(
Text( l10n.filterNoTagLabel,
l10n.filterNoTagLabel, style: const TextStyle(color: untaggedColor),
style: const TextStyle(color: untaggedColor), ),
), ],
], ),
), ),
), ),
secondChild: ExpandableFilterRow(
filters: sortedTags.map((kv) => kv.key).toList(),
isExpanded: context.select<Settings, bool>((v) => v.tagEditorCurrentFilterSectionExpanded),
showGenericIcon: false,
leadingBuilder: showCount
? (filter) => _TagCount(
count: sortedTags.firstWhere((kv) => kv.key == filter).value,
)
: null,
onTap: (filter) {
if (filtersByEntry.keys.length > 1) {
// for multiple entries, set tag for all of them
filtersByEntry.forEach((entry, filters) => filters.add(filter));
setState(() {});
} else {
// for single entry, remove tag (like pressing on the remove icon)
_removeTag(filter);
}
},
onRemove: _removeTag,
onLongPress: null,
),
crossFadeState: sortedTags.isEmpty ? CrossFadeState.showFirst : CrossFadeState.showSecond,
duration: Durations.tagEditorTransition,
), ),
secondChild: ExpandableFilterRow(
filters: sortedTags.map((kv) => kv.key).toList(),
isExpanded: context.select<Settings, bool>((v) => v.tagEditorCurrentFilterSectionExpanded),
showGenericIcon: false,
leadingBuilder: showCount
? (filter) => _TagCount(
count: sortedTags.firstWhere((kv) => kv.key == filter).value,
)
: null,
onTap: (filter) {
if (tagsByEntry.keys.length > 1) {
// for multiple entries, set tag for all of them
tagsByEntry.forEach((entry, filters) => filters.add(filter));
setState(() {});
} else {
// for single entry, remove tag (like pressing on the remove icon)
_removeTag(filter);
}
},
onRemove: _removeTag,
onLongPress: null,
),
crossFadeState: sortedTags.isEmpty ? CrossFadeState.showFirst : CrossFadeState.showSecond,
duration: Durations.tagEditorTransition,
), ),
), const Divider(height: 0),
const Divider(height: 0), _FilterRow(
_FilterRow( title: l10n.statsTopTagsSectionTitle,
title: l10n.statsTopTagsSectionTitle, filters: topTagFilters,
filters: topTagFilters, expandedNotifier: _expandedSectionNotifier,
expandedNotifier: _expandedSectionNotifier, onTap: _addTag,
onTap: _addTag, ),
), _FilterRow(
_FilterRow( title: l10n.tagEditorSectionRecent,
title: l10n.tagEditorSectionRecent, filters: recentFilters,
filters: recentFilters, expandedNotifier: _expandedSectionNotifier,
expandedNotifier: _expandedSectionNotifier, onTap: _addTag,
onTap: _addTag, ),
), _FilterRow(
_FilterRow( title: l10n.tagEditorSectionPlaceholders,
title: l10n.tagEditorSectionPlaceholders, filters: placeholderFilters,
filters: placeholderFilters, expandedNotifier: _expandedSectionNotifier,
expandedNotifier: _expandedSectionNotifier, onTap: _addTag,
onTap: _addTag, ),
), ],
], );
); },
}, );
); },
}, ),
), ),
), ),
); );
@ -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() { void _reset() {
_userAddedFilters.clear(); _userAddedFilters.clear();
tagsByEntry.forEach((entry, tags) { filtersByEntry.forEach((entry, tags) {
final Set<TagFilter> originalFilters = entry.tags.map(TagFilter.new).toSet(); final originalFilters = entry.tags.map(TagFilter.new).toSet();
tags tags
..clear() ..clear()
..addAll(originalFilters); ..addAll(originalFilters);
@ -263,14 +300,14 @@ class _TagEditorPageState extends State<TagEditorPage> {
_userAddedFilters _userAddedFilters
..remove(filter) ..remove(filter)
..add(filter); ..add(filter);
tagsByEntry.forEach((entry, tags) => tags.add(filter)); filtersByEntry.forEach((entry, tags) => tags.add(filter));
_newTagTextController.clear(); _newTagTextController.clear();
setState(() {}); setState(() {});
} }
void _removeTag(CollectionFilter filter) { void _removeTag(CollectionFilter filter) {
_userAddedFilters.remove(filter); _userAddedFilters.remove(filter);
tagsByEntry.forEach((entry, filters) => filters.remove(filter)); filtersByEntry.forEach((entry, filters) => filters.remove(filter));
setState(() {}); setState(() {});
} }
} }

View file

@ -6,6 +6,7 @@
"timeMinutes", "timeMinutes",
"timeDays", "timeDays",
"focalLength", "focalLength",
"discardButtonLabel",
"pickTooltip", "pickTooltip",
"sourceStateLoading", "sourceStateLoading",
"sourceStateCataloguing", "sourceStateCataloguing",
@ -614,6 +615,7 @@
], ],
"ckb": [ "ckb": [
"discardButtonLabel",
"chipActionGoToPlacePage", "chipActionGoToPlacePage",
"chipActionShowCountryStates", "chipActionShowCountryStates",
"entryActionRotateCCW", "entryActionRotateCCW",
@ -1194,7 +1196,28 @@
"filePickerUseThisFolder" "filePickerUseThisFolder"
], ],
"cs": [
"discardButtonLabel"
],
"de": [
"discardButtonLabel"
],
"el": [
"discardButtonLabel"
],
"es": [
"discardButtonLabel"
],
"eu": [
"discardButtonLabel"
],
"fa": [ "fa": [
"discardButtonLabel",
"clearTooltip", "clearTooltip",
"chipActionGoToPlacePage", "chipActionGoToPlacePage",
"chipActionLock", "chipActionLock",
@ -1678,8 +1701,13 @@
"filePickerUseThisFolder" "filePickerUseThisFolder"
], ],
"fr": [
"discardButtonLabel"
],
"gl": [ "gl": [
"columnCount", "columnCount",
"discardButtonLabel",
"chipActionGoToPlacePage", "chipActionGoToPlacePage",
"chipActionLock", "chipActionLock",
"chipActionShowCountryStates", "chipActionShowCountryStates",
@ -2203,6 +2231,7 @@
"focalLength", "focalLength",
"applyButtonLabel", "applyButtonLabel",
"deleteButtonLabel", "deleteButtonLabel",
"discardButtonLabel",
"nextButtonLabel", "nextButtonLabel",
"showButtonLabel", "showButtonLabel",
"hideButtonLabel", "hideButtonLabel",
@ -2840,6 +2869,7 @@
], ],
"hi": [ "hi": [
"discardButtonLabel",
"resetTooltip", "resetTooltip",
"saveTooltip", "saveTooltip",
"pickTooltip", "pickTooltip",
@ -3464,12 +3494,22 @@
"filePickerUseThisFolder" "filePickerUseThisFolder"
], ],
"hu": [
"discardButtonLabel"
],
"id": [
"discardButtonLabel"
],
"it": [ "it": [
"discardButtonLabel",
"settingsCollectionBurstPatternsTile" "settingsCollectionBurstPatternsTile"
], ],
"ja": [ "ja": [
"columnCount", "columnCount",
"discardButtonLabel",
"chipActionShowCountryStates", "chipActionShowCountryStates",
"chipActionCreateVault", "chipActionCreateVault",
"chipActionConfigureVault", "chipActionConfigureVault",
@ -3499,8 +3539,13 @@
"statsTopStatesSectionTitle" "statsTopStatesSectionTitle"
], ],
"ko": [
"discardButtonLabel"
],
"lt": [ "lt": [
"columnCount", "columnCount",
"discardButtonLabel",
"chipActionGoToPlacePage", "chipActionGoToPlacePage",
"chipActionLock", "chipActionLock",
"chipActionShowCountryStates", "chipActionShowCountryStates",
@ -3556,6 +3601,7 @@
], ],
"nb": [ "nb": [
"discardButtonLabel",
"chipActionShowCountryStates", "chipActionShowCountryStates",
"viewerActionLock", "viewerActionLock",
"viewerActionUnlock", "viewerActionUnlock",
@ -3576,6 +3622,7 @@
"nl": [ "nl": [
"columnCount", "columnCount",
"discardButtonLabel",
"chipActionGoToPlacePage", "chipActionGoToPlacePage",
"chipActionLock", "chipActionLock",
"chipActionShowCountryStates", "chipActionShowCountryStates",
@ -3644,6 +3691,7 @@
"nn": [ "nn": [
"columnCount", "columnCount",
"discardButtonLabel",
"sourceStateCataloguing", "sourceStateCataloguing",
"chipActionGoToPlacePage", "chipActionGoToPlacePage",
"chipActionLock", "chipActionLock",
@ -3984,6 +4032,7 @@
"focalLength", "focalLength",
"applyButtonLabel", "applyButtonLabel",
"deleteButtonLabel", "deleteButtonLabel",
"discardButtonLabel",
"nextButtonLabel", "nextButtonLabel",
"continueButtonLabel", "continueButtonLabel",
"cancelTooltip", "cancelTooltip",
@ -4551,7 +4600,20 @@
"filePickerUseThisFolder" "filePickerUseThisFolder"
], ],
"pl": [
"discardButtonLabel"
],
"pt": [
"discardButtonLabel"
],
"ro": [
"discardButtonLabel"
],
"ru": [ "ru": [
"discardButtonLabel",
"chipActionShowCountryStates", "chipActionShowCountryStates",
"viewerActionLock", "viewerActionLock",
"viewerActionUnlock", "viewerActionUnlock",
@ -4574,6 +4636,7 @@
"itemCount", "itemCount",
"columnCount", "columnCount",
"timeSeconds", "timeSeconds",
"discardButtonLabel",
"chipActionGoToPlacePage", "chipActionGoToPlacePage",
"chipActionLock", "chipActionLock",
"chipActionShowCountryStates", "chipActionShowCountryStates",
@ -5011,6 +5074,7 @@
"timeDays", "timeDays",
"focalLength", "focalLength",
"applyButtonLabel", "applyButtonLabel",
"discardButtonLabel",
"chipActionGoToPlacePage", "chipActionGoToPlacePage",
"chipActionLock", "chipActionLock",
"chipActionShowCountryStates", "chipActionShowCountryStates",
@ -5372,6 +5436,7 @@
], ],
"tr": [ "tr": [
"discardButtonLabel",
"chipActionGoToPlacePage", "chipActionGoToPlacePage",
"chipActionLock", "chipActionLock",
"chipActionShowCountryStates", "chipActionShowCountryStates",
@ -5417,7 +5482,12 @@
"tagPlaceholderState" "tagPlaceholderState"
], ],
"uk": [
"discardButtonLabel"
],
"zh": [ "zh": [
"discardButtonLabel",
"chipActionGoToPlacePage", "chipActionGoToPlacePage",
"viewerActionLock", "viewerActionLock",
"viewerActionUnlock", "viewerActionUnlock",
@ -5469,6 +5539,7 @@
"zh_Hant": [ "zh_Hant": [
"columnCount", "columnCount",
"discardButtonLabel",
"chipActionGoToPlacePage", "chipActionGoToPlacePage",
"chipActionLock", "chipActionLock",
"chipActionShowCountryStates", "chipActionShowCountryStates",