collection: rotate/flip in bulk
This commit is contained in:
parent
3c682b37b5
commit
bd47d52412
4 changed files with 99 additions and 16 deletions
|
@ -20,6 +20,9 @@ enum EntrySetAction {
|
||||||
copy,
|
copy,
|
||||||
move,
|
move,
|
||||||
rescan,
|
rescan,
|
||||||
|
rotateCCW,
|
||||||
|
rotateCW,
|
||||||
|
flip,
|
||||||
editDate,
|
editDate,
|
||||||
removeMetadata,
|
removeMetadata,
|
||||||
}
|
}
|
||||||
|
@ -69,6 +72,12 @@ extension ExtraEntrySetAction on EntrySetAction {
|
||||||
return context.l10n.collectionActionMove;
|
return context.l10n.collectionActionMove;
|
||||||
case EntrySetAction.rescan:
|
case EntrySetAction.rescan:
|
||||||
return context.l10n.collectionActionRescan;
|
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:
|
case EntrySetAction.editDate:
|
||||||
return context.l10n.entryInfoActionEditDate;
|
return context.l10n.entryInfoActionEditDate;
|
||||||
case EntrySetAction.removeMetadata:
|
case EntrySetAction.removeMetadata:
|
||||||
|
@ -112,6 +121,12 @@ extension ExtraEntrySetAction on EntrySetAction {
|
||||||
return AIcons.move;
|
return AIcons.move;
|
||||||
case EntrySetAction.rescan:
|
case EntrySetAction.rescan:
|
||||||
return AIcons.refresh;
|
return AIcons.refresh;
|
||||||
|
case EntrySetAction.rotateCCW:
|
||||||
|
return AIcons.rotateLeft;
|
||||||
|
case EntrySetAction.rotateCW:
|
||||||
|
return AIcons.rotateRight;
|
||||||
|
case EntrySetAction.flip:
|
||||||
|
return AIcons.flip;
|
||||||
case EntrySetAction.editDate:
|
case EntrySetAction.editDate:
|
||||||
return AIcons.date;
|
return AIcons.date;
|
||||||
case EntrySetAction.removeMetadata:
|
case EntrySetAction.removeMetadata:
|
||||||
|
|
|
@ -222,6 +222,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
icon: AIcons.edit,
|
icon: AIcons.edit,
|
||||||
title: context.l10n.collectionActionEdit,
|
title: context.l10n.collectionActionEdit,
|
||||||
items: [
|
items: [
|
||||||
|
_buildRotateAndFlipMenuItems(context, enabled: hasSelection),
|
||||||
_toMenuItem(EntrySetAction.editDate, enabled: hasSelection),
|
_toMenuItem(EntrySetAction.editDate, enabled: hasSelection),
|
||||||
_toMenuItem(EntrySetAction.removeMetadata, 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() {
|
void _onActivityChange() {
|
||||||
if (context.read<Selection<AvesEntry>>().isSelecting) {
|
if (context.read<Selection<AvesEntry>>().isSelecting) {
|
||||||
_browseToSelectAnimation.forward();
|
_browseToSelectAnimation.forward();
|
||||||
|
@ -303,6 +339,9 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
case EntrySetAction.rescan:
|
case EntrySetAction.rescan:
|
||||||
case EntrySetAction.map:
|
case EntrySetAction.map:
|
||||||
case EntrySetAction.stats:
|
case EntrySetAction.stats:
|
||||||
|
case EntrySetAction.rotateCCW:
|
||||||
|
case EntrySetAction.rotateCW:
|
||||||
|
case EntrySetAction.flip:
|
||||||
case EntrySetAction.editDate:
|
case EntrySetAction.editDate:
|
||||||
case EntrySetAction.removeMetadata:
|
case EntrySetAction.removeMetadata:
|
||||||
_actionDelegate.onActionSelected(context, action);
|
_actionDelegate.onActionSelected(context, action);
|
||||||
|
|
|
@ -49,6 +49,15 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
||||||
case EntrySetAction.rescan:
|
case EntrySetAction.rescan:
|
||||||
_rescan(context);
|
_rescan(context);
|
||||||
break;
|
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:
|
case EntrySetAction.editDate:
|
||||||
_editDate(context);
|
_editDate(context);
|
||||||
break;
|
break;
|
||||||
|
@ -313,12 +322,16 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _checkEditableFormats(
|
Future<Set<AvesEntry>?> _getEditableItems(
|
||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
required Set<AvesEntry> supported,
|
required Set<AvesEntry> selectedItems,
|
||||||
required Set<AvesEntry> unsupported,
|
required bool Function(AvesEntry entry) canEdit,
|
||||||
}) async {
|
}) 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 unsupportedTypes = unsupported.map((entry) => entry.mimeType).toSet().map(MimeUtils.displayType).toList()..sort();
|
||||||
final confirmed = await showDialog<bool>(
|
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 {
|
Future<void> _editDate(BuildContext context) async {
|
||||||
final selection = context.read<Selection<AvesEntry>>();
|
final selection = context.read<Selection<AvesEntry>>();
|
||||||
final selectedItems = _getExpandedSelectedItems(selection);
|
final selectedItems = _getExpandedSelectedItems(selection);
|
||||||
|
|
||||||
final bySupported = groupBy<AvesEntry, bool>(selectedItems, (entry) => entry.canEditExif);
|
final todoItems = await _getEditableItems(context, selectedItems: selectedItems, canEdit: (entry) => entry.canEditExif);
|
||||||
final todoItems = (bySupported[true] ?? []).toSet();
|
if (todoItems == null || todoItems.isEmpty) return;
|
||||||
final unsupported = (bySupported[false] ?? []).toSet();
|
|
||||||
if (!await _checkEditableFormats(context, supported: todoItems, unsupported: unsupported)) return;
|
|
||||||
|
|
||||||
final modifier = await selectDateModifier(context, todoItems);
|
final modifier = await selectDateModifier(context, todoItems);
|
||||||
if (modifier == null) return;
|
if (modifier == null) return;
|
||||||
|
@ -367,13 +398,11 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
||||||
final selection = context.read<Selection<AvesEntry>>();
|
final selection = context.read<Selection<AvesEntry>>();
|
||||||
final selectedItems = _getExpandedSelectedItems(selection);
|
final selectedItems = _getExpandedSelectedItems(selection);
|
||||||
|
|
||||||
final bySupported = groupBy<AvesEntry, bool>(selectedItems, (entry) => entry.canRemoveMetadata);
|
final todoItems = await _getEditableItems(context, selectedItems: selectedItems, canEdit: (entry) => entry.canRemoveMetadata);
|
||||||
final todoItems = (bySupported[true] ?? []).toSet();
|
if (todoItems == null || todoItems.isEmpty) return;
|
||||||
final unsupported = (bySupported[false] ?? []).toSet();
|
|
||||||
if (!await _checkEditableFormats(context, supported: todoItems, unsupported: unsupported)) return;
|
|
||||||
|
|
||||||
final types = await selectMetadataToRemove(context, todoItems);
|
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));
|
await _edit(context, selection, todoItems, (entry) => entry.removeMetadata(types));
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,9 +39,9 @@ class ViewerActionEditorPage extends StatelessWidget {
|
||||||
EntryAction.copyToClipboard,
|
EntryAction.copyToClipboard,
|
||||||
EntryAction.print,
|
EntryAction.print,
|
||||||
EntryAction.rotateScreen,
|
EntryAction.rotateScreen,
|
||||||
EntryAction.flip,
|
|
||||||
EntryAction.rotateCCW,
|
EntryAction.rotateCCW,
|
||||||
EntryAction.rotateCW,
|
EntryAction.rotateCW,
|
||||||
|
EntryAction.flip,
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
Loading…
Reference in a new issue