collection: rotate/flip in bulk

This commit is contained in:
Thibault Deckers 2021-10-31 11:32:17 +09:00
parent 3c682b37b5
commit bd47d52412
4 changed files with 99 additions and 16 deletions

View file

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

View file

@ -222,6 +222,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> 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<CollectionAppBar> with SingleTickerPr
);
}
PopupMenuItem<EntrySetAction> _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<Selection<AvesEntry>>().isSelecting) {
_browseToSelectAnimation.forward();
@ -303,6 +339,9 @@ class _CollectionAppBarState extends State<CollectionAppBar> 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);

View file

@ -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<bool> _checkEditableFormats(
Future<Set<AvesEntry>?> _getEditableItems(
BuildContext context, {
required Set<AvesEntry> supported,
required Set<AvesEntry> unsupported,
required Set<AvesEntry> selectedItems,
required bool Function(AvesEntry entry) canEdit,
}) async {
if (unsupported.isEmpty) return true;
final bySupported = groupBy<AvesEntry, bool>(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<bool>(
@ -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<void> _rotate(BuildContext context, {required bool clockwise}) async {
final selection = context.read<Selection<AvesEntry>>();
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<void> _flip(BuildContext context) async {
final selection = context.read<Selection<AvesEntry>>();
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<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 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<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 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));
}

View file

@ -39,9 +39,9 @@ class ViewerActionEditorPage extends StatelessWidget {
EntryAction.copyToClipboard,
EntryAction.print,
EntryAction.rotateScreen,
EntryAction.flip,
EntryAction.rotateCCW,
EntryAction.rotateCW,
EntryAction.flip,
];
@override