ask to rename/replace/skip when converting items with name conflict
This commit is contained in:
parent
d890d9d9ae
commit
87cfae1e9a
6 changed files with 81 additions and 19 deletions
|
@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file.
|
|||
### Added
|
||||
|
||||
- Collection: stack RAW and JPEG with same file names
|
||||
- Collection: ask to rename/replace/skip when converting items with name conflict
|
||||
|
||||
## <a id="v1.11.3"></a>[v1.11.3] - 2024-06-17
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package deckers.thibault.aves.model
|
||||
|
||||
import java.io.File
|
||||
|
||||
enum class NameConflictStrategy {
|
||||
RENAME, REPLACE, SKIP;
|
||||
|
||||
|
@ -9,4 +11,6 @@ enum class NameConflictStrategy {
|
|||
return valueOf(name.uppercase())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NameConflictResolution(var nameWithoutExtension: String?, var replacementFile: File?)
|
|
@ -41,6 +41,7 @@ import deckers.thibault.aves.metadata.xmp.GoogleXMP
|
|||
import deckers.thibault.aves.model.AvesEntry
|
||||
import deckers.thibault.aves.model.ExifOrientationOp
|
||||
import deckers.thibault.aves.model.FieldMap
|
||||
import deckers.thibault.aves.model.NameConflictResolution
|
||||
import deckers.thibault.aves.model.NameConflictStrategy
|
||||
import deckers.thibault.aves.model.SourceEntry
|
||||
import deckers.thibault.aves.utils.BitmapUtils
|
||||
|
@ -147,13 +148,14 @@ abstract class ImageProvider {
|
|||
val oldFile = File(sourcePath)
|
||||
if (oldFile.nameWithoutExtension != desiredNameWithoutExtension) {
|
||||
oldFile.parent?.let { dir ->
|
||||
resolveTargetFileNameWithoutExtension(
|
||||
val resolution = resolveTargetFileNameWithoutExtension(
|
||||
contextWrapper = activity,
|
||||
dir = dir,
|
||||
desiredNameWithoutExtension = desiredNameWithoutExtension,
|
||||
mimeType = mimeType,
|
||||
conflictStrategy = NameConflictStrategy.RENAME,
|
||||
)?.let { targetNameWithoutExtension ->
|
||||
)
|
||||
resolution.nameWithoutExtension?.let { targetNameWithoutExtension ->
|
||||
val targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}"
|
||||
val newFile = File(dir, targetFileName)
|
||||
if (oldFile != newFile) {
|
||||
|
@ -266,7 +268,7 @@ abstract class ImageProvider {
|
|||
exportMimeType: String,
|
||||
): FieldMap {
|
||||
val sourceMimeType = sourceEntry.mimeType
|
||||
val sourceUri = sourceEntry.uri
|
||||
var sourceUri = sourceEntry.uri
|
||||
val pageId = sourceEntry.pageId
|
||||
|
||||
var desiredNameWithoutExtension = if (sourceEntry.path != null) {
|
||||
|
@ -279,13 +281,17 @@ abstract class ImageProvider {
|
|||
val page = if (sourceMimeType == MimeTypes.TIFF) pageId + 1 else pageId
|
||||
desiredNameWithoutExtension += "_${page.toString().padStart(3, '0')}"
|
||||
}
|
||||
val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension(
|
||||
val resolution = resolveTargetFileNameWithoutExtension(
|
||||
contextWrapper = activity,
|
||||
dir = targetDir,
|
||||
desiredNameWithoutExtension = desiredNameWithoutExtension,
|
||||
mimeType = exportMimeType,
|
||||
conflictStrategy = nameConflictStrategy,
|
||||
) ?: return skippedFieldMap
|
||||
)
|
||||
val targetNameWithoutExtension = resolution.nameWithoutExtension ?: return skippedFieldMap
|
||||
resolution.replacementFile?.let { file ->
|
||||
sourceUri = Uri.fromFile(file)
|
||||
}
|
||||
|
||||
val targetMimeType: String
|
||||
val write: (OutputStream) -> Unit
|
||||
|
@ -391,6 +397,8 @@ abstract class ImageProvider {
|
|||
} finally {
|
||||
// clearing Glide target should happen after effectively writing the bitmap
|
||||
Glide.with(activity).clear(target)
|
||||
|
||||
resolution.replacementFile?.delete()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -470,7 +478,7 @@ abstract class ImageProvider {
|
|||
}
|
||||
|
||||
val captureMimeType = MimeTypes.JPEG
|
||||
val targetNameWithoutExtension = try {
|
||||
val resolution = try {
|
||||
resolveTargetFileNameWithoutExtension(
|
||||
contextWrapper = contextWrapper,
|
||||
dir = targetDir,
|
||||
|
@ -483,6 +491,7 @@ abstract class ImageProvider {
|
|||
return
|
||||
}
|
||||
|
||||
val targetNameWithoutExtension = resolution.nameWithoutExtension
|
||||
if (targetNameWithoutExtension == null) {
|
||||
// skip it
|
||||
callback.onSuccess(skippedFieldMap)
|
||||
|
@ -568,10 +577,13 @@ abstract class ImageProvider {
|
|||
desiredNameWithoutExtension: String,
|
||||
mimeType: String,
|
||||
conflictStrategy: NameConflictStrategy,
|
||||
): String? {
|
||||
): NameConflictResolution {
|
||||
var resolvedName: String? = desiredNameWithoutExtension
|
||||
var replacementFile: File? = null
|
||||
|
||||
val extension = extensionFor(mimeType)
|
||||
val targetFile = File(dir, "$desiredNameWithoutExtension$extension")
|
||||
return when (conflictStrategy) {
|
||||
when (conflictStrategy) {
|
||||
NameConflictStrategy.RENAME -> {
|
||||
var nameWithoutExtension = desiredNameWithoutExtension
|
||||
var i = 0
|
||||
|
@ -579,24 +591,28 @@ abstract class ImageProvider {
|
|||
i++
|
||||
nameWithoutExtension = "$desiredNameWithoutExtension ($i)"
|
||||
}
|
||||
nameWithoutExtension
|
||||
resolvedName = nameWithoutExtension
|
||||
}
|
||||
|
||||
NameConflictStrategy.REPLACE -> {
|
||||
if (targetFile.exists()) {
|
||||
// move replaced file to temp storage
|
||||
// so that it can be used as a source for conversion or metadata copy
|
||||
replacementFile = StorageUtils.createTempFile(contextWrapper).apply {
|
||||
targetFile.transferTo(outputStream())
|
||||
}
|
||||
deletePath(contextWrapper, targetFile.path, mimeType)
|
||||
}
|
||||
desiredNameWithoutExtension
|
||||
}
|
||||
|
||||
NameConflictStrategy.SKIP -> {
|
||||
if (targetFile.exists()) {
|
||||
null
|
||||
} else {
|
||||
desiredNameWithoutExtension
|
||||
resolvedName = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NameConflictResolution(resolvedName, replacementFile)
|
||||
}
|
||||
|
||||
// cf `MetadataFetchHandler.getCatalogMetadataByMetadataExtractor()` for a more thorough check
|
||||
|
|
|
@ -562,13 +562,14 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
}
|
||||
|
||||
val desiredNameWithoutExtension = desiredName.substringBeforeLast(".")
|
||||
val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension(
|
||||
val resolution = resolveTargetFileNameWithoutExtension(
|
||||
contextWrapper = activity,
|
||||
dir = targetDir,
|
||||
desiredNameWithoutExtension = desiredNameWithoutExtension,
|
||||
mimeType = mimeType,
|
||||
conflictStrategy = nameConflictStrategy,
|
||||
) ?: return skippedFieldMap
|
||||
)
|
||||
val targetNameWithoutExtension = resolution.nameWithoutExtension ?: return skippedFieldMap
|
||||
|
||||
val sourceDocFile = DocumentFileCompat.fromSingleUri(activity, sourceUri)
|
||||
val targetPath = createSingle(
|
||||
|
|
|
@ -134,4 +134,15 @@ class MimeTypes {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static const Map<String, String> _defaultExtensions = {
|
||||
bmp: '.bmp',
|
||||
gif: '.gif',
|
||||
jpeg: '.jpg',
|
||||
png: '.png',
|
||||
svg: '.svg',
|
||||
webp: '.webp',
|
||||
};
|
||||
|
||||
static String? extensionFor(String mimeType) => _defaultExtensions[mimeType];
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ 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';
|
||||
|
@ -70,6 +71,34 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
|
|||
}
|
||||
});
|
||||
|
||||
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;
|
||||
nameConflictStrategy = value;
|
||||
}
|
||||
|
||||
final selectionCount = selection.length;
|
||||
final source = context.read<CollectionSource>();
|
||||
source.pauseMonitoring();
|
||||
|
@ -79,7 +108,7 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
|
|||
selection,
|
||||
options: options,
|
||||
destinationAlbum: destinationAlbum,
|
||||
nameConflictStrategy: NameConflictStrategy.rename,
|
||||
nameConflictStrategy: nameConflictStrategy,
|
||||
),
|
||||
itemCount: selectionCount,
|
||||
onDone: (processed) async {
|
||||
|
@ -91,7 +120,6 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
|
|||
source.resumeMonitoring();
|
||||
unawaited(source.refreshUris(newUris));
|
||||
|
||||
final l10n = context.l10n;
|
||||
// get navigator beforehand because
|
||||
// local context may be deactivated when action is triggered after navigation
|
||||
final navigator = Navigator.maybeOf(context);
|
||||
|
@ -173,7 +201,8 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
|
|||
// 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>(
|
||||
|
|
Loading…
Reference in a new issue