From 466d150e49e0ca97f4613e2e055461a6fdd5502a Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 13 Sep 2021 16:37:26 +0900 Subject: [PATCH] improved metadata edit --- android/app/build.gradle | 6 +- .../aves/model/provider/ImageProvider.kt | 1 + lib/l10n/app_en.arb | 3 + lib/l10n/app_ko.arb | 2 + lib/model/entry.dart | 30 ++++++- lib/model/source/media_store_source.dart | 1 + lib/widgets/viewer/entry_action_delegate.dart | 8 +- .../info/entry_info_action_delegate.dart | 90 +++++++++++++++++++ lib/widgets/viewer/info/info_app_bar.dart | 61 +------------ 9 files changed, 133 insertions(+), 69 deletions(-) create mode 100644 lib/widgets/viewer/info/entry_info_action_delegate.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index 5c865db38..41223bc91 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -120,10 +120,10 @@ dependencies { implementation 'com.caverock:androidsvg-aar:1.4' implementation 'com.commonsware.cwac:document:0.4.1' implementation 'com.drewnoakes:metadata-extractor:2.16.0' - // https://jitpack.io/com/github/deckerst/Android-TiffBitmapFactory/**********/build.log + // https://jitpack.io/p/deckerst/Android-TiffBitmapFactory implementation 'com.github.deckerst:Android-TiffBitmapFactory:876e53870a' // forked, built by JitPack - // https://jitpack.io/com/github/deckerst/pixymeta-android/**********/build.log - implementation 'com.github.deckerst:pixymeta-android:0827df80b9' // forked, built by JitPack + // https://jitpack.io/p/deckerst/pixymeta-android + implementation 'com.github.deckerst:pixymeta-android:082ed1dafc' // forked, built by JitPack implementation 'com.github.bumptech.glide:glide:4.12.0' kapt 'androidx.annotation:annotation:1.2.0' diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt index a5f0ef5cb..a52a733b1 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt @@ -672,6 +672,7 @@ abstract class ImageProvider { } } } catch (e: Exception) { + Log.d(LOG_TAG, "failed to remove metadata", e) callback.onFailure(e) return } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 44bae0d16..51be3194e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -332,6 +332,9 @@ "removeEntryMetadataDialogMore": "More", "@removeEntryMetadataDialogMore": {}, + "removeEntryMetadataMotionPhotoXmpWarningDialogMessage": "XMP is required to play the video inside this motion photo. Are you sure you want to remove it?", + "@removeEntryMetadataMotionPhotoXmpWarningDialogMessage": {}, + "videoSpeedDialogLabel": "Playback speed", "@videoSpeedDialogLabel": {}, diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index c3446634d..ff446bcfa 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -153,6 +153,8 @@ "removeEntryMetadataDialogTitle": "메타데이터 삭제", "removeEntryMetadataDialogMore": "더 보기", + "removeEntryMetadataMotionPhotoXmpWarningDialogMessage": "XMP가 있어야 모션 포토에 포함되는 동영상을 재생할 수 있습니다. 삭제하시겠습니까?", + "videoSpeedDialogLabel": "재생 배속", "videoStreamSelectionDialogVideo": "동영상", diff --git a/lib/model/entry.dart b/lib/model/entry.dart index 53fb7d2cf..62122fb12 100644 --- a/lib/model/entry.dart +++ b/lib/model/entry.dart @@ -36,7 +36,7 @@ class AvesEntry { // `dateModifiedSecs` can be missing in viewer mode int? _dateModifiedSecs; - final int? sourceDateTakenMillis; + int? sourceDateTakenMillis; int? _durationMillis; int? _catalogDateMillis; CatalogMetadata? _catalogMetadata; @@ -564,8 +564,13 @@ class AvesEntry { if (path is String) this.path = path; final contentId = newFields['contentId']; if (contentId is int) this.contentId = contentId; + final sourceTitle = newFields['title']; if (sourceTitle is String) this.sourceTitle = sourceTitle; + final sourceRotationDegrees = newFields['sourceRotationDegrees']; + if (sourceRotationDegrees is int) this.sourceRotationDegrees = sourceRotationDegrees; + final sourceDateTakenMillis = newFields['sourceDateTakenMillis']; + if (sourceDateTakenMillis is int) this.sourceDateTakenMillis = sourceDateTakenMillis; final width = newFields['width']; if (width is int) this.width = width; @@ -591,6 +596,24 @@ class AvesEntry { metadataChangeNotifier.notifyListeners(); } + Future refresh({required bool persist}) async { + _catalogMetadata = null; + _addressDetails = null; + _bestDate = null; + _bestTitle = null; + _xmpSubjects = null; + if (persist) { + await metadataDb.removeIds({contentId!}, metadataOnly: true); + } + + final updated = await mediaFileService.getEntry(uri, mimeType); + if (updated != null) { + await _applyNewFields(updated.toMap(), persist: persist); + await catalog(background: false, persist: persist); + await locate(background: false); + } + } + Future rotate({required bool clockwise, required bool persist}) async { final newFields = await metadataEditService.rotate(this, clockwise: clockwise); if (newFields.isEmpty) return false; @@ -619,8 +642,7 @@ class AvesEntry { final newFields = await metadataEditService.editDate(this, modifier); if (newFields.isEmpty) return false; - await _applyNewFields(newFields, persist: persist); - await catalog(background: false, persist: persist, force: true); + await refresh(persist: persist); return true; } @@ -628,7 +650,7 @@ class AvesEntry { final newFields = await metadataEditService.removeTypes(this, types); if (newFields.isEmpty) return false; - await _applyNewFields(newFields, persist: persist); + await refresh(persist: persist); return true; } diff --git a/lib/model/source/media_store_source.dart b/lib/model/source/media_store_source.dart index ac9e97ab6..8aa4ccee0 100644 --- a/lib/model/source/media_store_source.dart +++ b/lib/model/source/media_store_source.dart @@ -130,6 +130,7 @@ class MediaStoreSource extends CollectionSource { Future> refreshUris(Set changedUris) async { if (!_initialized || !isMonitoring) return changedUris; + debugPrint('$runtimeType refreshUris ${changedUris.length} uris'); final uriByContentId = Map.fromEntries(changedUris.map((uri) { final pathSegments = Uri.parse(uri).pathSegments; // e.g. URI `content://media/` has no path segment diff --git a/lib/widgets/viewer/entry_action_delegate.dart b/lib/widgets/viewer/entry_action_delegate.dart index 9a8660d64..1433ec610 100644 --- a/lib/widgets/viewer/entry_action_delegate.dart +++ b/lib/widgets/viewer/entry_action_delegate.dart @@ -109,14 +109,14 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix Future _flip(BuildContext context, AvesEntry entry) async { if (!await checkStoragePermission(context, {entry})) return; - final success = await entry.flip(persist: isMainMode(context)); + final success = await entry.flip(persist: _isMainMode(context)); if (!success) showFeedback(context, context.l10n.genericFailureFeedback); } Future _rotate(BuildContext context, AvesEntry entry, {required bool clockwise}) async { if (!await checkStoragePermission(context, {entry})) return; - final success = await entry.rotate(clockwise: clockwise, persist: isMainMode(context)); + final success = await entry.rotate(clockwise: clockwise, persist: _isMainMode(context)); if (!success) showFeedback(context, context.l10n.genericFailureFeedback); } @@ -269,7 +269,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix if (!await checkStoragePermission(context, {entry})) return; - final success = await context.read().renameEntry(entry, newName, persist: isMainMode(context)); + final success = await context.read().renameEntry(entry, newName, persist: _isMainMode(context)); if (success) { showFeedback(context, context.l10n.genericSuccessFeedback); @@ -278,7 +278,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix } } - bool isMainMode(BuildContext context) => context.read>().value == AppMode.main; + bool _isMainMode(BuildContext context) => context.read>().value == AppMode.main; void _goToSourceViewer(BuildContext context, AvesEntry entry) { Navigator.push( diff --git a/lib/widgets/viewer/info/entry_info_action_delegate.dart b/lib/widgets/viewer/info/entry_info_action_delegate.dart new file mode 100644 index 000000000..0f6c8b2a1 --- /dev/null +++ b/lib/widgets/viewer/info/entry_info_action_delegate.dart @@ -0,0 +1,90 @@ +import 'package:aves/app_mode.dart'; +import 'package:aves/model/actions/entry_info_actions.dart'; +import 'package:aves/model/entry.dart'; +import 'package:aves/model/metadata/date_modifier.dart'; +import 'package:aves/model/metadata/enums.dart'; +import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/widgets/common/action_mixins/feedback.dart'; +import 'package:aves/widgets/common/action_mixins/permission_aware.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/dialogs/aves_dialog.dart'; +import 'package:aves/widgets/dialogs/edit_entry_date_dialog.dart'; +import 'package:aves/widgets/dialogs/remove_entry_metadata_dialog.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin { + final AvesEntry entry; + + const EntryInfoActionDelegate(this.entry); + + void onActionSelected(BuildContext context, EntryInfoAction action) async { + switch (action) { + case EntryInfoAction.editDate: + await _showDateEditDialog(context); + break; + case EntryInfoAction.removeMetadata: + await _showMetadataRemovalDialog(context); + break; + } + } + + bool _isMainMode(BuildContext context) => context.read>().value == AppMode.main; + + Future _edit(BuildContext context, Future Function() apply) async { + if (!await checkStoragePermission(context, {entry})) return; + + final source = context.read(); + source?.pauseMonitoring(); + final success = await apply(); + if (success) { + showFeedback(context, context.l10n.genericSuccessFeedback); + } else { + showFeedback(context, context.l10n.genericFailureFeedback); + } + source?.resumeMonitoring(); + } + + Future _showDateEditDialog(BuildContext context) async { + final modifier = await showDialog( + context: context, + builder: (context) => EditEntryDateDialog(entry: entry), + ); + if (modifier == null) return; + + await _edit(context, () => entry.editDate(modifier, persist: _isMainMode(context))); + } + + Future _showMetadataRemovalDialog(BuildContext context) async { + final types = await showDialog>( + context: context, + builder: (context) => RemoveEntryMetadataDialog(entry: entry), + ); + if (types == null || types.isEmpty) return; + + if (entry.isMotionPhoto && types.contains(MetadataType.xmp)) { + final proceed = await showDialog( + context: context, + builder: (context) { + return AvesDialog( + context: context, + content: Text(context.l10n.removeEntryMetadataMotionPhotoXmpWarningDialogMessage), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(MaterialLocalizations.of(context).cancelButtonLabel), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: Text(context.l10n.applyButtonLabel), + ), + ], + ); + }, + ); + if (proceed == null || !proceed) return; + } + + await _edit(context, () => entry.removeMetadata(types, persist: _isMainMode(context))); + } +} diff --git a/lib/widgets/viewer/info/info_app_bar.dart b/lib/widgets/viewer/info/info_app_bar.dart index 0881cf9a4..7692d35cb 100644 --- a/lib/widgets/viewer/info/info_app_bar.dart +++ b/lib/widgets/viewer/info/info_app_bar.dart @@ -1,24 +1,17 @@ import 'package:aves/model/actions/entry_info_actions.dart'; import 'package:aves/model/entry.dart'; -import 'package:aves/model/metadata/date_modifier.dart'; -import 'package:aves/model/metadata/enums.dart'; -import 'package:aves/model/source/collection_source.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/widgets/common/action_mixins/feedback.dart'; -import 'package:aves/widgets/common/action_mixins/permission_aware.dart'; import 'package:aves/widgets/common/app_bar_title.dart'; import 'package:aves/widgets/common/basic/menu.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/dialogs/edit_entry_date_dialog.dart'; -import 'package:aves/widgets/dialogs/remove_entry_metadata_dialog.dart'; +import 'package:aves/widgets/viewer/info/entry_info_action_delegate.dart'; import 'package:aves/widgets/viewer/info/info_search.dart'; import 'package:aves/widgets/viewer/info/metadata/metadata_section.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -import 'package:provider/provider.dart'; -class InfoAppBar extends StatelessWidget with FeedbackMixin, PermissionAwareMixin { +class InfoAppBar extends StatelessWidget { final AvesEntry entry; final ValueNotifier> metadataNotifier; final VoidCallback onBackPressed; @@ -68,7 +61,7 @@ class InfoAppBar extends StatelessWidget with FeedbackMixin, PermissionAwareMixi }, onSelected: (action) { // wait for the popup menu to hide before proceeding with the action - Future.delayed(Durations.popupMenuAnimation * timeDilation, () => _onActionSelected(context, action)); + Future.delayed(Durations.popupMenuAnimation * timeDilation, () => EntryInfoActionDelegate(entry).onActionSelected(context, action)); }, ), ), @@ -88,52 +81,4 @@ class InfoAppBar extends StatelessWidget with FeedbackMixin, PermissionAwareMixi ), ); } - - void _onActionSelected(BuildContext context, EntryInfoAction action) async { - switch (action) { - case EntryInfoAction.editDate: - await _showDateEditDialog(context); - break; - case EntryInfoAction.removeMetadata: - await _showMetadataRemovalDialog(context); - break; - } - } - - Future _showDateEditDialog(BuildContext context) async { - final modifier = await showDialog( - context: context, - builder: (context) => EditEntryDateDialog(entry: entry), - ); - if (modifier == null) return; - - if (!await checkStoragePermission(context, {entry})) return; - - // TODO TLAD [meta edit] handle viewer mode - final success = await entry.editDate(modifier, persist: true); - if (success) { - showFeedback(context, context.l10n.genericSuccessFeedback); - } else { - showFeedback(context, context.l10n.genericFailureFeedback); - } - } - - Future _showMetadataRemovalDialog(BuildContext context) async { - final types = await showDialog>( - context: context, - builder: (context) => RemoveEntryMetadataDialog(entry: entry), - ); - if (types == null) return; - - if (!await checkStoragePermission(context, {entry})) return; - - // TODO TLAD [meta edit] handle viewer mode - final success = await entry.removeMetadata(types, persist: true); - if (success) { - await context.read().refreshMetadata({entry}); - showFeedback(context, context.l10n.genericSuccessFeedback); - } else { - showFeedback(context, context.l10n.genericFailureFeedback); - } - } }