656 lines
26 KiB
Dart
656 lines
26 KiB
Dart
import 'dart:async';
|
||
import 'dart:io';
|
||
|
||
import 'package:aves/app_mode.dart';
|
||
import 'package:aves/model/entry/entry.dart';
|
||
import 'package:aves/model/entry/extensions/favourites.dart';
|
||
import 'package:aves/model/entry/extensions/keys.dart';
|
||
import 'package:aves/model/entry/extensions/multipage.dart';
|
||
import 'package:aves/model/entry/extensions/props.dart';
|
||
import 'package:aves/model/favourites.dart';
|
||
import 'package:aves/model/filters/covered/stored_album.dart';
|
||
import 'package:aves/model/filters/trash.dart';
|
||
import 'package:aves/model/highlight.dart';
|
||
import 'package:aves/model/metadata/date_modifier.dart';
|
||
import 'package:aves/model/multipage.dart';
|
||
import 'package:aves/model/settings/settings.dart';
|
||
import 'package:aves/model/source/collection_lens.dart';
|
||
import 'package:aves/model/source/collection_source.dart';
|
||
import 'package:aves/ref/mime_types.dart';
|
||
import 'package:aves/services/common/image_op_events.dart';
|
||
import 'package:aves/services/common/services.dart';
|
||
import 'package:aves/services/media/enums.dart';
|
||
import 'package:aves/services/media/media_edit_service.dart';
|
||
import 'package:aves/theme/durations.dart';
|
||
import 'package:aves/theme/themes.dart';
|
||
import 'package:aves/utils/android_file_utils.dart';
|
||
import 'package:aves/widgets/collection/collection_page.dart';
|
||
import 'package:aves/widgets/collection/entry_set_action_delegate.dart';
|
||
import 'package:aves/widgets/common/action_mixins/feedback.dart';
|
||
import 'package:aves/widgets/common/action_mixins/permission_aware.dart';
|
||
import 'package:aves/widgets/common/action_mixins/size_aware.dart';
|
||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||
import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart';
|
||
import 'package:aves/widgets/dialogs/pick_dialogs/pick_album_stub.dart';
|
||
import 'package:aves/widgets/dialogs/selection_dialogs/single_selection.dart';
|
||
import 'package:aves/widgets/filter_grids/common/enums.dart';
|
||
import 'package:aves/widgets/viewer/controls/notifications.dart';
|
||
import 'package:aves_model/aves_model.dart';
|
||
import 'package:collection/collection.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:provider/provider.dart';
|
||
|
||
// ✅ NEW: usa “Crea album” di Aves quando la destinazione è solo remota
|
||
import 'package:aves/widgets/dialogs/filter_editors/create_stored_album_dialog.dart';
|
||
|
||
mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
|
||
// ------------------------------------------------------------
|
||
// Helpers: remote-only destination -> create local album via Aves dialog
|
||
// ------------------------------------------------------------
|
||
|
||
bool _isRemoteOnlyAlbumPath(String path) => path.startsWith('/photos/');
|
||
|
||
Future<String?> _ensureLocalAlbumForRemoteDestination(
|
||
BuildContext context, {
|
||
required String destinationAlbum,
|
||
}) async {
|
||
// già locale
|
||
if (!_isRemoteOnlyAlbumPath(destinationAlbum)) {
|
||
return destinationAlbum;
|
||
}
|
||
|
||
// suggerisci il nome come basename della cartella remota
|
||
final suggestedName = pContext.basename(destinationAlbum);
|
||
|
||
// dialog “Crea album” (Aves), precompilato col nome
|
||
final localAlbum = await showDialog<String>(
|
||
context: context,
|
||
builder: (_) => CreateStoredAlbumDialog(initialName: suggestedName),
|
||
routeSettings: const RouteSettings(name: CreateStoredAlbumDialog.routeName),
|
||
);
|
||
|
||
if (localAlbum == null || localAlbum.trim().isEmpty) return null;
|
||
|
||
final path = localAlbum.trim();
|
||
|
||
// se non esiste ancora, chiedi permesso sul parent (come fa Aves in rename/move),
|
||
// poi prova a creare la directory
|
||
final parent = pContext.dirname(path);
|
||
final exists = await Directory(path).exists();
|
||
if (!exists) {
|
||
if (!await checkStoragePermissionForAlbums(context, {parent})) return null;
|
||
try {
|
||
await Directory(path).create(recursive: true);
|
||
} catch (_) {
|
||
// se fallisce per scoped storage, lasciamo che il native move/export gestisca (se previsto)
|
||
}
|
||
}
|
||
|
||
// registra l’album nella source (per farlo comparire subito)
|
||
context.read<CollectionSource>().createStoredAlbum(path);
|
||
|
||
return path;
|
||
}
|
||
|
||
// ------------------------------------------------------------
|
||
// Export/Convert
|
||
// ------------------------------------------------------------
|
||
|
||
// 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,
|
||
chipTypes: {AlbumChipType.stored},
|
||
initialGroup: null,
|
||
);
|
||
if (destinationAlbumFilter == null || destinationAlbumFilter is! StoredAlbumFilter) return false;
|
||
|
||
// ✅ se la destinazione è un album “solo remoto”, crea prima la cartella locale via dialog Aves
|
||
var destinationAlbum = destinationAlbumFilter.album;
|
||
final ensured = await _ensureLocalAlbumForRemoteDestination(
|
||
context,
|
||
destinationAlbum: destinationAlbum,
|
||
);
|
||
if (ensured == null) return false;
|
||
destinationAlbum = ensured;
|
||
|
||
if (!await checkStoragePermissionForAlbums(context, {destinationAlbum})) return false;
|
||
|
||
if (!await checkFreeSpaceForMove(context, targetEntries, destinationAlbum, MoveType.export)) return false;
|
||
|
||
final transientMultiPageInfo = <MultiPageInfo>{};
|
||
final selection = <AvesEntry>{};
|
||
await Future.forEach(targetEntries, (targetEntry) async {
|
||
if (targetEntry.isMultiPage) {
|
||
final multiPageInfo = await targetEntry.getMultiPageInfo();
|
||
if (multiPageInfo != null) {
|
||
transientMultiPageInfo.add(multiPageInfo);
|
||
if (targetEntry.isMotionPhoto) {
|
||
await multiPageInfo.extractMotionPhotoVideo();
|
||
}
|
||
if (multiPageInfo.pageCount > 1) {
|
||
selection.addAll(multiPageInfo.exportEntries);
|
||
}
|
||
}
|
||
} else {
|
||
selection.add(targetEntry);
|
||
}
|
||
});
|
||
|
||
final l10n = context.l10n;
|
||
|
||
var nameConflictStrategy = NameConflictStrategy.rename;
|
||
final destinationDirectory = Directory(destinationAlbum);
|
||
final destinationExtension = MimeTypes.extensionFor(options.mimeType);
|
||
final names = [
|
||
...selection.map((v) => '${v.filenameWithoutExtension}$destinationExtension'),
|
||
// do not guard up front based on directory existence,
|
||
// as conflicts could be within moved entries scattered across multiple albums
|
||
if (await destinationDirectory.exists()) ...destinationDirectory.listSync().map((v) => pContext.basename(v.path)),
|
||
].map((v) => v.toLowerCase()).toList();
|
||
// case insensitive comparison
|
||
final uniqueNames = names.toSet();
|
||
if (uniqueNames.length < names.length) {
|
||
final value = await showDialog<NameConflictStrategy>(
|
||
context: context,
|
||
builder: (context) => AvesSingleSelectionDialog<NameConflictStrategy>(
|
||
initialValue: nameConflictStrategy,
|
||
options: Map.fromEntries(NameConflictStrategy.values.map((v) => MapEntry(v, v.getName(context)))),
|
||
message: l10n.nameConflictDialogSingleSourceMessage,
|
||
confirmationButtonLabel: l10n.continueButtonLabel,
|
||
),
|
||
routeSettings: const RouteSettings(name: AvesSingleSelectionDialog.routeName),
|
||
);
|
||
if (value == null) return false;
|
||
nameConflictStrategy = value;
|
||
}
|
||
|
||
final selectionCount = selection.length;
|
||
final source = context.read<CollectionSource>();
|
||
source.pauseMonitoring();
|
||
await showOpReport<ExportOpEvent>(
|
||
context: context,
|
||
opStream: mediaEditService.export(
|
||
selection,
|
||
options: options,
|
||
destinationAlbum: destinationAlbum,
|
||
nameConflictStrategy: nameConflictStrategy,
|
||
),
|
||
itemCount: selectionCount,
|
||
onDone: (processed) async {
|
||
final successOps = processed.where((op) => op.success).toSet();
|
||
final exportedOps = successOps.where((op) => !op.skipped && op.newFields[EntryFields.uri] != null).toSet();
|
||
final newUris = exportedOps.map((op) => op.newFields[EntryFields.uri] as String).toSet();
|
||
final isMainMode = context.read<ValueNotifier<AppMode>>().value == AppMode.main;
|
||
|
||
// check source favourite status
|
||
final favouriteSourceUris = selection.where((entry) => entry.isFavourite).map((entry) => entry.uri).toSet();
|
||
final favouriteNewUris = <String>{};
|
||
exportedOps.forEach((op) {
|
||
final sourceUri = op.uri;
|
||
if (favouriteSourceUris.contains(sourceUri)) {
|
||
final newUri = op.newFields[EntryFields.uri] as String;
|
||
favouriteNewUris.add(newUri);
|
||
}
|
||
});
|
||
|
||
source.resumeMonitoring();
|
||
unawaited(
|
||
source.refreshUris(newUris).then((_) {
|
||
// transfer favourite status on exports
|
||
final newFavouriteEntries = source.allEntries.where((entry) => favouriteNewUris.contains(entry.uri)).toSet();
|
||
favourites.add(newFavouriteEntries);
|
||
}),
|
||
);
|
||
|
||
// get navigator beforehand because
|
||
// local context may be deactivated when action is triggered after navigation
|
||
final navigator = Navigator.maybeOf(context);
|
||
final showAction = isMainMode && newUris.isNotEmpty
|
||
? SnackBarAction(
|
||
label: l10n.showButtonLabel,
|
||
onPressed: () {
|
||
if (navigator != null) {
|
||
navigator.pushAndRemoveUntil(
|
||
MaterialPageRoute(
|
||
settings: const RouteSettings(name: CollectionPage.routeName),
|
||
builder: (context) => CollectionPage(
|
||
source: source,
|
||
filters: {StoredAlbumFilter(destinationAlbum, source.getStoredAlbumDisplayName(context, destinationAlbum))},
|
||
highlightTest: (entry) => newUris.contains(entry.uri),
|
||
),
|
||
),
|
||
(route) => false,
|
||
);
|
||
}
|
||
},
|
||
)
|
||
: null;
|
||
final successCount = successOps.length;
|
||
if (successCount < selectionCount) {
|
||
final count = selectionCount - successCount;
|
||
showFeedback(
|
||
context,
|
||
FeedbackType.warn,
|
||
l10n.collectionExportFailureFeedback(count),
|
||
showAction,
|
||
);
|
||
} else {
|
||
showFeedback(
|
||
context,
|
||
FeedbackType.info,
|
||
l10n.genericSuccessFeedback,
|
||
showAction,
|
||
);
|
||
}
|
||
},
|
||
);
|
||
transientMultiPageInfo.forEach((v) => v.dispose());
|
||
return true;
|
||
}
|
||
|
||
// ------------------------------------------------------------
|
||
// Quick move/copy
|
||
// ------------------------------------------------------------
|
||
|
||
// returns whether it completed the action (with or without failures)
|
||
Future<bool> doQuickMove(
|
||
BuildContext context, {
|
||
required MoveType moveType,
|
||
required Map<String, Set<AvesEntry>> entriesByDestination,
|
||
bool hideShowAction = false,
|
||
VoidCallback? onSuccess,
|
||
}) async {
|
||
if (moveType == MoveType.move) {
|
||
// skip moving entries to their directory
|
||
entriesByDestination.forEach((destinationAlbum, entries) {
|
||
entries.removeWhere((entry) => entry.directory == destinationAlbum);
|
||
});
|
||
entriesByDestination.removeWhere((_, entries) => entries.isEmpty);
|
||
}
|
||
|
||
final entries = entriesByDestination.values.expand((v) => v).toSet();
|
||
final todoCount = entries.length;
|
||
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 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 false;
|
||
|
||
final hasEnoughSpaceByDestination = await Future.wait(
|
||
destinationAlbums.map((destinationAlbum) {
|
||
return checkFreeSpaceForMove(context, entries, destinationAlbum, moveType);
|
||
}),
|
||
);
|
||
if (hasEnoughSpaceByDestination.any((v) => !v)) return false;
|
||
|
||
final l10n = context.l10n;
|
||
var nameConflictStrategy = NameConflictStrategy.rename;
|
||
if (!toBin && destinationAlbums.length == 1) {
|
||
final destinationDirectory = Directory(destinationAlbums.single);
|
||
final names = [
|
||
...entries.map((v) => '${v.filenameWithoutExtension}${v.extension}'),
|
||
// do not guard up front based on directory existence,
|
||
// as conflicts could be within moved entries scattered across multiple albums
|
||
if (await destinationDirectory.exists()) ...destinationDirectory.listSync().map((v) => pContext.basename(v.path)),
|
||
].map((v) => v.toLowerCase()).toList();
|
||
// case insensitive comparison
|
||
final uniqueNames = names.toSet();
|
||
if (uniqueNames.length < names.length) {
|
||
final value = await showDialog<NameConflictStrategy>(
|
||
context: context,
|
||
builder: (context) => AvesSingleSelectionDialog<NameConflictStrategy>(
|
||
initialValue: nameConflictStrategy,
|
||
options: Map.fromEntries(NameConflictStrategy.values.map((v) => MapEntry(v, v.getName(context)))),
|
||
message: originAlbums.length == 1 ? l10n.nameConflictDialogSingleSourceMessage : l10n.nameConflictDialogMultipleSourceMessage,
|
||
confirmationButtonLabel: l10n.continueButtonLabel,
|
||
),
|
||
routeSettings: const RouteSettings(name: AvesSingleSelectionDialog.routeName),
|
||
);
|
||
if (value == null) return false;
|
||
nameConflictStrategy = value;
|
||
}
|
||
}
|
||
|
||
if ({MoveType.move, MoveType.copy, MoveType.toBin}.contains(moveType) && !await _checkUndatedItems(context, entries)) return false;
|
||
|
||
final source = context.read<CollectionSource>();
|
||
source.pauseMonitoring();
|
||
final opId = mediaEditService.newOpId;
|
||
await showOpReport<MoveOpEvent>(
|
||
context: context,
|
||
opStream: mediaEditService.move(
|
||
opId: opId,
|
||
entriesByDestination: entriesByDestination,
|
||
copy: copy,
|
||
nameConflictStrategy: nameConflictStrategy,
|
||
),
|
||
itemCount: todoCount,
|
||
onCancel: () => mediaEditService.cancelFileOp(opId),
|
||
onDone: (processed) async {
|
||
final successOps = processed.where((op) => op.success).toSet();
|
||
|
||
// move
|
||
final movedOps = successOps.where((op) => !op.skipped && !op.deleted).toSet();
|
||
final movedEntries = movedOps.map((op) => op.uri).map((uri) => entries.firstWhereOrNull((entry) => entry.uri == uri)).nonNulls.toSet();
|
||
await source.updateAfterMove(
|
||
todoEntries: entries,
|
||
moveType: moveType,
|
||
destinationAlbums: destinationAlbums,
|
||
movedOps: movedOps,
|
||
);
|
||
|
||
// delete (when trying to move to bin obsolete entries)
|
||
final deletedOps = successOps.where((op) => op.deleted).toSet();
|
||
final deletedUris = deletedOps.map((op) => op.uri).toSet();
|
||
await source.removeEntries(deletedUris, includeTrash: true);
|
||
|
||
source.resumeMonitoring();
|
||
|
||
// cleanup
|
||
if ({MoveType.move, MoveType.toBin}.contains(moveType)) {
|
||
await storageService.deleteEmptyRegularDirectories(originAlbums);
|
||
}
|
||
|
||
final successCount = successOps.length;
|
||
if (successCount < todoCount) {
|
||
final count = todoCount - successCount;
|
||
showFeedback(
|
||
context,
|
||
FeedbackType.warn,
|
||
copy ? l10n.collectionCopyFailureFeedback(count) : l10n.collectionMoveFailureFeedback(count),
|
||
);
|
||
} else {
|
||
final count = movedOps.length;
|
||
final appMode = context.read<ValueNotifier<AppMode>?>()?.value;
|
||
|
||
SnackBarAction? action;
|
||
if (count > 0 && appMode == AppMode.main) {
|
||
// get navigator beforehand because
|
||
// local context may be deactivated when action is triggered after navigation
|
||
final navigator = Navigator.maybeOf(context);
|
||
if (toBin) {
|
||
if (movedEntries.isNotEmpty) {
|
||
action = SnackBarAction(
|
||
label: Themes.asButtonLabel(l10n.entryActionRestore),
|
||
onPressed: () {
|
||
if (navigator != null) {
|
||
doMove(
|
||
navigator.context,
|
||
moveType: MoveType.fromBin,
|
||
entries: movedEntries,
|
||
hideShowAction: true,
|
||
);
|
||
}
|
||
},
|
||
);
|
||
}
|
||
} else if (!hideShowAction) {
|
||
action = SnackBarAction(
|
||
label: l10n.showButtonLabel,
|
||
onPressed: () {
|
||
if (navigator != null) {
|
||
_showMovedItems(navigator.context, destinationAlbums, movedOps);
|
||
}
|
||
},
|
||
);
|
||
}
|
||
}
|
||
|
||
if (!toBin || (toBin && settings.confirmAfterMoveToBin)) {
|
||
showFeedback(
|
||
context,
|
||
FeedbackType.info,
|
||
copy ? l10n.collectionCopySuccessFeedback(count) : l10n.collectionMoveSuccessFeedback(count),
|
||
action,
|
||
);
|
||
}
|
||
|
||
EntryMovedNotification(moveType, movedEntries).dispatch(context);
|
||
onSuccess?.call();
|
||
}
|
||
},
|
||
);
|
||
return true;
|
||
}
|
||
|
||
// ------------------------------------------------------------
|
||
// Move/copy entry set
|
||
// ------------------------------------------------------------
|
||
|
||
// returns whether it completed the action (with or without failures)
|
||
Future<bool> doMove(
|
||
BuildContext context, {
|
||
required MoveType moveType,
|
||
required Set<AvesEntry> entries,
|
||
bool hideShowAction = false,
|
||
VoidCallback? onSuccess,
|
||
}) async {
|
||
if (moveType == MoveType.toBin) {
|
||
final l10n = context.l10n;
|
||
if (!await showSkippableConfirmationDialog(
|
||
context: context,
|
||
type: ConfirmationDialog.moveToBin,
|
||
message: l10n.binEntriesConfirmationDialogMessage(entries.length),
|
||
confirmationButtonLabel: l10n.deleteButtonLabel,
|
||
)) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
final entriesByDestination = <String, Set<AvesEntry>>{};
|
||
switch (moveType) {
|
||
case MoveType.copy:
|
||
case MoveType.move:
|
||
case MoveType.export:
|
||
final destinationAlbumFilter = await pickAlbum(
|
||
context: context,
|
||
moveType: moveType,
|
||
chipTypes: {AlbumChipType.stored},
|
||
initialGroup: null,
|
||
);
|
||
if (destinationAlbumFilter == null || destinationAlbumFilter is! StoredAlbumFilter) return false;
|
||
|
||
var destinationAlbum = destinationAlbumFilter.album;
|
||
|
||
// ✅ se era solo remoto, crea prima l’album locale via dialog Aves e usa quello
|
||
final ensured = await _ensureLocalAlbumForRemoteDestination(
|
||
context,
|
||
destinationAlbum: destinationAlbum,
|
||
);
|
||
if (ensured == null) return false;
|
||
destinationAlbum = ensured;
|
||
|
||
settings.recentDestinationAlbums = settings.recentDestinationAlbums
|
||
..remove(destinationAlbum)
|
||
..insert(0, destinationAlbum);
|
||
|
||
entriesByDestination[destinationAlbum] = entries;
|
||
|
||
case MoveType.toBin:
|
||
entriesByDestination[AndroidFileUtils.trashDirPath] = entries;
|
||
|
||
case MoveType.fromBin:
|
||
groupBy<AvesEntry, String?>(entries, (e) => e.directory).forEach((originAlbum, dirEntries) {
|
||
if (originAlbum != null) {
|
||
entriesByDestination[originAlbum] = dirEntries.toSet();
|
||
}
|
||
});
|
||
}
|
||
|
||
return await doQuickMove(
|
||
context,
|
||
moveType: moveType,
|
||
entriesByDestination: entriesByDestination,
|
||
onSuccess: onSuccess,
|
||
);
|
||
}
|
||
|
||
// ------------------------------------------------------------
|
||
// Rename
|
||
// ------------------------------------------------------------
|
||
|
||
// returns whether it completed the action (with or without failures)
|
||
Future<bool> rename(
|
||
BuildContext context, {
|
||
required Map<AvesEntry, String> entriesToNewName,
|
||
required bool persist,
|
||
VoidCallback? onSuccess,
|
||
}) async {
|
||
final entries = entriesToNewName.keys.toSet();
|
||
final todoCount = entries.length;
|
||
assert(todoCount > 0);
|
||
|
||
if (!await checkStoragePermission(context, entries)) return false;
|
||
|
||
if (!await _checkUndatedItems(context, entries)) return false;
|
||
|
||
final source = context.read<CollectionSource>();
|
||
source.pauseMonitoring();
|
||
final opId = mediaEditService.newOpId;
|
||
await showOpReport<MoveOpEvent>(
|
||
context: context,
|
||
opStream: mediaEditService.rename(
|
||
opId: opId,
|
||
entriesToNewName: entriesToNewName,
|
||
),
|
||
itemCount: todoCount,
|
||
onCancel: () => mediaEditService.cancelFileOp(opId),
|
||
onDone: (processed) async {
|
||
final successOps = processed.where((op) => op.success).toSet();
|
||
final movedOps = successOps.where((op) => !op.skipped).toSet();
|
||
await source.updateAfterRename(
|
||
todoEntries: entries,
|
||
movedOps: movedOps,
|
||
persist: persist,
|
||
);
|
||
source.resumeMonitoring();
|
||
|
||
final l10n = context.l10n;
|
||
final successCount = successOps.length;
|
||
if (successCount < todoCount) {
|
||
final count = todoCount - successCount;
|
||
showFeedback(context, FeedbackType.warn, l10n.collectionRenameFailureFeedback(count));
|
||
} else {
|
||
final count = movedOps.length;
|
||
showFeedback(context, FeedbackType.info, l10n.collectionRenameSuccessFeedback(count));
|
||
onSuccess?.call();
|
||
}
|
||
},
|
||
);
|
||
return true;
|
||
}
|
||
|
||
Future<bool> _checkUndatedItems(BuildContext context, Set<AvesEntry> entries) async {
|
||
final undatedItems = entries.where((entry) {
|
||
if (!entry.isCatalogued) return false;
|
||
final dateMillis = entry.catalogMetadata?.dateMillis;
|
||
return dateMillis == null || dateMillis == 0;
|
||
}).toSet();
|
||
if (undatedItems.isNotEmpty) {
|
||
final confirmationDialogDelegate = MoveUndatedConfirmationDialogDelegate();
|
||
final confirmed = await showSkippableConfirmationDialog(
|
||
context: context,
|
||
type: ConfirmationDialog.moveUndatedItems,
|
||
delegate: confirmationDialogDelegate,
|
||
confirmationButtonLabel: context.l10n.continueButtonLabel,
|
||
);
|
||
confirmationDialogDelegate.dispose();
|
||
if (!confirmed) return false;
|
||
|
||
if (settings.setMetadataDateBeforeFileOp) {
|
||
final entriesToDate = undatedItems.where((entry) => entry.canEditDate).toSet();
|
||
if (entriesToDate.isNotEmpty) {
|
||
await EntrySetActionDelegate().editDate(
|
||
context,
|
||
entries: entriesToDate,
|
||
modifier: DateModifier.copyField(DateFieldSource.fileModifiedDate),
|
||
showResult: false,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
Future<void> _showMovedItems(
|
||
BuildContext context,
|
||
Set<String> destinationAlbums,
|
||
Set<MoveOpEvent> movedOps,
|
||
) async {
|
||
final newUris = movedOps.map((op) => op.newFields[EntryFields.uri] as String?).toSet();
|
||
bool highlightTest(AvesEntry entry) => newUris.contains(entry.uri);
|
||
|
||
final collection = context.read<CollectionLens?>();
|
||
if (collection == null || collection.filters.any((f) => f is StoredAlbumFilter || f is TrashFilter)) {
|
||
final source = context.read<CollectionSource>();
|
||
final targetFilters = collection?.filters.where((f) => f != TrashFilter.instance).toSet() ?? {};
|
||
// we could simply add the filter to the current collection
|
||
// but navigating makes the change less jarring
|
||
if (destinationAlbums.length == 1) {
|
||
final destinationAlbum = destinationAlbums.single;
|
||
targetFilters.removeWhere((f) => f is StoredAlbumFilter);
|
||
targetFilters.add(StoredAlbumFilter(destinationAlbum, source.getStoredAlbumDisplayName(context, destinationAlbum)));
|
||
}
|
||
unawaited(
|
||
Navigator.maybeOf(context)?.pushAndRemoveUntil(
|
||
MaterialPageRoute(
|
||
settings: const RouteSettings(name: CollectionPage.routeName),
|
||
builder: (context) => CollectionPage(
|
||
source: source,
|
||
filters: targetFilters,
|
||
highlightTest: highlightTest,
|
||
),
|
||
),
|
||
(route) => false,
|
||
),
|
||
);
|
||
} else {
|
||
// track in current page, without navigation
|
||
await Future.delayed(ADurations.highlightScrollInitDelay);
|
||
final targetEntry = collection.sortedEntries.firstWhereOrNull(highlightTest);
|
||
if (targetEntry != null) {
|
||
context.read<HighlightInfo>().trackItem(targetEntry, highlightItem: targetEntry);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
class MoveUndatedConfirmationDialogDelegate extends ConfirmationDialogDelegate {
|
||
final ValueNotifier<bool> _setMetadataDate = ValueNotifier(false);
|
||
|
||
MoveUndatedConfirmationDialogDelegate() {
|
||
_setMetadataDate.value = settings.setMetadataDateBeforeFileOp;
|
||
}
|
||
|
||
void dispose() {
|
||
_setMetadataDate.dispose();
|
||
}
|
||
|
||
@override
|
||
List<Widget> build(BuildContext context) => [
|
||
Padding(
|
||
padding: const EdgeInsets.all(16) + const EdgeInsets.only(top: 8),
|
||
child: Text(context.l10n.moveUndatedConfirmationDialogMessage),
|
||
),
|
||
ValueListenableBuilder<bool>(
|
||
valueListenable: _setMetadataDate,
|
||
builder: (context, flag, child) => SwitchListTile(
|
||
value: flag,
|
||
onChanged: (v) => _setMetadataDate.value = v,
|
||
title: Text(context.l10n.moveUndatedConfirmationDialogSetDate),
|
||
),
|
||
),
|
||
];
|
||
|
||
@override
|
||
void apply() => settings.setMetadataDateBeforeFileOp = _setMetadataDate.value;
|
||
}
|