improved metadata edit

This commit is contained in:
Thibault Deckers 2021-09-13 16:37:26 +09:00
parent fe88782297
commit 466d150e49
9 changed files with 133 additions and 69 deletions

View file

@ -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'

View file

@ -672,6 +672,7 @@ abstract class ImageProvider {
}
}
} catch (e: Exception) {
Log.d(LOG_TAG, "failed to remove metadata", e)
callback.onFailure(e)
return
}

View file

@ -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": {},

View file

@ -153,6 +153,8 @@
"removeEntryMetadataDialogTitle": "메타데이터 삭제",
"removeEntryMetadataDialogMore": "더 보기",
"removeEntryMetadataMotionPhotoXmpWarningDialogMessage": "XMP가 있어야 모션 포토에 포함되는 동영상을 재생할 수 있습니다. 삭제하시겠습니까?",
"videoSpeedDialogLabel": "재생 배속",
"videoStreamSelectionDialogVideo": "동영상",

View file

@ -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<void> 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<bool> 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;
}

View file

@ -130,6 +130,7 @@ class MediaStoreSource extends CollectionSource {
Future<Set<String>> refreshUris(Set<String> 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

View file

@ -109,14 +109,14 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
Future<void> _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<void> _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<CollectionSource>().renameEntry(entry, newName, persist: isMainMode(context));
final success = await context.read<CollectionSource>().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<ValueNotifier<AppMode>>().value == AppMode.main;
bool _isMainMode(BuildContext context) => context.read<ValueNotifier<AppMode>>().value == AppMode.main;
void _goToSourceViewer(BuildContext context, AvesEntry entry) {
Navigator.push(

View file

@ -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<ValueNotifier<AppMode>>().value == AppMode.main;
Future<void> _edit(BuildContext context, Future<bool> Function() apply) async {
if (!await checkStoragePermission(context, {entry})) return;
final source = context.read<CollectionSource?>();
source?.pauseMonitoring();
final success = await apply();
if (success) {
showFeedback(context, context.l10n.genericSuccessFeedback);
} else {
showFeedback(context, context.l10n.genericFailureFeedback);
}
source?.resumeMonitoring();
}
Future<void> _showDateEditDialog(BuildContext context) async {
final modifier = await showDialog<DateModifier>(
context: context,
builder: (context) => EditEntryDateDialog(entry: entry),
);
if (modifier == null) return;
await _edit(context, () => entry.editDate(modifier, persist: _isMainMode(context)));
}
Future<void> _showMetadataRemovalDialog(BuildContext context) async {
final types = await showDialog<Set<MetadataType>>(
context: context,
builder: (context) => RemoveEntryMetadataDialog(entry: entry),
);
if (types == null || types.isEmpty) return;
if (entry.isMotionPhoto && types.contains(MetadataType.xmp)) {
final proceed = await showDialog<bool>(
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)));
}
}

View file

@ -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<Map<String, MetadataDirectory>> 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<void> _showDateEditDialog(BuildContext context) async {
final modifier = await showDialog<DateModifier>(
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<void> _showMetadataRemovalDialog(BuildContext context) async {
final types = await showDialog<Set<MetadataType>>(
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<CollectionSource>().refreshMetadata({entry});
showFeedback(context, context.l10n.genericSuccessFeedback);
} else {
showFeedback(context, context.l10n.genericFailureFeedback);
}
}
}