#1201 keep selection when action on several items is interrupted before processing

This commit is contained in:
Thibault Deckers 2025-02-02 22:49:07 +01:00
parent 16da0ec3f5
commit 0a3a792a7e
3 changed files with 57 additions and 39 deletions

View file

@ -11,6 +11,8 @@ All notable changes to this project will be documented in this file.
### Changed ### Changed
- improved subsampling and filter quality strategy - improved subsampling and filter quality strategy
- ignore moving an item to its current directory
- keep selection when action on several items is interrupted before processing
- upgraded Flutter to stable v3.27.3 - upgraded Flutter to stable v3.27.3
### Fixed ### Fixed

View file

@ -292,26 +292,29 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
final details = vaults.getVault(entry.directory); final details = vaults.getVault(entry.directory);
return details?.useBin ?? settings.enableBin; return details?.useBin ?? settings.enableBin;
}); });
await Future.forEach( var completed = true;
byBinUsage.entries, await Future.forEach(byBinUsage.entries, (kv) async {
(kv) => doDelete( completed &= await doDelete(
context: context, context: context,
entries: kv.value.toSet(), entries: kv.value.toSet(),
enableBin: kv.key, enableBin: kv.key,
)); );
});
_browse(context); if (completed) {
_browse(context);
}
} }
Future<void> doDelete({ // returns whether it completed the action (with or without failures)
Future<bool> doDelete({
required BuildContext context, required BuildContext context,
required Set<AvesEntry> entries, required Set<AvesEntry> entries,
required bool enableBin, required bool enableBin,
}) async { }) async {
final pureTrash = entries.every((entry) => entry.trashed); final pureTrash = entries.every((entry) => entry.trashed);
if (enableBin && !pureTrash) { if (enableBin && !pureTrash) {
await doMove(context, moveType: MoveType.toBin, entries: entries); return await doMove(context, moveType: MoveType.toBin, entries: entries);
return;
} }
final l10n = context.l10n; final l10n = context.l10n;
@ -325,10 +328,10 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
message: l10n.deleteEntriesConfirmationDialogMessage(todoCount), message: l10n.deleteEntriesConfirmationDialogMessage(todoCount),
confirmationButtonLabel: l10n.deleteButtonLabel, confirmationButtonLabel: l10n.deleteButtonLabel,
)) { )) {
return; return false;
} }
if (!await checkStoragePermissionForAlbums(context, storageDirs, entries: entries)) return; if (!await checkStoragePermissionForAlbums(context, storageDirs, entries: entries)) return false;
source.pauseMonitoring(); source.pauseMonitoring();
final opId = mediaEditService.newOpId; final opId = mediaEditService.newOpId;
@ -354,13 +357,16 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
await storageService.deleteEmptyRegularDirectories(storageDirs); await storageService.deleteEmptyRegularDirectories(storageDirs);
}, },
); );
return true;
} }
Future<void> _move(BuildContext context, {required MoveType moveType}) async { Future<void> _move(BuildContext context, {required MoveType moveType}) async {
final entries = _getTargetItems(context); final entries = _getTargetItems(context);
await doMove(context, moveType: moveType, entries: entries); final completed = await doMove(context, moveType: moveType, entries: entries);
_browse(context); if (completed) {
_browse(context);
}
} }
Future<void> _rename(BuildContext context) async { Future<void> _rename(BuildContext context) async {
@ -381,9 +387,11 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
return MapEntry(entry, '$newName${entry.extension}'); return MapEntry(entry, '$newName${entry.extension}');
}); });
final entriesToNewName = Map.fromEntries(await Future.wait(namingFutures)).whereNotNullValue(); final entriesToNewName = Map.fromEntries(await Future.wait(namingFutures)).whereNotNullValue();
await rename(context, entriesToNewName: entriesToNewName, persist: true); final completed = await rename(context, entriesToNewName: entriesToNewName, persist: true);
_browse(context); if (completed) {
_browse(context);
}
} }
Future<void> _convert(BuildContext context) async { Future<void> _convert(BuildContext context) async {
@ -398,13 +406,14 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
switch (options.action) { switch (options.action) {
case EntryConvertAction.convert: case EntryConvertAction.convert:
await doExport(context, entries, options); final completed = await doExport(context, entries, options);
if (completed) {
_browse(context);
}
case EntryConvertAction.convertMotionPhotoToStillImage: case EntryConvertAction.convertMotionPhotoToStillImage:
final todoItems = entries.where((entry) => entry.isMotionPhoto).toSet(); final todoItems = entries.where((entry) => entry.isMotionPhoto).toSet();
await _edit(context, todoItems, (entry) => entry.removeTrailerVideo()); await _edit(context, todoItems, (entry) => entry.removeTrailerVideo());
} }
_browse(context);
} }
Future<void> _toggleFavourite(BuildContext context) async { Future<void> _toggleFavourite(BuildContext context) async {

View file

@ -37,14 +37,15 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
Future<void> doExport(BuildContext context, Set<AvesEntry> targetEntries, EntryConvertOptions options) async { // returns whether it completed the action (with or without failures)
Future<bool> doExport(BuildContext context, Set<AvesEntry> targetEntries, EntryConvertOptions options) async {
final destinationAlbumFilter = await pickAlbum(context: context, moveType: MoveType.export, storedAlbumsOnly: true); final destinationAlbumFilter = await pickAlbum(context: context, moveType: MoveType.export, storedAlbumsOnly: true);
if (destinationAlbumFilter == null || destinationAlbumFilter is! StoredAlbumFilter) return; if (destinationAlbumFilter == null || destinationAlbumFilter is! StoredAlbumFilter) return false;
final destinationAlbum = destinationAlbumFilter.album; final destinationAlbum = destinationAlbumFilter.album;
if (!await checkStoragePermissionForAlbums(context, {destinationAlbum})) return; if (!await checkStoragePermissionForAlbums(context, {destinationAlbum})) return false;
if (!await checkFreeSpaceForMove(context, targetEntries, destinationAlbum, MoveType.export)) return; if (!await checkFreeSpaceForMove(context, targetEntries, destinationAlbum, MoveType.export)) return false;
final transientMultiPageInfo = <MultiPageInfo>{}; final transientMultiPageInfo = <MultiPageInfo>{};
final selection = <AvesEntry>{}; final selection = <AvesEntry>{};
@ -89,7 +90,7 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
), ),
routeSettings: const RouteSettings(name: AvesSingleSelectionDialog.routeName), routeSettings: const RouteSettings(name: AvesSingleSelectionDialog.routeName),
); );
if (value == null) return; if (value == null) return false;
nameConflictStrategy = value; nameConflictStrategy = value;
} }
@ -157,9 +158,11 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
}, },
); );
transientMultiPageInfo.forEach((v) => v.dispose()); transientMultiPageInfo.forEach((v) => v.dispose());
return true;
} }
Future<void> doQuickMove( // returns whether it completed the action (with or without failures)
Future<bool> doQuickMove(
BuildContext context, { BuildContext context, {
required MoveType moveType, required MoveType moveType,
required Map<String, Set<AvesEntry>> entriesByDestination, required Map<String, Set<AvesEntry>> entriesByDestination,
@ -176,23 +179,23 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
final entries = entriesByDestination.values.expand((v) => v).toSet(); final entries = entriesByDestination.values.expand((v) => v).toSet();
final todoCount = entries.length; final todoCount = entries.length;
if (todoCount == 0) return; if (todoCount == 0) return true;
final toBin = moveType == MoveType.toBin; final toBin = moveType == MoveType.toBin;
final copy = moveType == MoveType.copy; final copy = moveType == MoveType.copy;
// permission for modification at destinations // permission for modification at destinations
final destinationAlbums = entriesByDestination.keys.toSet(); final destinationAlbums = entriesByDestination.keys.toSet();
if (!await checkStoragePermissionForAlbums(context, destinationAlbums)) return; if (!await checkStoragePermissionForAlbums(context, destinationAlbums)) return false;
// permission for modification at origins // permission for modification at origins
final originAlbums = entries.map((e) => e.directory).nonNulls.toSet(); final originAlbums = entries.map((e) => e.directory).nonNulls.toSet();
if ({MoveType.move, MoveType.toBin}.contains(moveType) && !await checkStoragePermissionForAlbums(context, originAlbums, entries: entries)) return; if ({MoveType.move, MoveType.toBin}.contains(moveType) && !await checkStoragePermissionForAlbums(context, originAlbums, entries: entries)) return false;
final hasEnoughSpaceByDestination = await Future.wait(destinationAlbums.map((destinationAlbum) { final hasEnoughSpaceByDestination = await Future.wait(destinationAlbums.map((destinationAlbum) {
return checkFreeSpaceForMove(context, entries, destinationAlbum, moveType); return checkFreeSpaceForMove(context, entries, destinationAlbum, moveType);
})); }));
if (hasEnoughSpaceByDestination.any((v) => !v)) return; if (hasEnoughSpaceByDestination.any((v) => !v)) return false;
final l10n = context.l10n; final l10n = context.l10n;
var nameConflictStrategy = NameConflictStrategy.rename; var nameConflictStrategy = NameConflictStrategy.rename;
@ -217,12 +220,12 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
), ),
routeSettings: const RouteSettings(name: AvesSingleSelectionDialog.routeName), routeSettings: const RouteSettings(name: AvesSingleSelectionDialog.routeName),
); );
if (value == null) return; if (value == null) return false;
nameConflictStrategy = value; nameConflictStrategy = value;
} }
} }
if ({MoveType.move, MoveType.copy}.contains(moveType) && !await _checkUndatedItems(context, entries)) return; if ({MoveType.move, MoveType.copy}.contains(moveType) && !await _checkUndatedItems(context, entries)) return false;
final source = context.read<CollectionSource>(); final source = context.read<CollectionSource>();
source.pauseMonitoring(); source.pauseMonitoring();
@ -321,9 +324,11 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
} }
}, },
); );
return true;
} }
Future<void> doMove( // returns whether it completed the action (with or without failures)
Future<bool> doMove(
BuildContext context, { BuildContext context, {
required MoveType moveType, required MoveType moveType,
required Set<AvesEntry> entries, required Set<AvesEntry> entries,
@ -338,7 +343,7 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
message: l10n.binEntriesConfirmationDialogMessage(entries.length), message: l10n.binEntriesConfirmationDialogMessage(entries.length),
confirmationButtonLabel: l10n.deleteButtonLabel, confirmationButtonLabel: l10n.deleteButtonLabel,
)) { )) {
return; return false;
} }
} }
@ -348,7 +353,7 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
case MoveType.move: case MoveType.move:
case MoveType.export: case MoveType.export:
final destinationAlbumFilter = await pickAlbum(context: context, moveType: moveType, storedAlbumsOnly: true); final destinationAlbumFilter = await pickAlbum(context: context, moveType: moveType, storedAlbumsOnly: true);
if (destinationAlbumFilter == null || destinationAlbumFilter is! StoredAlbumFilter) return; if (destinationAlbumFilter == null || destinationAlbumFilter is! StoredAlbumFilter) return false;
final destinationAlbum = destinationAlbumFilter.album; final destinationAlbum = destinationAlbumFilter.album;
settings.recentDestinationAlbums = settings.recentDestinationAlbums settings.recentDestinationAlbums = settings.recentDestinationAlbums
@ -365,7 +370,7 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
}); });
} }
await doQuickMove( return await doQuickMove(
context, context,
moveType: moveType, moveType: moveType,
entriesByDestination: entriesByDestination, entriesByDestination: entriesByDestination,
@ -373,7 +378,8 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
); );
} }
Future<void> rename( // returns whether it completed the action (with or without failures)
Future<bool> rename(
BuildContext context, { BuildContext context, {
required Map<AvesEntry, String> entriesToNewName, required Map<AvesEntry, String> entriesToNewName,
required bool persist, required bool persist,
@ -383,9 +389,9 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
final todoCount = entries.length; final todoCount = entries.length;
assert(todoCount > 0); assert(todoCount > 0);
if (!await checkStoragePermission(context, entries)) return; if (!await checkStoragePermission(context, entries)) return false;
if (!await _checkUndatedItems(context, entries)) return; if (!await _checkUndatedItems(context, entries)) return false;
final source = context.read<CollectionSource>(); final source = context.read<CollectionSource>();
source.pauseMonitoring(); source.pauseMonitoring();
@ -420,6 +426,7 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
} }
}, },
); );
return true;
} }
Future<bool> _checkUndatedItems(BuildContext context, Set<AvesEntry> entries) async { Future<bool> _checkUndatedItems(BuildContext context, Set<AvesEntry> entries) async {