#2 edit location in bulk
This commit is contained in:
parent
e0f45f03c1
commit
b97c51e541
13 changed files with 85 additions and 30 deletions
|
@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Info: edit location of JPG/PNG/WEBP/DNG images via Exif
|
||||||
- Viewer: resize option when exporting
|
- Viewer: resize option when exporting
|
||||||
- Settings: export/import covers & favourites along with settings
|
- Settings: export/import covers & favourites along with settings
|
||||||
- Portuguese translation (thanks Jonatas De Almeida Barros)
|
- Portuguese translation (thanks Jonatas De Almeida Barros)
|
||||||
|
|
|
@ -74,6 +74,7 @@
|
||||||
"videoActionSettings": "Préférences",
|
"videoActionSettings": "Préférences",
|
||||||
|
|
||||||
"entryInfoActionEditDate": "Modifier la date",
|
"entryInfoActionEditDate": "Modifier la date",
|
||||||
|
"entryInfoActionEditLocation": "Modifier le lieu",
|
||||||
"entryInfoActionEditRating": "Modifier la notation",
|
"entryInfoActionEditRating": "Modifier la notation",
|
||||||
"entryInfoActionEditTags": "Modifier les libellés",
|
"entryInfoActionEditTags": "Modifier les libellés",
|
||||||
"entryInfoActionRemoveMetadata": "Retirer les métadonnées",
|
"entryInfoActionRemoveMetadata": "Retirer les métadonnées",
|
||||||
|
@ -195,6 +196,13 @@
|
||||||
"editEntryDateDialogHours": "Heures",
|
"editEntryDateDialogHours": "Heures",
|
||||||
"editEntryDateDialogMinutes": "Minutes",
|
"editEntryDateDialogMinutes": "Minutes",
|
||||||
|
|
||||||
|
"editEntryLocationDialogTitle": "Lieu",
|
||||||
|
"editEntryLocationDialogChooseOnMapTooltip": "Sélectionner sur la carte",
|
||||||
|
"editEntryLocationDialogLatitude": "Latitude",
|
||||||
|
"editEntryLocationDialogLongitude": "Longitude",
|
||||||
|
|
||||||
|
"locationPickerUseThisLocationButton": "Utiliser ce lieu",
|
||||||
|
|
||||||
"editEntryRatingDialogTitle": "Notation",
|
"editEntryRatingDialogTitle": "Notation",
|
||||||
|
|
||||||
"removeEntryMetadataDialogTitle": "Retrait de métadonnées",
|
"removeEntryMetadataDialogTitle": "Retrait de métadonnées",
|
||||||
|
|
|
@ -74,6 +74,7 @@
|
||||||
"videoActionSettings": "설정",
|
"videoActionSettings": "설정",
|
||||||
|
|
||||||
"entryInfoActionEditDate": "날짜 및 시간 수정",
|
"entryInfoActionEditDate": "날짜 및 시간 수정",
|
||||||
|
"entryInfoActionEditLocation": "위치 수정",
|
||||||
"entryInfoActionEditRating": "별점 수정",
|
"entryInfoActionEditRating": "별점 수정",
|
||||||
"entryInfoActionEditTags": "태그 수정",
|
"entryInfoActionEditTags": "태그 수정",
|
||||||
"entryInfoActionRemoveMetadata": "메타데이터 삭제",
|
"entryInfoActionRemoveMetadata": "메타데이터 삭제",
|
||||||
|
@ -195,6 +196,13 @@
|
||||||
"editEntryDateDialogHours": "시간",
|
"editEntryDateDialogHours": "시간",
|
||||||
"editEntryDateDialogMinutes": "분",
|
"editEntryDateDialogMinutes": "분",
|
||||||
|
|
||||||
|
"editEntryLocationDialogTitle": "위치",
|
||||||
|
"editEntryLocationDialogChooseOnMapTooltip": "지도에서 선택",
|
||||||
|
"editEntryLocationDialogLatitude": "위도",
|
||||||
|
"editEntryLocationDialogLongitude": "경도",
|
||||||
|
|
||||||
|
"locationPickerUseThisLocationButton": "이 위치 사용",
|
||||||
|
|
||||||
"editEntryRatingDialogTitle": "별점",
|
"editEntryRatingDialogTitle": "별점",
|
||||||
|
|
||||||
"removeEntryMetadataDialogTitle": "메타데이터 삭제",
|
"removeEntryMetadataDialogTitle": "메타데이터 삭제",
|
||||||
|
|
|
@ -26,6 +26,7 @@ enum EntrySetAction {
|
||||||
rotateCW,
|
rotateCW,
|
||||||
flip,
|
flip,
|
||||||
editDate,
|
editDate,
|
||||||
|
editLocation,
|
||||||
editRating,
|
editRating,
|
||||||
editTags,
|
editTags,
|
||||||
removeMetadata,
|
removeMetadata,
|
||||||
|
@ -107,6 +108,8 @@ extension ExtraEntrySetAction on EntrySetAction {
|
||||||
return context.l10n.entryActionFlip;
|
return context.l10n.entryActionFlip;
|
||||||
case EntrySetAction.editDate:
|
case EntrySetAction.editDate:
|
||||||
return context.l10n.entryInfoActionEditDate;
|
return context.l10n.entryInfoActionEditDate;
|
||||||
|
case EntrySetAction.editLocation:
|
||||||
|
return context.l10n.entryInfoActionEditLocation;
|
||||||
case EntrySetAction.editRating:
|
case EntrySetAction.editRating:
|
||||||
return context.l10n.entryInfoActionEditRating;
|
return context.l10n.entryInfoActionEditRating;
|
||||||
case EntrySetAction.editTags:
|
case EntrySetAction.editTags:
|
||||||
|
@ -166,6 +169,8 @@ extension ExtraEntrySetAction on EntrySetAction {
|
||||||
return AIcons.flip;
|
return AIcons.flip;
|
||||||
case EntrySetAction.editDate:
|
case EntrySetAction.editDate:
|
||||||
return AIcons.date;
|
return AIcons.date;
|
||||||
|
case EntrySetAction.editLocation:
|
||||||
|
return AIcons.location;
|
||||||
case EntrySetAction.editRating:
|
case EntrySetAction.editRating:
|
||||||
return AIcons.editRating;
|
return AIcons.editRating;
|
||||||
case EntrySetAction.editTags:
|
case EntrySetAction.editTags:
|
||||||
|
|
|
@ -100,4 +100,4 @@ extension ExtraDateFieldSource on DateFieldSource {
|
||||||
return MetadataField.exifGpsDatestamp;
|
return MetadataField.exifGpsDatestamp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,12 @@ mixin LocationMixin on SourceBase {
|
||||||
Future<void> locateEntries(AnalysisController controller, Set<AvesEntry> candidateEntries) async {
|
Future<void> locateEntries(AnalysisController controller, Set<AvesEntry> candidateEntries) async {
|
||||||
await _locateCountries(controller, candidateEntries);
|
await _locateCountries(controller, candidateEntries);
|
||||||
await _locatePlaces(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;
|
static bool locateCountriesTest(AvesEntry entry) => entry.hasGps && !entry.hasAddress;
|
||||||
|
|
|
@ -25,6 +25,7 @@ class Constants {
|
||||||
// Bidi fun, cf https://www.unicode.org/reports/tr9/
|
// Bidi fun, cf https://www.unicode.org/reports/tr9/
|
||||||
// First Strong Isolate
|
// First Strong Isolate
|
||||||
static const fsi = '\u2068';
|
static const fsi = '\u2068';
|
||||||
|
|
||||||
// Pop Directional Isolate
|
// Pop Directional Isolate
|
||||||
static const pdi = '\u2069';
|
static const pdi = '\u2069';
|
||||||
|
|
||||||
|
|
|
@ -254,6 +254,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
_buildRotateAndFlipMenuItems(context, canApply: canApply),
|
_buildRotateAndFlipMenuItems(context, canApply: canApply),
|
||||||
...[
|
...[
|
||||||
EntrySetAction.editDate,
|
EntrySetAction.editDate,
|
||||||
|
EntrySetAction.editLocation,
|
||||||
EntrySetAction.editRating,
|
EntrySetAction.editRating,
|
||||||
EntrySetAction.editTags,
|
EntrySetAction.editTags,
|
||||||
EntrySetAction.removeMetadata,
|
EntrySetAction.removeMetadata,
|
||||||
|
@ -439,6 +440,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
case EntrySetAction.rotateCW:
|
case EntrySetAction.rotateCW:
|
||||||
case EntrySetAction.flip:
|
case EntrySetAction.flip:
|
||||||
case EntrySetAction.editDate:
|
case EntrySetAction.editDate:
|
||||||
|
case EntrySetAction.editLocation:
|
||||||
case EntrySetAction.editRating:
|
case EntrySetAction.editRating:
|
||||||
case EntrySetAction.editTags:
|
case EntrySetAction.editTags:
|
||||||
case EntrySetAction.removeMetadata:
|
case EntrySetAction.removeMetadata:
|
||||||
|
|
|
@ -79,6 +79,7 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
||||||
case EntrySetAction.rotateCW:
|
case EntrySetAction.rotateCW:
|
||||||
case EntrySetAction.flip:
|
case EntrySetAction.flip:
|
||||||
case EntrySetAction.editDate:
|
case EntrySetAction.editDate:
|
||||||
|
case EntrySetAction.editLocation:
|
||||||
case EntrySetAction.editRating:
|
case EntrySetAction.editRating:
|
||||||
case EntrySetAction.editTags:
|
case EntrySetAction.editTags:
|
||||||
case EntrySetAction.removeMetadata:
|
case EntrySetAction.removeMetadata:
|
||||||
|
@ -122,6 +123,7 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
||||||
case EntrySetAction.rotateCW:
|
case EntrySetAction.rotateCW:
|
||||||
case EntrySetAction.flip:
|
case EntrySetAction.flip:
|
||||||
case EntrySetAction.editDate:
|
case EntrySetAction.editDate:
|
||||||
|
case EntrySetAction.editLocation:
|
||||||
case EntrySetAction.editRating:
|
case EntrySetAction.editRating:
|
||||||
case EntrySetAction.editTags:
|
case EntrySetAction.editTags:
|
||||||
case EntrySetAction.removeMetadata:
|
case EntrySetAction.removeMetadata:
|
||||||
|
@ -185,6 +187,9 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
||||||
case EntrySetAction.editDate:
|
case EntrySetAction.editDate:
|
||||||
_editDate(context);
|
_editDate(context);
|
||||||
break;
|
break;
|
||||||
|
case EntrySetAction.editLocation:
|
||||||
|
_editLocation(context);
|
||||||
|
break;
|
||||||
case EntrySetAction.editRating:
|
case EntrySetAction.editRating:
|
||||||
_editRating(context);
|
_editRating(context);
|
||||||
break;
|
break;
|
||||||
|
@ -428,6 +433,9 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
||||||
|
|
||||||
if (!await checkStoragePermissionForAlbums(context, selectionDirs, entries: todoItems)) return;
|
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>();
|
final source = context.read<CollectionSource>();
|
||||||
source.pauseMonitoring();
|
source.pauseMonitoring();
|
||||||
var cancelled = false;
|
var cancelled = false;
|
||||||
|
@ -448,7 +456,18 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
||||||
final editedOps = successOps.where((e) => !e.skipped).toSet();
|
final editedOps = successOps.where((e) => !e.skipped).toSet();
|
||||||
selection.browse();
|
selection.browse();
|
||||||
source.resumeMonitoring();
|
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 l10n = context.l10n;
|
||||||
final successCount = successOps.length;
|
final successCount = successOps.length;
|
||||||
|
@ -536,6 +555,20 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
||||||
await _edit(context, selection, todoItems, (entry) => entry.editDate(modifier));
|
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 {
|
Future<void> _editRating(BuildContext context) async {
|
||||||
final selection = context.read<Selection<AvesEntry>>();
|
final selection = context.read<Selection<AvesEntry>>();
|
||||||
final selectedItems = _getExpandedSelectedItems(selection);
|
final selectedItems = _getExpandedSelectedItems(selection);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/theme/icons.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/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||||
import 'package:aves/widgets/dialogs/aves_dialog.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 FocusNode _latitudeFocusNode = FocusNode(), _longitudeFocusNode = FocusNode();
|
||||||
final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false);
|
final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false);
|
||||||
|
|
||||||
static const _coordinateFormat = '0.000000';
|
NumberFormat get coordinateFormatter => NumberFormat('0.000000', context.l10n.localeName);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@ -79,6 +80,7 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
|
||||||
focusNode: _latitudeFocusNode,
|
focusNode: _latitudeFocusNode,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: context.l10n.editEntryLocationDialogLatitude,
|
labelText: context.l10n.editEntryLocationDialogLatitude,
|
||||||
|
hintText: coordinateFormatter.format(Constants.pointNemo.latitude),
|
||||||
),
|
),
|
||||||
onChanged: (_) => _validate(),
|
onChanged: (_) => _validate(),
|
||||||
),
|
),
|
||||||
|
@ -87,6 +89,7 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
|
||||||
focusNode: _longitudeFocusNode,
|
focusNode: _longitudeFocusNode,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: context.l10n.editEntryLocationDialogLongitude,
|
labelText: context.l10n.editEntryLocationDialogLongitude,
|
||||||
|
hintText: coordinateFormatter.format(Constants.pointNemo.longitude),
|
||||||
),
|
),
|
||||||
onChanged: (_) => _validate(),
|
onChanged: (_) => _validate(),
|
||||||
),
|
),
|
||||||
|
@ -149,10 +152,8 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setLocation(BuildContext context, LatLng? latLng) {
|
void _setLocation(BuildContext context, LatLng? latLng) {
|
||||||
final locale = context.l10n.localeName;
|
_latitudeController.text = latLng != null ? coordinateFormatter.format(latLng.latitude) : '';
|
||||||
final formatter = NumberFormat(_coordinateFormat, locale);
|
_longitudeController.text = latLng != null ? coordinateFormatter.format(latLng.longitude) : '';
|
||||||
_latitudeController.text = latLng != null ? formatter.format(latLng.latitude) : '';
|
|
||||||
_longitudeController.text = latLng != null ? formatter.format(latLng.longitude) : '';
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_action = _LocationAction.set;
|
_action = _LocationAction.set;
|
||||||
_validate();
|
_validate();
|
||||||
|
@ -187,11 +188,9 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
|
||||||
}
|
}
|
||||||
|
|
||||||
LatLng? _parseLatLng() {
|
LatLng? _parseLatLng() {
|
||||||
final locale = context.l10n.localeName;
|
|
||||||
final formatter = NumberFormat(_coordinateFormat, locale);
|
|
||||||
double? tryParse(String text) {
|
double? tryParse(String text) {
|
||||||
try {
|
try {
|
||||||
return double.tryParse(text) ?? (formatter.parse(text).toDouble());
|
return double.tryParse(text) ?? (coordinateFormatter.parse(text).toDouble());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ignore
|
// ignore
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -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/media_query_data_provider.dart';
|
||||||
import 'package:aves/widgets/common/providers/query_provider.dart';
|
import 'package:aves/widgets/common/providers/query_provider.dart';
|
||||||
import 'package:aves/widgets/common/providers/selection_provider.dart';
|
import 'package:aves/widgets/common/providers/selection_provider.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
|
|
||||||
class ItemPickDialog extends StatefulWidget {
|
class ItemPickDialog extends StatefulWidget {
|
||||||
static const routeName = '/item_pick';
|
static const routeName = '/item_pick';
|
||||||
|
|
|
@ -324,7 +324,17 @@ class _LocationRow extends AnimatedWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
const DecoratedIcon(AIcons.location, shadows: Constants.embossShadows, size: _iconSize),
|
const DecoratedIcon(AIcons.location, shadows: Constants.embossShadows, size: _iconSize),
|
||||||
|
|
|
@ -21,24 +21,6 @@
|
||||||
"locationPickerUseThisLocationButton"
|
"locationPickerUseThisLocationButton"
|
||||||
],
|
],
|
||||||
|
|
||||||
"fr": [
|
|
||||||
"entryInfoActionEditLocation",
|
|
||||||
"editEntryLocationDialogTitle",
|
|
||||||
"editEntryLocationDialogChooseOnMapTooltip",
|
|
||||||
"editEntryLocationDialogLatitude",
|
|
||||||
"editEntryLocationDialogLongitude",
|
|
||||||
"locationPickerUseThisLocationButton"
|
|
||||||
],
|
|
||||||
|
|
||||||
"ko": [
|
|
||||||
"entryInfoActionEditLocation",
|
|
||||||
"editEntryLocationDialogTitle",
|
|
||||||
"editEntryLocationDialogChooseOnMapTooltip",
|
|
||||||
"editEntryLocationDialogLatitude",
|
|
||||||
"editEntryLocationDialogLongitude",
|
|
||||||
"locationPickerUseThisLocationButton"
|
|
||||||
],
|
|
||||||
|
|
||||||
"pt": [
|
"pt": [
|
||||||
"entryInfoActionEditLocation",
|
"entryInfoActionEditLocation",
|
||||||
"exportEntryDialogWidth",
|
"exportEntryDialogWidth",
|
||||||
|
|
Loading…
Reference in a new issue