#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
- 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
### Fixed

View file

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

View file

@ -37,14 +37,15 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
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);
if (destinationAlbumFilter == null || destinationAlbumFilter is! StoredAlbumFilter) return;
if (destinationAlbumFilter == null || destinationAlbumFilter is! StoredAlbumFilter) return false;
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 selection = <AvesEntry>{};
@ -89,7 +90,7 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
),
routeSettings: const RouteSettings(name: AvesSingleSelectionDialog.routeName),
);
if (value == null) return;
if (value == null) return false;
nameConflictStrategy = value;
}
@ -157,9 +158,11 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
},
);
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, {
required MoveType moveType,
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 todoCount = entries.length;
if (todoCount == 0) return;
if (todoCount == 0) return true;
final toBin = moveType == MoveType.toBin;
final copy = moveType == MoveType.copy;
// permission for modification at destinations
final destinationAlbums = entriesByDestination.keys.toSet();
if (!await checkStoragePermissionForAlbums(context, destinationAlbums)) return;
if (!await checkStoragePermissionForAlbums(context, destinationAlbums)) return false;
// permission for modification at origins
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) {
return checkFreeSpaceForMove(context, entries, destinationAlbum, moveType);
}));
if (hasEnoughSpaceByDestination.any((v) => !v)) return;
if (hasEnoughSpaceByDestination.any((v) => !v)) return false;
final l10n = context.l10n;
var nameConflictStrategy = NameConflictStrategy.rename;
@ -217,12 +220,12 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
),
routeSettings: const RouteSettings(name: AvesSingleSelectionDialog.routeName),
);
if (value == null) return;
if (value == null) return false;
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>();
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, {
required MoveType moveType,
required Set<AvesEntry> entries,
@ -338,7 +343,7 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
message: l10n.binEntriesConfirmationDialogMessage(entries.length),
confirmationButtonLabel: l10n.deleteButtonLabel,
)) {
return;
return false;
}
}
@ -348,7 +353,7 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
case MoveType.move:
case MoveType.export:
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;
settings.recentDestinationAlbums = settings.recentDestinationAlbums
@ -365,7 +370,7 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
});
}
await doQuickMove(
return await doQuickMove(
context,
moveType: moveType,
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, {
required Map<AvesEntry, String> entriesToNewName,
required bool persist,
@ -383,9 +389,9 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
final todoCount = entries.length;
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>();
source.pauseMonitoring();
@ -420,6 +426,7 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
}
},
);
return true;
}
Future<bool> _checkUndatedItems(BuildContext context, Set<AvesEntry> entries) async {