diff --git a/CHANGELOG.md b/CHANGELOG.md index e83ecfac3..58560410d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 9755d745f..115812126 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -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", diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 60d864d40..2edf0589b 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -74,6 +74,7 @@ "videoActionSettings": "설정", "entryInfoActionEditDate": "날짜 및 시간 수정", + "entryInfoActionEditLocation": "위치 수정", "entryInfoActionEditRating": "별점 수정", "entryInfoActionEditTags": "태그 수정", "entryInfoActionRemoveMetadata": "메타데이터 삭제", @@ -195,6 +196,13 @@ "editEntryDateDialogHours": "시간", "editEntryDateDialogMinutes": "분", + "editEntryLocationDialogTitle": "위치", + "editEntryLocationDialogChooseOnMapTooltip": "지도에서 선택", + "editEntryLocationDialogLatitude": "위도", + "editEntryLocationDialogLongitude": "경도", + + "locationPickerUseThisLocationButton": "이 위치 사용", + "editEntryRatingDialogTitle": "별점", "removeEntryMetadataDialogTitle": "메타데이터 삭제", diff --git a/lib/model/actions/entry_set_actions.dart b/lib/model/actions/entry_set_actions.dart index f3601560b..bb0b1a62c 100644 --- a/lib/model/actions/entry_set_actions.dart +++ b/lib/model/actions/entry_set_actions.dart @@ -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: diff --git a/lib/model/metadata/enums.dart b/lib/model/metadata/enums.dart index 22b20e2db..086f35894 100644 --- a/lib/model/metadata/enums.dart +++ b/lib/model/metadata/enums.dart @@ -100,4 +100,4 @@ extension ExtraDateFieldSource on DateFieldSource { return MetadataField.exifGpsDatestamp; } } -} \ No newline at end of file +} diff --git a/lib/model/source/location.dart b/lib/model/source/location.dart index eff0c4625..dbabb7954 100644 --- a/lib/model/source/location.dart +++ b/lib/model/source/location.dart @@ -30,6 +30,12 @@ mixin LocationMixin on SourceBase { Future locateEntries(AnalysisController controller, Set 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; diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index 8d434e1e0..9d8266c28 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -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'; diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index 7894a7a08..7d0589010 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -254,6 +254,7 @@ class _CollectionAppBarState extends State with SingleTickerPr _buildRotateAndFlipMenuItems(context, canApply: canApply), ...[ EntrySetAction.editDate, + EntrySetAction.editLocation, EntrySetAction.editRating, EntrySetAction.editTags, EntrySetAction.removeMetadata, @@ -439,6 +440,7 @@ class _CollectionAppBarState extends State with SingleTickerPr case EntrySetAction.rotateCW: case EntrySetAction.flip: case EntrySetAction.editDate: + case EntrySetAction.editLocation: case EntrySetAction.editRating: case EntrySetAction.editTags: case EntrySetAction.removeMetadata: diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart index dc5b75c6f..03a14e5bc 100644 --- a/lib/widgets/collection/entry_set_action_delegate.dart +++ b/lib/widgets/collection/entry_set_action_delegate.dart @@ -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 obsoleteTags = todoItems.expand((entry) => entry.tags).toSet(); + Set obsoleteCountryCodes = todoItems.where((entry) => entry.hasAddress).map((entry) => entry.addressDetails?.countryCode).whereNotNull().toSet(); + final source = context.read(); 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 _editLocation(BuildContext context) async { + final collection = context.read(); + final selection = context.read>(); + 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 _editRating(BuildContext context) async { final selection = context.read>(); final selectedItems = _getExpandedSelectedItems(selection); diff --git a/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart b/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart index f7fdcc1ab..3f3e67c95 100644 --- a/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart @@ -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 { final FocusNode _latitudeFocusNode = FocusNode(), _longitudeFocusNode = FocusNode(); final ValueNotifier _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 { focusNode: _latitudeFocusNode, decoration: InputDecoration( labelText: context.l10n.editEntryLocationDialogLatitude, + hintText: coordinateFormatter.format(Constants.pointNemo.latitude), ), onChanged: (_) => _validate(), ), @@ -87,6 +89,7 @@ class _EditEntryLocationDialogState extends State { focusNode: _longitudeFocusNode, decoration: InputDecoration( labelText: context.l10n.editEntryLocationDialogLongitude, + hintText: coordinateFormatter.format(Constants.pointNemo.longitude), ), onChanged: (_) => _validate(), ), @@ -149,10 +152,8 @@ class _EditEntryLocationDialogState extends State { } 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 { } 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; diff --git a/lib/widgets/dialogs/item_pick_dialog.dart b/lib/widgets/dialogs/item_pick_dialog.dart index d12c7a1df..c72252e22 100644 --- a/lib/widgets/dialogs/item_pick_dialog.dart +++ b/lib/widgets/dialogs/item_pick_dialog.dart @@ -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'; diff --git a/lib/widgets/viewer/overlay/bottom/common.dart b/lib/widgets/viewer/overlay/bottom/common.dart index 75a677123..f30eda557 100644 --- a/lib/widgets/viewer/overlay/bottom/common.dart +++ b/lib/widgets/viewer/overlay/bottom/common.dart @@ -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), diff --git a/untranslated.json b/untranslated.json index 4c6c626d6..541c07c7b 100644 --- a/untranslated.json +++ b/untranslated.json @@ -21,24 +21,6 @@ "locationPickerUseThisLocationButton" ], - "fr": [ - "entryInfoActionEditLocation", - "editEntryLocationDialogTitle", - "editEntryLocationDialogChooseOnMapTooltip", - "editEntryLocationDialogLatitude", - "editEntryLocationDialogLongitude", - "locationPickerUseThisLocationButton" - ], - - "ko": [ - "entryInfoActionEditLocation", - "editEntryLocationDialogTitle", - "editEntryLocationDialogChooseOnMapTooltip", - "editEntryLocationDialogLatitude", - "editEntryLocationDialogLongitude", - "locationPickerUseThisLocationButton" - ], - "pt": [ "entryInfoActionEditLocation", "exportEntryDialogWidth",