improved metadata edit
This commit is contained in:
parent
fe88782297
commit
466d150e49
9 changed files with 133 additions and 69 deletions
|
@ -120,10 +120,10 @@ dependencies {
|
||||||
implementation 'com.caverock:androidsvg-aar:1.4'
|
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||||
implementation 'com.commonsware.cwac:document:0.4.1'
|
implementation 'com.commonsware.cwac:document:0.4.1'
|
||||||
implementation 'com.drewnoakes:metadata-extractor:2.16.0'
|
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
|
implementation 'com.github.deckerst:Android-TiffBitmapFactory:876e53870a' // forked, built by JitPack
|
||||||
// https://jitpack.io/com/github/deckerst/pixymeta-android/**********/build.log
|
// https://jitpack.io/p/deckerst/pixymeta-android
|
||||||
implementation 'com.github.deckerst:pixymeta-android:0827df80b9' // forked, built by JitPack
|
implementation 'com.github.deckerst:pixymeta-android:082ed1dafc' // forked, built by JitPack
|
||||||
implementation 'com.github.bumptech.glide:glide:4.12.0'
|
implementation 'com.github.bumptech.glide:glide:4.12.0'
|
||||||
|
|
||||||
kapt 'androidx.annotation:annotation:1.2.0'
|
kapt 'androidx.annotation:annotation:1.2.0'
|
||||||
|
|
|
@ -672,6 +672,7 @@ abstract class ImageProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Log.d(LOG_TAG, "failed to remove metadata", e)
|
||||||
callback.onFailure(e)
|
callback.onFailure(e)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -332,6 +332,9 @@
|
||||||
"removeEntryMetadataDialogMore": "More",
|
"removeEntryMetadataDialogMore": "More",
|
||||||
"@removeEntryMetadataDialogMore": {},
|
"@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": "Playback speed",
|
||||||
"@videoSpeedDialogLabel": {},
|
"@videoSpeedDialogLabel": {},
|
||||||
|
|
||||||
|
|
|
@ -153,6 +153,8 @@
|
||||||
"removeEntryMetadataDialogTitle": "메타데이터 삭제",
|
"removeEntryMetadataDialogTitle": "메타데이터 삭제",
|
||||||
"removeEntryMetadataDialogMore": "더 보기",
|
"removeEntryMetadataDialogMore": "더 보기",
|
||||||
|
|
||||||
|
"removeEntryMetadataMotionPhotoXmpWarningDialogMessage": "XMP가 있어야 모션 포토에 포함되는 동영상을 재생할 수 있습니다. 삭제하시겠습니까?",
|
||||||
|
|
||||||
"videoSpeedDialogLabel": "재생 배속",
|
"videoSpeedDialogLabel": "재생 배속",
|
||||||
|
|
||||||
"videoStreamSelectionDialogVideo": "동영상",
|
"videoStreamSelectionDialogVideo": "동영상",
|
||||||
|
|
|
@ -36,7 +36,7 @@ class AvesEntry {
|
||||||
|
|
||||||
// `dateModifiedSecs` can be missing in viewer mode
|
// `dateModifiedSecs` can be missing in viewer mode
|
||||||
int? _dateModifiedSecs;
|
int? _dateModifiedSecs;
|
||||||
final int? sourceDateTakenMillis;
|
int? sourceDateTakenMillis;
|
||||||
int? _durationMillis;
|
int? _durationMillis;
|
||||||
int? _catalogDateMillis;
|
int? _catalogDateMillis;
|
||||||
CatalogMetadata? _catalogMetadata;
|
CatalogMetadata? _catalogMetadata;
|
||||||
|
@ -564,8 +564,13 @@ class AvesEntry {
|
||||||
if (path is String) this.path = path;
|
if (path is String) this.path = path;
|
||||||
final contentId = newFields['contentId'];
|
final contentId = newFields['contentId'];
|
||||||
if (contentId is int) this.contentId = contentId;
|
if (contentId is int) this.contentId = contentId;
|
||||||
|
|
||||||
final sourceTitle = newFields['title'];
|
final sourceTitle = newFields['title'];
|
||||||
if (sourceTitle is String) this.sourceTitle = sourceTitle;
|
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'];
|
final width = newFields['width'];
|
||||||
if (width is int) this.width = width;
|
if (width is int) this.width = width;
|
||||||
|
@ -591,6 +596,24 @@ class AvesEntry {
|
||||||
metadataChangeNotifier.notifyListeners();
|
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 {
|
Future<bool> rotate({required bool clockwise, required bool persist}) async {
|
||||||
final newFields = await metadataEditService.rotate(this, clockwise: clockwise);
|
final newFields = await metadataEditService.rotate(this, clockwise: clockwise);
|
||||||
if (newFields.isEmpty) return false;
|
if (newFields.isEmpty) return false;
|
||||||
|
@ -619,8 +642,7 @@ class AvesEntry {
|
||||||
final newFields = await metadataEditService.editDate(this, modifier);
|
final newFields = await metadataEditService.editDate(this, modifier);
|
||||||
if (newFields.isEmpty) return false;
|
if (newFields.isEmpty) return false;
|
||||||
|
|
||||||
await _applyNewFields(newFields, persist: persist);
|
await refresh(persist: persist);
|
||||||
await catalog(background: false, persist: persist, force: true);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -628,7 +650,7 @@ class AvesEntry {
|
||||||
final newFields = await metadataEditService.removeTypes(this, types);
|
final newFields = await metadataEditService.removeTypes(this, types);
|
||||||
if (newFields.isEmpty) return false;
|
if (newFields.isEmpty) return false;
|
||||||
|
|
||||||
await _applyNewFields(newFields, persist: persist);
|
await refresh(persist: persist);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -130,6 +130,7 @@ class MediaStoreSource extends CollectionSource {
|
||||||
Future<Set<String>> refreshUris(Set<String> changedUris) async {
|
Future<Set<String>> refreshUris(Set<String> changedUris) async {
|
||||||
if (!_initialized || !isMonitoring) return changedUris;
|
if (!_initialized || !isMonitoring) return changedUris;
|
||||||
|
|
||||||
|
debugPrint('$runtimeType refreshUris ${changedUris.length} uris');
|
||||||
final uriByContentId = Map.fromEntries(changedUris.map((uri) {
|
final uriByContentId = Map.fromEntries(changedUris.map((uri) {
|
||||||
final pathSegments = Uri.parse(uri).pathSegments;
|
final pathSegments = Uri.parse(uri).pathSegments;
|
||||||
// e.g. URI `content://media/` has no path segment
|
// e.g. URI `content://media/` has no path segment
|
||||||
|
|
|
@ -109,14 +109,14 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
||||||
Future<void> _flip(BuildContext context, AvesEntry entry) async {
|
Future<void> _flip(BuildContext context, AvesEntry entry) async {
|
||||||
if (!await checkStoragePermission(context, {entry})) return;
|
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);
|
if (!success) showFeedback(context, context.l10n.genericFailureFeedback);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _rotate(BuildContext context, AvesEntry entry, {required bool clockwise}) async {
|
Future<void> _rotate(BuildContext context, AvesEntry entry, {required bool clockwise}) async {
|
||||||
if (!await checkStoragePermission(context, {entry})) return;
|
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);
|
if (!success) showFeedback(context, context.l10n.genericFailureFeedback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -269,7 +269,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
||||||
|
|
||||||
if (!await checkStoragePermission(context, {entry})) return;
|
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) {
|
if (success) {
|
||||||
showFeedback(context, context.l10n.genericSuccessFeedback);
|
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) {
|
void _goToSourceViewer(BuildContext context, AvesEntry entry) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
|
|
90
lib/widgets/viewer/info/entry_info_action_delegate.dart
Normal file
90
lib/widgets/viewer/info/entry_info_action_delegate.dart
Normal 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)));
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,24 +1,17 @@
|
||||||
import 'package:aves/model/actions/entry_info_actions.dart';
|
import 'package:aves/model/actions/entry_info_actions.dart';
|
||||||
import 'package:aves/model/entry.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/durations.dart';
|
||||||
import 'package:aves/theme/icons.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/app_bar_title.dart';
|
||||||
import 'package:aves/widgets/common/basic/menu.dart';
|
import 'package:aves/widgets/common/basic/menu.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/dialogs/edit_entry_date_dialog.dart';
|
import 'package:aves/widgets/viewer/info/entry_info_action_delegate.dart';
|
||||||
import 'package:aves/widgets/dialogs/remove_entry_metadata_dialog.dart';
|
|
||||||
import 'package:aves/widgets/viewer/info/info_search.dart';
|
import 'package:aves/widgets/viewer/info/info_search.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/metadata_section.dart';
|
import 'package:aves/widgets/viewer/info/metadata/metadata_section.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.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 AvesEntry entry;
|
||||||
final ValueNotifier<Map<String, MetadataDirectory>> metadataNotifier;
|
final ValueNotifier<Map<String, MetadataDirectory>> metadataNotifier;
|
||||||
final VoidCallback onBackPressed;
|
final VoidCallback onBackPressed;
|
||||||
|
@ -68,7 +61,7 @@ class InfoAppBar extends StatelessWidget with FeedbackMixin, PermissionAwareMixi
|
||||||
},
|
},
|
||||||
onSelected: (action) {
|
onSelected: (action) {
|
||||||
// wait for the popup menu to hide before proceeding with the 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue