collection: remove metadata in bulk
This commit is contained in:
parent
0bf238245f
commit
3c682b37b5
9 changed files with 171 additions and 107 deletions
|
@ -388,7 +388,7 @@
|
||||||
"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": "XMP is required to play the video inside a motion photo. Are you sure you want to remove it?",
|
||||||
"@removeEntryMetadataMotionPhotoXmpWarningDialogMessage": {},
|
"@removeEntryMetadataMotionPhotoXmpWarningDialogMessage": {},
|
||||||
|
|
||||||
"videoSpeedDialogLabel": "Playback speed",
|
"videoSpeedDialogLabel": "Playback speed",
|
||||||
|
|
|
@ -173,7 +173,7 @@
|
||||||
"removeEntryMetadataDialogTitle": "메타데이터 삭제",
|
"removeEntryMetadataDialogTitle": "메타데이터 삭제",
|
||||||
"removeEntryMetadataDialogMore": "더 보기",
|
"removeEntryMetadataDialogMore": "더 보기",
|
||||||
|
|
||||||
"removeEntryMetadataMotionPhotoXmpWarningDialogMessage": "XMP가 있어야 모션 포토에 포함되는 동영상을 재생할 수 있습니다. 삭제하시겠습니까?",
|
"removeEntryMetadataMotionPhotoXmpWarningDialogMessage": "XMP가 있어야 모션 포토에 포함되는 동영상을 재생할 수 있습니다. 삭제하시겠습니까?",
|
||||||
|
|
||||||
"videoSpeedDialogLabel": "재생 배속",
|
"videoSpeedDialogLabel": "재생 배속",
|
||||||
|
|
||||||
|
|
|
@ -173,7 +173,7 @@
|
||||||
"removeEntryMetadataDialogTitle": "Удаление метаданных",
|
"removeEntryMetadataDialogTitle": "Удаление метаданных",
|
||||||
"removeEntryMetadataDialogMore": "Дополнительно",
|
"removeEntryMetadataDialogMore": "Дополнительно",
|
||||||
|
|
||||||
"removeEntryMetadataMotionPhotoXmpWarningDialogMessage": "Для воспроизведения видео внутри этой живой фотографии требуется XMP профиль. Вы уверены, что хотите удалить его?",
|
"removeEntryMetadataMotionPhotoXmpWarningDialogMessage": "Для воспроизведения видео внутри этой живой фотографии требуется XMP профиль. Вы уверены, что хотите удалить его?",
|
||||||
|
|
||||||
"videoSpeedDialogLabel": "Скорость воспроизведения",
|
"videoSpeedDialogLabel": "Скорость воспроизведения",
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ enum EntrySetAction {
|
||||||
move,
|
move,
|
||||||
rescan,
|
rescan,
|
||||||
editDate,
|
editDate,
|
||||||
|
removeMetadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
class EntrySetActions {
|
class EntrySetActions {
|
||||||
|
@ -70,6 +71,8 @@ extension ExtraEntrySetAction on EntrySetAction {
|
||||||
return context.l10n.collectionActionRescan;
|
return context.l10n.collectionActionRescan;
|
||||||
case EntrySetAction.editDate:
|
case EntrySetAction.editDate:
|
||||||
return context.l10n.entryInfoActionEditDate;
|
return context.l10n.entryInfoActionEditDate;
|
||||||
|
case EntrySetAction.removeMetadata:
|
||||||
|
return context.l10n.entryInfoActionRemoveMetadata;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,6 +114,8 @@ extension ExtraEntrySetAction on EntrySetAction {
|
||||||
return AIcons.refresh;
|
return AIcons.refresh;
|
||||||
case EntrySetAction.editDate:
|
case EntrySetAction.editDate:
|
||||||
return AIcons.date;
|
return AIcons.date;
|
||||||
|
case EntrySetAction.removeMetadata:
|
||||||
|
return AIcons.clear;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -223,6 +223,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
title: context.l10n.collectionActionEdit,
|
title: context.l10n.collectionActionEdit,
|
||||||
items: [
|
items: [
|
||||||
_toMenuItem(EntrySetAction.editDate, enabled: hasSelection),
|
_toMenuItem(EntrySetAction.editDate, enabled: hasSelection),
|
||||||
|
_toMenuItem(EntrySetAction.removeMetadata, enabled: hasSelection),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -303,6 +304,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
case EntrySetAction.map:
|
case EntrySetAction.map:
|
||||||
case EntrySetAction.stats:
|
case EntrySetAction.stats:
|
||||||
case EntrySetAction.editDate:
|
case EntrySetAction.editDate:
|
||||||
|
case EntrySetAction.removeMetadata:
|
||||||
_actionDelegate.onActionSelected(context, action);
|
_actionDelegate.onActionSelected(context, action);
|
||||||
break;
|
break;
|
||||||
case EntrySetAction.select:
|
case EntrySetAction.select:
|
||||||
|
|
|
@ -6,7 +6,6 @@ import 'package:aves/model/actions/move_type.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/highlight.dart';
|
import 'package:aves/model/highlight.dart';
|
||||||
import 'package:aves/model/metadata/date_modifier.dart';
|
|
||||||
import 'package:aves/model/selection.dart';
|
import 'package:aves/model/selection.dart';
|
||||||
import 'package:aves/model/source/analysis_controller.dart';
|
import 'package:aves/model/source/analysis_controller.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
|
@ -17,13 +16,13 @@ import 'package:aves/services/media/enums.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/utils/mime_utils.dart';
|
import 'package:aves/utils/mime_utils.dart';
|
||||||
import 'package:aves/widgets/collection/collection_page.dart';
|
import 'package:aves/widgets/collection/collection_page.dart';
|
||||||
|
import 'package:aves/widgets/common/action_mixins/entry_editor.dart';
|
||||||
import 'package:aves/widgets/common/action_mixins/feedback.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/action_mixins/permission_aware.dart';
|
||||||
import 'package:aves/widgets/common/action_mixins/size_aware.dart';
|
import 'package:aves/widgets/common/action_mixins/size_aware.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||||
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
|
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
|
||||||
import 'package:aves/widgets/dialogs/edit_entry_date_dialog.dart';
|
|
||||||
import 'package:aves/widgets/filter_grids/album_pick.dart';
|
import 'package:aves/widgets/filter_grids/album_pick.dart';
|
||||||
import 'package:aves/widgets/map/map_page.dart';
|
import 'package:aves/widgets/map/map_page.dart';
|
||||||
import 'package:aves/widgets/stats/stats_page.dart';
|
import 'package:aves/widgets/stats/stats_page.dart';
|
||||||
|
@ -32,7 +31,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
|
class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
|
||||||
void onActionSelected(BuildContext context, EntrySetAction action) {
|
void onActionSelected(BuildContext context, EntrySetAction action) {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case EntrySetAction.share:
|
case EntrySetAction.share:
|
||||||
|
@ -53,6 +52,9 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
||||||
case EntrySetAction.editDate:
|
case EntrySetAction.editDate:
|
||||||
_editDate(context);
|
_editDate(context);
|
||||||
break;
|
break;
|
||||||
|
case EntrySetAction.removeMetadata:
|
||||||
|
_removeMetadata(context);
|
||||||
|
break;
|
||||||
case EntrySetAction.map:
|
case EntrySetAction.map:
|
||||||
_goToMap(context);
|
_goToMap(context);
|
||||||
break;
|
break;
|
||||||
|
@ -163,15 +165,15 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
||||||
|
|
||||||
// do not directly use selection when moving and post-processing items
|
// do not directly use selection when moving and post-processing items
|
||||||
// as source monitoring may remove obsolete items from the original selection
|
// as source monitoring may remove obsolete items from the original selection
|
||||||
final todoEntries = selectedItems.toSet();
|
final todoItems = selectedItems.toSet();
|
||||||
|
|
||||||
final copy = moveType == MoveType.copy;
|
final copy = moveType == MoveType.copy;
|
||||||
final todoCount = todoEntries.length;
|
final todoCount = todoItems.length;
|
||||||
assert(todoCount > 0);
|
assert(todoCount > 0);
|
||||||
|
|
||||||
final destinationDirectory = Directory(destinationAlbum);
|
final destinationDirectory = Directory(destinationAlbum);
|
||||||
final names = [
|
final names = [
|
||||||
...todoEntries.map((v) => '${v.filenameWithoutExtension}${v.extension}'),
|
...todoItems.map((v) => '${v.filenameWithoutExtension}${v.extension}'),
|
||||||
// do not guard up front based on directory existence,
|
// do not guard up front based on directory existence,
|
||||||
// as conflicts could be within moved entries scattered across multiple albums
|
// as conflicts could be within moved entries scattered across multiple albums
|
||||||
if (await destinationDirectory.exists()) ...destinationDirectory.listSync().map((v) => pContext.basename(v.path)),
|
if (await destinationDirectory.exists()) ...destinationDirectory.listSync().map((v) => pContext.basename(v.path)),
|
||||||
|
@ -198,7 +200,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
||||||
showOpReport<MoveOpEvent>(
|
showOpReport<MoveOpEvent>(
|
||||||
context: context,
|
context: context,
|
||||||
opStream: mediaFileService.move(
|
opStream: mediaFileService.move(
|
||||||
todoEntries,
|
todoItems,
|
||||||
copy: copy,
|
copy: copy,
|
||||||
destinationAlbum: destinationAlbum,
|
destinationAlbum: destinationAlbum,
|
||||||
nameConflictStrategy: nameConflictStrategy,
|
nameConflictStrategy: nameConflictStrategy,
|
||||||
|
@ -208,7 +210,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
||||||
final successOps = processed.where((e) => e.success).toSet();
|
final successOps = processed.where((e) => e.success).toSet();
|
||||||
final movedOps = successOps.where((e) => !e.newFields.containsKey('skipped')).toSet();
|
final movedOps = successOps.where((e) => !e.newFields.containsKey('skipped')).toSet();
|
||||||
await source.updateAfterMove(
|
await source.updateAfterMove(
|
||||||
todoEntries: todoEntries,
|
todoEntries: todoItems,
|
||||||
copy: copy,
|
copy: copy,
|
||||||
destinationAlbum: destinationAlbum,
|
destinationAlbum: destinationAlbum,
|
||||||
movedOps: movedOps,
|
movedOps: movedOps,
|
||||||
|
@ -272,57 +274,23 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _editDate(BuildContext context) async {
|
Future<void> _edit(
|
||||||
final l10n = context.l10n;
|
BuildContext context,
|
||||||
|
Selection<AvesEntry> selection,
|
||||||
|
Set<AvesEntry> todoItems,
|
||||||
|
Future<bool> Function(AvesEntry entry) op,
|
||||||
|
) async {
|
||||||
|
final selectionDirs = todoItems.map((e) => e.directory).whereNotNull().toSet();
|
||||||
|
final todoCount = todoItems.length;
|
||||||
|
|
||||||
|
if (!await checkStoragePermissionForAlbums(context, selectionDirs, entries: todoItems)) return;
|
||||||
|
|
||||||
final source = context.read<CollectionSource>();
|
final source = context.read<CollectionSource>();
|
||||||
final selection = context.read<Selection<AvesEntry>>();
|
|
||||||
final selectedItems = _getExpandedSelectedItems(selection);
|
|
||||||
|
|
||||||
final bySupported = groupBy<AvesEntry, bool>(selectedItems, (entry) => entry.canEditExif);
|
|
||||||
final todoEntries = (bySupported[true] ?? []).toSet();
|
|
||||||
final unsupportedItems = (bySupported[false] ?? []);
|
|
||||||
if (unsupportedItems.isNotEmpty) {
|
|
||||||
final unsupportedTypes = unsupportedItems.map((entry) => entry.mimeType).toSet().map(MimeUtils.displayType).toList()..sort();
|
|
||||||
final confirmed = await showDialog<bool>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) {
|
|
||||||
return AvesDialog(
|
|
||||||
context: context,
|
|
||||||
title: l10n.unsupportedTypeDialogTitle,
|
|
||||||
content: Text(l10n.unsupportedTypeDialogMessage(unsupportedTypes.length, unsupportedTypes.join(', '))),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
|
|
||||||
),
|
|
||||||
if (todoEntries.isNotEmpty)
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context, true),
|
|
||||||
child: Text(l10n.continueButtonLabel),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (confirmed == null || !confirmed) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final selectionDirs = todoEntries.map((e) => e.directory).whereNotNull().toSet();
|
|
||||||
final todoCount = todoEntries.length;
|
|
||||||
|
|
||||||
final modifier = await showDialog<DateModifier>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => EditEntryDateDialog(entry: todoEntries.first),
|
|
||||||
);
|
|
||||||
if (modifier == null) return;
|
|
||||||
|
|
||||||
if (!await checkStoragePermissionForAlbums(context, selectionDirs, entries: todoEntries)) return;
|
|
||||||
|
|
||||||
source.pauseMonitoring();
|
source.pauseMonitoring();
|
||||||
showOpReport<ImageOpEvent>(
|
showOpReport<ImageOpEvent>(
|
||||||
context: context,
|
context: context,
|
||||||
opStream: Stream.fromIterable(todoEntries).asyncMap((entry) async {
|
opStream: Stream.fromIterable(todoItems).asyncMap((entry) async {
|
||||||
final success = await entry.editDate(modifier);
|
final success = await op(entry);
|
||||||
return ImageOpEvent(success: success, uri: entry.uri);
|
return ImageOpEvent(success: success, uri: entry.uri);
|
||||||
}).asBroadcastStream(),
|
}).asBroadcastStream(),
|
||||||
itemCount: todoCount,
|
itemCount: todoCount,
|
||||||
|
@ -332,6 +300,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
||||||
source.resumeMonitoring();
|
source.resumeMonitoring();
|
||||||
unawaited(source.refreshUris(successOps.map((v) => v.uri).toSet()));
|
unawaited(source.refreshUris(successOps.map((v) => v.uri).toSet()));
|
||||||
|
|
||||||
|
final l10n = context.l10n;
|
||||||
final successCount = successOps.length;
|
final successCount = successOps.length;
|
||||||
if (successCount < todoCount) {
|
if (successCount < todoCount) {
|
||||||
final count = todoCount - successCount;
|
final count = todoCount - successCount;
|
||||||
|
@ -344,6 +313,71 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> _checkEditableFormats(
|
||||||
|
BuildContext context, {
|
||||||
|
required Set<AvesEntry> supported,
|
||||||
|
required Set<AvesEntry> unsupported,
|
||||||
|
}) async {
|
||||||
|
if (unsupported.isEmpty) return true;
|
||||||
|
|
||||||
|
final unsupportedTypes = unsupported.map((entry) => entry.mimeType).toSet().map(MimeUtils.displayType).toList()..sort();
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
return AvesDialog(
|
||||||
|
context: context,
|
||||||
|
title: l10n.unsupportedTypeDialogTitle,
|
||||||
|
content: Text(l10n.unsupportedTypeDialogMessage(unsupportedTypes.length, unsupportedTypes.join(', '))),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
|
||||||
|
),
|
||||||
|
if (supported.isNotEmpty)
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, true),
|
||||||
|
child: Text(l10n.continueButtonLabel),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (confirmed == null || !confirmed) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _editDate(BuildContext context) async {
|
||||||
|
final selection = context.read<Selection<AvesEntry>>();
|
||||||
|
final selectedItems = _getExpandedSelectedItems(selection);
|
||||||
|
|
||||||
|
final bySupported = groupBy<AvesEntry, bool>(selectedItems, (entry) => entry.canEditExif);
|
||||||
|
final todoItems = (bySupported[true] ?? []).toSet();
|
||||||
|
final unsupported = (bySupported[false] ?? []).toSet();
|
||||||
|
if (!await _checkEditableFormats(context, supported: todoItems, unsupported: unsupported)) return;
|
||||||
|
|
||||||
|
final modifier = await selectDateModifier(context, todoItems);
|
||||||
|
if (modifier == null) return;
|
||||||
|
|
||||||
|
await _edit(context, selection, todoItems, (entry) => entry.editDate(modifier));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _removeMetadata(BuildContext context) async {
|
||||||
|
final selection = context.read<Selection<AvesEntry>>();
|
||||||
|
final selectedItems = _getExpandedSelectedItems(selection);
|
||||||
|
|
||||||
|
final bySupported = groupBy<AvesEntry, bool>(selectedItems, (entry) => entry.canRemoveMetadata);
|
||||||
|
final todoItems = (bySupported[true] ?? []).toSet();
|
||||||
|
final unsupported = (bySupported[false] ?? []).toSet();
|
||||||
|
if (!await _checkEditableFormats(context, supported: todoItems, unsupported: unsupported)) return;
|
||||||
|
|
||||||
|
final types = await selectMetadataToRemove(context, todoItems);
|
||||||
|
if (types == null) return;
|
||||||
|
|
||||||
|
await _edit(context, selection, todoItems, (entry) => entry.removeMetadata(types));
|
||||||
|
}
|
||||||
|
|
||||||
void _goToMap(BuildContext context) {
|
void _goToMap(BuildContext context) {
|
||||||
final selection = context.read<Selection<AvesEntry>>();
|
final selection = context.read<Selection<AvesEntry>>();
|
||||||
final collection = context.read<CollectionLens>();
|
final collection = context.read<CollectionLens>();
|
||||||
|
|
60
lib/widgets/common/action_mixins/entry_editor.dart
Normal file
60
lib/widgets/common/action_mixins/entry_editor.dart
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import 'package:aves/model/entry.dart';
|
||||||
|
import 'package:aves/model/metadata/date_modifier.dart';
|
||||||
|
import 'package:aves/model/metadata/enums.dart';
|
||||||
|
import 'package:aves/ref/mime_types.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';
|
||||||
|
|
||||||
|
mixin EntryEditorMixin {
|
||||||
|
Future<DateModifier?> selectDateModifier(BuildContext context, Set<AvesEntry> entries) async {
|
||||||
|
if (entries.isEmpty) return null;
|
||||||
|
|
||||||
|
final modifier = await showDialog<DateModifier>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => EditEntryDateDialog(
|
||||||
|
entry: entries.first,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return modifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Set<MetadataType>?> selectMetadataToRemove(BuildContext context, Set<AvesEntry> entries) async {
|
||||||
|
if (entries.isEmpty) return null;
|
||||||
|
|
||||||
|
final types = await showDialog<Set<MetadataType>>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => RemoveEntryMetadataDialog(
|
||||||
|
showJpegTypes: entries.any((entry) => entry.mimeType == MimeTypes.jpeg),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (types == null || types.isEmpty) return null;
|
||||||
|
|
||||||
|
if (entries.any((entry) => entry.isMotionPhoto) && types.contains(MetadataType.xmp)) {
|
||||||
|
final confirmed = 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 (confirmed == null || !confirmed) return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return types;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,5 @@
|
||||||
import 'package:aves/model/entry.dart';
|
|
||||||
import 'package:aves/model/metadata/enums.dart';
|
import 'package:aves/model/metadata/enums.dart';
|
||||||
import 'package:aves/ref/brand_colors.dart';
|
import 'package:aves/ref/brand_colors.dart';
|
||||||
import 'package:aves/ref/mime_types.dart';
|
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/utils/color_utils.dart';
|
import 'package:aves/utils/color_utils.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
|
@ -14,11 +12,11 @@ import 'package:provider/provider.dart';
|
||||||
import 'aves_dialog.dart';
|
import 'aves_dialog.dart';
|
||||||
|
|
||||||
class RemoveEntryMetadataDialog extends StatefulWidget {
|
class RemoveEntryMetadataDialog extends StatefulWidget {
|
||||||
final AvesEntry entry;
|
final bool showJpegTypes;
|
||||||
|
|
||||||
const RemoveEntryMetadataDialog({
|
const RemoveEntryMetadataDialog({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.entry,
|
required this.showJpegTypes,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -31,14 +29,12 @@ class _RemoveEntryMetadataDialogState extends State<RemoveEntryMetadataDialog> {
|
||||||
bool _showMore = false;
|
bool _showMore = false;
|
||||||
final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false);
|
final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false);
|
||||||
|
|
||||||
AvesEntry get entry => widget.entry;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
final byMain = groupBy([
|
final byMain = groupBy([
|
||||||
...MetadataTypes.common,
|
...MetadataTypes.common,
|
||||||
if (entry.mimeType == MimeTypes.jpeg) ...MetadataTypes.jpeg,
|
if (widget.showJpegTypes) ...MetadataTypes.jpeg,
|
||||||
], MetadataTypes.main.contains);
|
], MetadataTypes.main.contains);
|
||||||
_mainOptions = (byMain[true] ?? [])..sort(_compareTypeText);
|
_mainOptions = (byMain[true] ?? [])..sort(_compareTypeText);
|
||||||
_moreOptions = (byMain[false] ?? [])..sort(_compareTypeText);
|
_moreOptions = (byMain[false] ?? [])..sort(_compareTypeText);
|
||||||
|
|
|
@ -1,20 +1,16 @@
|
||||||
import 'package:aves/app_mode.dart';
|
import 'package:aves/app_mode.dart';
|
||||||
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/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
|
import 'package:aves/widgets/common/action_mixins/entry_editor.dart';
|
||||||
import 'package:aves/widgets/common/action_mixins/feedback.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/action_mixins/permission_aware.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.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:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
class EntryInfoActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwareMixin {
|
||||||
final AvesEntry entry;
|
final AvesEntry entry;
|
||||||
|
|
||||||
const EntryInfoActionDelegate(this.entry);
|
const EntryInfoActionDelegate(this.entry);
|
||||||
|
@ -22,10 +18,10 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
||||||
void onActionSelected(BuildContext context, EntryInfoAction action) async {
|
void onActionSelected(BuildContext context, EntryInfoAction action) async {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case EntryInfoAction.editDate:
|
case EntryInfoAction.editDate:
|
||||||
await _showDateEditDialog(context);
|
await _editDate(context);
|
||||||
break;
|
break;
|
||||||
case EntryInfoAction.removeMetadata:
|
case EntryInfoAction.removeMetadata:
|
||||||
await _showMetadataRemovalDialog(context);
|
await _removeMetadata(context);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,45 +48,16 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
||||||
source?.resumeMonitoring();
|
source?.resumeMonitoring();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showDateEditDialog(BuildContext context) async {
|
Future<void> _editDate(BuildContext context) async {
|
||||||
final modifier = await showDialog<DateModifier>(
|
final modifier = await selectDateModifier(context, {entry});
|
||||||
context: context,
|
|
||||||
builder: (context) => EditEntryDateDialog(entry: entry),
|
|
||||||
);
|
|
||||||
if (modifier == null) return;
|
if (modifier == null) return;
|
||||||
|
|
||||||
await _edit(context, () => entry.editDate(modifier));
|
await _edit(context, () => entry.editDate(modifier));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showMetadataRemovalDialog(BuildContext context) async {
|
Future<void> _removeMetadata(BuildContext context) async {
|
||||||
final types = await showDialog<Set<MetadataType>>(
|
final types = await selectMetadataToRemove(context, {entry});
|
||||||
context: context,
|
if (types == null) return;
|
||||||
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));
|
await _edit(context, () => entry.removeMetadata(types));
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue