#2 edit location in bulk

This commit is contained in:
Thibault Deckers 2022-01-27 09:19:28 +09:00
parent e0f45f03c1
commit b97c51e541
13 changed files with 85 additions and 30 deletions

View file

@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.
### Added
- Info: edit location of JPG/PNG/WEBP/DNG images via Exif
- Viewer: resize option when exporting
- Settings: export/import covers & favourites along with settings
- Portuguese translation (thanks Jonatas De Almeida Barros)

View file

@ -74,6 +74,7 @@
"videoActionSettings": "Préférences",
"entryInfoActionEditDate": "Modifier la date",
"entryInfoActionEditLocation": "Modifier le lieu",
"entryInfoActionEditRating": "Modifier la notation",
"entryInfoActionEditTags": "Modifier les libellés",
"entryInfoActionRemoveMetadata": "Retirer les métadonnées",
@ -195,6 +196,13 @@
"editEntryDateDialogHours": "Heures",
"editEntryDateDialogMinutes": "Minutes",
"editEntryLocationDialogTitle": "Lieu",
"editEntryLocationDialogChooseOnMapTooltip": "Sélectionner sur la carte",
"editEntryLocationDialogLatitude": "Latitude",
"editEntryLocationDialogLongitude": "Longitude",
"locationPickerUseThisLocationButton": "Utiliser ce lieu",
"editEntryRatingDialogTitle": "Notation",
"removeEntryMetadataDialogTitle": "Retrait de métadonnées",

View file

@ -74,6 +74,7 @@
"videoActionSettings": "설정",
"entryInfoActionEditDate": "날짜 및 시간 수정",
"entryInfoActionEditLocation": "위치 수정",
"entryInfoActionEditRating": "별점 수정",
"entryInfoActionEditTags": "태그 수정",
"entryInfoActionRemoveMetadata": "메타데이터 삭제",
@ -195,6 +196,13 @@
"editEntryDateDialogHours": "시간",
"editEntryDateDialogMinutes": "분",
"editEntryLocationDialogTitle": "위치",
"editEntryLocationDialogChooseOnMapTooltip": "지도에서 선택",
"editEntryLocationDialogLatitude": "위도",
"editEntryLocationDialogLongitude": "경도",
"locationPickerUseThisLocationButton": "이 위치 사용",
"editEntryRatingDialogTitle": "별점",
"removeEntryMetadataDialogTitle": "메타데이터 삭제",

View file

@ -26,6 +26,7 @@ enum EntrySetAction {
rotateCW,
flip,
editDate,
editLocation,
editRating,
editTags,
removeMetadata,
@ -107,6 +108,8 @@ extension ExtraEntrySetAction on EntrySetAction {
return context.l10n.entryActionFlip;
case EntrySetAction.editDate:
return context.l10n.entryInfoActionEditDate;
case EntrySetAction.editLocation:
return context.l10n.entryInfoActionEditLocation;
case EntrySetAction.editRating:
return context.l10n.entryInfoActionEditRating;
case EntrySetAction.editTags:
@ -166,6 +169,8 @@ extension ExtraEntrySetAction on EntrySetAction {
return AIcons.flip;
case EntrySetAction.editDate:
return AIcons.date;
case EntrySetAction.editLocation:
return AIcons.location;
case EntrySetAction.editRating:
return AIcons.editRating;
case EntrySetAction.editTags:

View file

@ -30,6 +30,12 @@ mixin LocationMixin on SourceBase {
Future<void> locateEntries(AnalysisController controller, Set<AvesEntry> candidateEntries) async {
await _locateCountries(controller, candidateEntries);
await _locatePlaces(controller, candidateEntries);
final unlocatedIds = candidateEntries.where((entry) => !entry.hasGps).map((entry) => entry.contentId).whereNotNull().toSet();
if (unlocatedIds.isNotEmpty) {
await metadataDb.removeIds(unlocatedIds, dataTypes: {EntryDataType.address});
onAddressMetadataChanged();
}
}
static bool locateCountriesTest(AvesEntry entry) => entry.hasGps && !entry.hasAddress;

View file

@ -25,6 +25,7 @@ class Constants {
// Bidi fun, cf https://www.unicode.org/reports/tr9/
// First Strong Isolate
static const fsi = '\u2068';
// Pop Directional Isolate
static const pdi = '\u2069';

View file

@ -254,6 +254,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
_buildRotateAndFlipMenuItems(context, canApply: canApply),
...[
EntrySetAction.editDate,
EntrySetAction.editLocation,
EntrySetAction.editRating,
EntrySetAction.editTags,
EntrySetAction.removeMetadata,
@ -439,6 +440,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
case EntrySetAction.rotateCW:
case EntrySetAction.flip:
case EntrySetAction.editDate:
case EntrySetAction.editLocation:
case EntrySetAction.editRating:
case EntrySetAction.editTags:
case EntrySetAction.removeMetadata:

View file

@ -79,6 +79,7 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
case EntrySetAction.rotateCW:
case EntrySetAction.flip:
case EntrySetAction.editDate:
case EntrySetAction.editLocation:
case EntrySetAction.editRating:
case EntrySetAction.editTags:
case EntrySetAction.removeMetadata:
@ -122,6 +123,7 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
case EntrySetAction.rotateCW:
case EntrySetAction.flip:
case EntrySetAction.editDate:
case EntrySetAction.editLocation:
case EntrySetAction.editRating:
case EntrySetAction.editTags:
case EntrySetAction.removeMetadata:
@ -185,6 +187,9 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
case EntrySetAction.editDate:
_editDate(context);
break;
case EntrySetAction.editLocation:
_editLocation(context);
break;
case EntrySetAction.editRating:
_editRating(context);
break;
@ -428,6 +433,9 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
if (!await checkStoragePermissionForAlbums(context, selectionDirs, entries: todoItems)) return;
Set<String> obsoleteTags = todoItems.expand((entry) => entry.tags).toSet();
Set<String> obsoleteCountryCodes = todoItems.where((entry) => entry.hasAddress).map((entry) => entry.addressDetails?.countryCode).whereNotNull().toSet();
final source = context.read<CollectionSource>();
source.pauseMonitoring();
var cancelled = false;
@ -448,7 +456,18 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
final editedOps = successOps.where((e) => !e.skipped).toSet();
selection.browse();
source.resumeMonitoring();
unawaited(source.refreshUris(editedOps.map((v) => v.uri).toSet()));
unawaited(source.refreshUris(editedOps.map((v) => v.uri).toSet()).then((_) {
// invalidate filters derived from values before edition
// this invalidation must happen after the source is refreshed,
// otherwise filter chips may eagerly rebuild in between with the old state
if (obsoleteCountryCodes.isNotEmpty) {
source.invalidateCountryFilterSummary(countryCodes: obsoleteCountryCodes);
}
if (obsoleteTags.isNotEmpty) {
source.invalidateTagFilterSummary(tags: obsoleteTags);
}
}));
final l10n = context.l10n;
final successCount = successOps.length;
@ -536,6 +555,20 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
await _edit(context, selection, todoItems, (entry) => entry.editDate(modifier));
}
Future<void> _editLocation(BuildContext context) async {
final collection = context.read<CollectionLens>();
final selection = context.read<Selection<AvesEntry>>();
final selectedItems = _getExpandedSelectedItems(selection);
final todoItems = await _getEditableItems(context, selectedItems: selectedItems, canEdit: (entry) => entry.canEditLocation);
if (todoItems == null || todoItems.isEmpty) return;
final location = await selectLocation(context, todoItems, collection);
if (location == null) return;
await _edit(context, selection, todoItems, (entry) => entry.editLocation(location));
}
Future<void> _editRating(BuildContext context) async {
final selection = context.read<Selection<AvesEntry>>();
final selectedItems = _getExpandedSelectedItems(selection);

View file

@ -1,6 +1,7 @@
import 'package:aves/model/entry.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/dialogs/aves_dialog.dart';
@ -29,7 +30,7 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
final FocusNode _latitudeFocusNode = FocusNode(), _longitudeFocusNode = FocusNode();
final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false);
static const _coordinateFormat = '0.000000';
NumberFormat get coordinateFormatter => NumberFormat('0.000000', context.l10n.localeName);
@override
void initState() {
@ -79,6 +80,7 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
focusNode: _latitudeFocusNode,
decoration: InputDecoration(
labelText: context.l10n.editEntryLocationDialogLatitude,
hintText: coordinateFormatter.format(Constants.pointNemo.latitude),
),
onChanged: (_) => _validate(),
),
@ -87,6 +89,7 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
focusNode: _longitudeFocusNode,
decoration: InputDecoration(
labelText: context.l10n.editEntryLocationDialogLongitude,
hintText: coordinateFormatter.format(Constants.pointNemo.longitude),
),
onChanged: (_) => _validate(),
),
@ -149,10 +152,8 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
}
void _setLocation(BuildContext context, LatLng? latLng) {
final locale = context.l10n.localeName;
final formatter = NumberFormat(_coordinateFormat, locale);
_latitudeController.text = latLng != null ? formatter.format(latLng.latitude) : '';
_longitudeController.text = latLng != null ? formatter.format(latLng.longitude) : '';
_latitudeController.text = latLng != null ? coordinateFormatter.format(latLng.latitude) : '';
_longitudeController.text = latLng != null ? coordinateFormatter.format(latLng.longitude) : '';
setState(() {
_action = _LocationAction.set;
_validate();
@ -187,11 +188,9 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
}
LatLng? _parseLatLng() {
final locale = context.l10n.localeName;
final formatter = NumberFormat(_coordinateFormat, locale);
double? tryParse(String text) {
try {
return double.tryParse(text) ?? (formatter.parse(text).toDouble());
return double.tryParse(text) ?? (coordinateFormatter.parse(text).toDouble());
} catch (e) {
// ignore
return null;

View file

@ -8,9 +8,9 @@ import 'package:aves/widgets/common/basic/insets.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/common/providers/query_provider.dart';
import 'package:aves/widgets/common/providers/selection_provider.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:collection/collection.dart';
class ItemPickDialog extends StatefulWidget {
static const routeName = '/item_pick';

View file

@ -324,7 +324,17 @@ class _LocationRow extends AnimatedWidget {
@override
Widget build(BuildContext context) {
final location = entry.hasAddress ? entry.shortAddress : settings.coordinateFormat.format(context.l10n, entry.latLng!);
late final String location;
if (entry.hasAddress) {
location = entry.shortAddress;
} else {
final latLng = entry.latLng;
if (latLng != null) {
location = settings.coordinateFormat.format(context.l10n, latLng);
} else {
location = '';
}
}
return Row(
children: [
const DecoratedIcon(AIcons.location, shadows: Constants.embossShadows, size: _iconSize),

View file

@ -21,24 +21,6 @@
"locationPickerUseThisLocationButton"
],
"fr": [
"entryInfoActionEditLocation",
"editEntryLocationDialogTitle",
"editEntryLocationDialogChooseOnMapTooltip",
"editEntryLocationDialogLatitude",
"editEntryLocationDialogLongitude",
"locationPickerUseThisLocationButton"
],
"ko": [
"entryInfoActionEditLocation",
"editEntryLocationDialogTitle",
"editEntryLocationDialogChooseOnMapTooltip",
"editEntryLocationDialogLatitude",
"editEntryLocationDialogLongitude",
"locationPickerUseThisLocationButton"
],
"pt": [
"entryInfoActionEditLocation",
"exportEntryDialogWidth",