#242 info: edit tags with dynamic placeholders for country / place
This commit is contained in:
parent
9fa977a7c1
commit
8ed8787c24
6 changed files with 232 additions and 50 deletions
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
95
lib/model/filters/placeholder.dart
Normal file
95
lib/model/filters/placeholder.dart
Normal file
|
@ -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<Object?> get props => [placeholder];
|
||||
|
||||
PlaceholderFilter._private(this.placeholder) : super(reversed: false) {
|
||||
switch (placeholder) {
|
||||
case _country:
|
||||
case _place:
|
||||
_icon = AIcons.location;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
factory PlaceholderFilter.fromMap(Map<String, dynamic> json) {
|
||||
return PlaceholderFilter._private(
|
||||
json['placeholder'],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toMap() => {
|
||||
'type': type,
|
||||
'placeholder': placeholder,
|
||||
};
|
||||
|
||||
Future<String?> 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';
|
||||
}
|
|
@ -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<Map<AvesEntry, Set<String>>?> selectTags(BuildContext context, Set<AvesEntry> 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 `<CollectionFilter>{...}` instead of `toSet()` to circumvent an implicit typing issue, as of Dart v2.18.2
|
||||
final filters = <CollectionFilter>{...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 = <AvesEntry, Set<String>>{};
|
||||
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));
|
||||
});
|
||||
|
||||
return tagsByEntry;
|
||||
}
|
||||
|
||||
|
|
|
@ -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<AvesEntry, Set<String>> tagsByEntry;
|
||||
final Map<AvesEntry, Set<CollectionFilter>> filtersByEntry;
|
||||
|
||||
const TagEditorPage({
|
||||
super.key,
|
||||
required this.tagsByEntry,
|
||||
required this.filtersByEntry,
|
||||
});
|
||||
|
||||
@override
|
||||
|
@ -31,14 +32,15 @@ class _TagEditorPageState extends State<TagEditorPage> {
|
|||
final TextEditingController _newTagTextController = TextEditingController();
|
||||
final FocusNode _newTagTextFocusNode = FocusNode();
|
||||
final ValueNotifier<String?> _expandedSectionNotifier = ValueNotifier(null);
|
||||
late final List<String> _topTags;
|
||||
late final List<CollectionFilter> _topTags;
|
||||
late final List<PlaceholderFilter> _placeholders = [PlaceholderFilter.country, PlaceholderFilter.place];
|
||||
|
||||
static final List<String> _recentTags = [];
|
||||
static final List<CollectionFilter> _recentTags = [];
|
||||
|
||||
static const Color untaggedColor = Colors.blueGrey;
|
||||
static const int tagHistoryCount = 10;
|
||||
|
||||
Map<AvesEntry, Set<String>> get tagsByEntry => widget.tagsByEntry;
|
||||
Map<AvesEntry, Set<CollectionFilter>> get tagsByEntry => widget.filtersByEntry;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -50,11 +52,11 @@ class _TagEditorPageState extends State<TagEditorPage> {
|
|||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final showCount = tagsByEntry.length > 1;
|
||||
final Map<String, int> entryCountByTag = {};
|
||||
final Map<CollectionFilter, int> entryCountByTag = {};
|
||||
tagsByEntry.entries.forEach((kv) {
|
||||
kv.value.forEach((tag) => entryCountByTag[tag] = (entryCountByTag[tag] ?? 0) + 1);
|
||||
});
|
||||
List<MapEntry<String, int>> sortedTags = _sortEntryCountByTag(entryCountByTag);
|
||||
List<MapEntry<CollectionFilter, int>> sortedTags = _sortEntryCountByTag(entryCountByTag);
|
||||
|
||||
return MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
|
@ -76,9 +78,10 @@ class _TagEditorPageState extends State<TagEditorPage> {
|
|||
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<TagEditorPage> {
|
|||
),
|
||||
autofocus: true,
|
||||
onSubmitted: (newTag) {
|
||||
_addTag(newTag);
|
||||
_addCustomTag(newTag);
|
||||
_newTagTextFocusNode.requestFocus();
|
||||
},
|
||||
),
|
||||
|
@ -105,7 +108,7 @@ class _TagEditorPageState extends State<TagEditorPage> {
|
|||
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<TagEditorPage> {
|
|||
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<TagEditorPage> {
|
|||
expandedNotifier: _expandedSectionNotifier,
|
||||
onTap: _addTag,
|
||||
),
|
||||
_FilterRow(
|
||||
title: l10n.tagEditorSectionPlaceholders,
|
||||
filters: placeholderFilters,
|
||||
expandedNotifier: _expandedSectionNotifier,
|
||||
onTap: _addTag,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
@ -184,49 +192,54 @@ class _TagEditorPageState extends State<TagEditorPage> {
|
|||
visibleEntries?.forEach((entry) {
|
||||
entry.tags.forEach((tag) => entryCountByTag[tag] = (entryCountByTag[tag] ?? 0) + 1);
|
||||
});
|
||||
List<MapEntry<String, int>> sortedTopTags = _sortEntryCountByTag(entryCountByTag);
|
||||
List<MapEntry<CollectionFilter, int>> sortedTopTags = _sortEntryCountByTag(entryCountByTag.map((key, value) => MapEntry(TagFilter(key), value)));
|
||||
_topTags = sortedTopTags.map((kv) => kv.key).toList();
|
||||
}
|
||||
|
||||
List<MapEntry<String, int>> _sortEntryCountByTag(Map<String, int> entryCountByTag) {
|
||||
List<MapEntry<CollectionFilter, int>> _sortEntryCountByTag(Map<CollectionFilter, int> 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<TagFilter> 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<TagFilter> filters;
|
||||
final List<CollectionFilter> filters;
|
||||
final ValueNotifier<String?> 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,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue