From bd47d52412560f99855380b846d7101615fd6814 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Sun, 31 Oct 2021 11:32:17 +0900 Subject: [PATCH] collection: rotate/flip in bulk --- lib/model/actions/entry_set_actions.dart | 15 +++++ lib/widgets/collection/app_bar.dart | 39 ++++++++++++ .../collection/entry_set_action_delegate.dart | 59 ++++++++++++++----- .../viewer/viewer_actions_editor.dart | 2 +- 4 files changed, 99 insertions(+), 16 deletions(-) diff --git a/lib/model/actions/entry_set_actions.dart b/lib/model/actions/entry_set_actions.dart index 15149d27a..180ea59e1 100644 --- a/lib/model/actions/entry_set_actions.dart +++ b/lib/model/actions/entry_set_actions.dart @@ -20,6 +20,9 @@ enum EntrySetAction { copy, move, rescan, + rotateCCW, + rotateCW, + flip, editDate, removeMetadata, } @@ -69,6 +72,12 @@ extension ExtraEntrySetAction on EntrySetAction { return context.l10n.collectionActionMove; case EntrySetAction.rescan: return context.l10n.collectionActionRescan; + case EntrySetAction.rotateCCW: + return context.l10n.entryActionRotateCCW; + case EntrySetAction.rotateCW: + return context.l10n.entryActionRotateCW; + case EntrySetAction.flip: + return context.l10n.entryActionFlip; case EntrySetAction.editDate: return context.l10n.entryInfoActionEditDate; case EntrySetAction.removeMetadata: @@ -112,6 +121,12 @@ extension ExtraEntrySetAction on EntrySetAction { return AIcons.move; case EntrySetAction.rescan: return AIcons.refresh; + case EntrySetAction.rotateCCW: + return AIcons.rotateLeft; + case EntrySetAction.rotateCW: + return AIcons.rotateRight; + case EntrySetAction.flip: + return AIcons.flip; case EntrySetAction.editDate: return AIcons.date; case EntrySetAction.removeMetadata: diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index 5cef27c02..a50bd38cb 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -222,6 +222,7 @@ class _CollectionAppBarState extends State with SingleTickerPr icon: AIcons.edit, title: context.l10n.collectionActionEdit, items: [ + _buildRotateAndFlipMenuItems(context, enabled: hasSelection), _toMenuItem(EntrySetAction.editDate, enabled: hasSelection), _toMenuItem(EntrySetAction.removeMetadata, enabled: hasSelection), ], @@ -273,6 +274,41 @@ class _CollectionAppBarState extends State with SingleTickerPr ); } + PopupMenuItem _buildRotateAndFlipMenuItems(BuildContext context, {required bool enabled}) { + Widget buildDivider() => const SizedBox( + height: 16, + child: VerticalDivider( + width: 1, + thickness: 1, + ), + ); + + Widget buildItem(EntrySetAction action) => Expanded( + child: PopupMenuItem( + value: action, + enabled: enabled, + child: Tooltip( + message: action.getText(context), + child: Center(child: action.getIcon()), + ), + ), + ); + + return PopupMenuItem( + child: Row( + children: [ + buildDivider(), + buildItem(EntrySetAction.rotateCCW), + buildDivider(), + buildItem(EntrySetAction.rotateCW), + buildDivider(), + buildItem(EntrySetAction.flip), + buildDivider(), + ], + ), + ); + } + void _onActivityChange() { if (context.read>().isSelecting) { _browseToSelectAnimation.forward(); @@ -303,6 +339,9 @@ class _CollectionAppBarState extends State with SingleTickerPr case EntrySetAction.rescan: case EntrySetAction.map: case EntrySetAction.stats: + case EntrySetAction.rotateCCW: + case EntrySetAction.rotateCW: + case EntrySetAction.flip: case EntrySetAction.editDate: case EntrySetAction.removeMetadata: _actionDelegate.onActionSelected(context, action); diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart index c34678891..00f9c5d47 100644 --- a/lib/widgets/collection/entry_set_action_delegate.dart +++ b/lib/widgets/collection/entry_set_action_delegate.dart @@ -49,6 +49,15 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa case EntrySetAction.rescan: _rescan(context); break; + case EntrySetAction.rotateCCW: + _rotate(context, clockwise: false); + break; + case EntrySetAction.rotateCW: + _rotate(context, clockwise: true); + break; + case EntrySetAction.flip: + _flip(context); + break; case EntrySetAction.editDate: _editDate(context); break; @@ -313,12 +322,16 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa ); } - Future _checkEditableFormats( + Future?> _getEditableItems( BuildContext context, { - required Set supported, - required Set unsupported, + required Set selectedItems, + required bool Function(AvesEntry entry) canEdit, }) async { - if (unsupported.isEmpty) return true; + final bySupported = groupBy(selectedItems, canEdit); + final supported = (bySupported[true] ?? []).toSet(); + final unsupported = (bySupported[false] ?? []).toSet(); + + if (unsupported.isEmpty) return supported; final unsupportedTypes = unsupported.map((entry) => entry.mimeType).toSet().map(MimeUtils.displayType).toList()..sort(); final confirmed = await showDialog( @@ -343,19 +356,37 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa ); }, ); - if (confirmed == null || !confirmed) return false; + if (confirmed == null || !confirmed) return null; - return true; + return supported; + } + + Future _rotate(BuildContext context, {required bool clockwise}) async { + final selection = context.read>(); + final selectedItems = _getExpandedSelectedItems(selection); + + final todoItems = await _getEditableItems(context, selectedItems: selectedItems, canEdit: (entry) => entry.canRotateAndFlip); + if (todoItems == null || todoItems.isEmpty) return; + + await _edit(context, selection, todoItems, (entry) => entry.rotate(clockwise: clockwise, persist: true)); + } + + Future _flip(BuildContext context) async { + final selection = context.read>(); + final selectedItems = _getExpandedSelectedItems(selection); + + final todoItems = await _getEditableItems(context, selectedItems: selectedItems, canEdit: (entry) => entry.canRotateAndFlip); + if (todoItems == null || todoItems.isEmpty) return; + + await _edit(context, selection, todoItems, (entry) => entry.flip(persist: true)); } Future _editDate(BuildContext context) async { final selection = context.read>(); final selectedItems = _getExpandedSelectedItems(selection); - final bySupported = groupBy(selectedItems, (entry) => entry.canEditExif); - final todoItems = (bySupported[true] ?? []).toSet(); - final unsupported = (bySupported[false] ?? []).toSet(); - if (!await _checkEditableFormats(context, supported: todoItems, unsupported: unsupported)) return; + final todoItems = await _getEditableItems(context, selectedItems: selectedItems, canEdit: (entry) => entry.canEditExif); + if (todoItems == null || todoItems.isEmpty) return; final modifier = await selectDateModifier(context, todoItems); if (modifier == null) return; @@ -367,13 +398,11 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa final selection = context.read>(); final selectedItems = _getExpandedSelectedItems(selection); - final bySupported = groupBy(selectedItems, (entry) => entry.canRemoveMetadata); - final todoItems = (bySupported[true] ?? []).toSet(); - final unsupported = (bySupported[false] ?? []).toSet(); - if (!await _checkEditableFormats(context, supported: todoItems, unsupported: unsupported)) return; + final todoItems = await _getEditableItems(context, selectedItems: selectedItems, canEdit: (entry) => entry.canRemoveMetadata); + if (todoItems == null || todoItems.isEmpty) return; final types = await selectMetadataToRemove(context, todoItems); - if (types == null) return; + if (types == null || types.isEmpty) return; await _edit(context, selection, todoItems, (entry) => entry.removeMetadata(types)); } diff --git a/lib/widgets/settings/viewer/viewer_actions_editor.dart b/lib/widgets/settings/viewer/viewer_actions_editor.dart index f0b048731..11e383422 100644 --- a/lib/widgets/settings/viewer/viewer_actions_editor.dart +++ b/lib/widgets/settings/viewer/viewer_actions_editor.dart @@ -39,9 +39,9 @@ class ViewerActionEditorPage extends StatelessWidget { EntryAction.copyToClipboard, EntryAction.print, EntryAction.rotateScreen, - EntryAction.flip, EntryAction.rotateCCW, EntryAction.rotateCW, + EntryAction.flip, ]; @override