album: rename by moving entries
This commit is contained in:
parent
f32c3f1154
commit
44fe56efdb
6 changed files with 74 additions and 242 deletions
|
@ -9,7 +9,6 @@ import androidx.annotation.NonNull;
|
|||
|
||||
import com.bumptech.glide.Glide;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
@ -59,9 +58,6 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler {
|
|||
case "rotate":
|
||||
new Thread(() -> rotate(call, new MethodResultWrapper(result))).start();
|
||||
break;
|
||||
case "renameDirectory":
|
||||
new Thread(() -> renameDirectory(call, new MethodResultWrapper(result))).start();
|
||||
break;
|
||||
default:
|
||||
result.notImplemented();
|
||||
break;
|
||||
|
@ -183,29 +179,4 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void renameDirectory(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
||||
String dirPath = call.argument("path");
|
||||
String newName = call.argument("newName");
|
||||
if (dirPath == null || newName == null) {
|
||||
result.error("renameDirectory-args", "failed because of missing arguments", null);
|
||||
return;
|
||||
}
|
||||
if (!dirPath.endsWith(File.separator)) {
|
||||
dirPath += File.separator;
|
||||
}
|
||||
|
||||
ImageProvider provider = new MediaStoreImageProvider();
|
||||
provider.renameDirectory(activity, dirPath, newName, new ImageProvider.AlbumRenameOpCallback() {
|
||||
@Override
|
||||
public void onSuccess(List<Map<String, Object>> fieldsByEntry) {
|
||||
result.success(fieldsByEntry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable throwable) {
|
||||
result.error("renameDirectory-failure", "failed to rename directory", throwable.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -17,20 +17,14 @@ import com.bumptech.glide.load.resource.bitmap.TransformationUtils;
|
|||
import com.commonsware.cwac.document.DocumentFileCompat;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.SettableFuture;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import deckers.thibault.aves.model.AvesImageEntry;
|
||||
import deckers.thibault.aves.utils.MetadataHelper;
|
||||
|
@ -92,122 +86,6 @@ public abstract class ImageProvider {
|
|||
scanNewPath(context, newFile.getPath(), mimeType, callback);
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
public void renameDirectory(Context context, final String oldDirPath, String newDirName, final AlbumRenameOpCallback callback) {
|
||||
DocumentFileCompat destinationDirDocFile = StorageUtils.createDirectoryIfAbsent(context, oldDirPath);
|
||||
if (destinationDirDocFile == null) {
|
||||
callback.onFailure(new Exception("failed to find directory at path=" + oldDirPath));
|
||||
return;
|
||||
}
|
||||
|
||||
// list entries with their content IDs before renaming
|
||||
Uri[] baseContentUris = new Uri[]{MediaStore.Images.Media.EXTERNAL_CONTENT_URI, MediaStore.Video.Media.EXTERNAL_CONTENT_URI};
|
||||
Map<Uri, List<Map<String, Object>>> entriesByBaseContentUri = Arrays.stream(baseContentUris).collect(Collectors.toMap(
|
||||
uri -> uri,
|
||||
uri -> listContentEntries(context, uri, oldDirPath)
|
||||
));
|
||||
|
||||
// rename directory
|
||||
boolean renamed;
|
||||
try {
|
||||
renamed = destinationDirDocFile.renameTo(newDirName);
|
||||
} catch (FileNotFoundException e) {
|
||||
callback.onFailure(new Exception("failed to rename to name=" + newDirName + " directory at path=" + oldDirPath, e));
|
||||
return;
|
||||
}
|
||||
if (!renamed) {
|
||||
callback.onFailure(new Exception("failed to rename to name=" + newDirName + " directory at path=" + oldDirPath));
|
||||
return;
|
||||
}
|
||||
String newDirPath = new File(oldDirPath).getParent() + File.separator + newDirName + File.separator;
|
||||
|
||||
// scan old paths for cleanup, and new paths to fetch content IDs
|
||||
Collection<SettableFuture<Map<String, Object>>> scanFutures = new ArrayList<>();
|
||||
entriesByBaseContentUri.forEach((baseContentUri, entries) -> {
|
||||
int count = entries.size();
|
||||
if (count > 0) {
|
||||
String[] mimeTypes = new String[count];
|
||||
String[] oldPaths = new String[count];
|
||||
String[] newPaths = new String[count];
|
||||
Map<String, Integer> oldContentIdByPath = new HashMap<>();
|
||||
Map<String, SettableFuture<Map<String, Object>>> scanFutureByPath = new HashMap<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
Map<String, Object> entry = entries.get(i);
|
||||
String displayName = (String) entry.get("displayName");
|
||||
String newPath = newDirPath + displayName;
|
||||
mimeTypes[i] = (String) entry.get("mimeType");
|
||||
oldPaths[i] = oldDirPath + displayName;
|
||||
newPaths[i] = newPath;
|
||||
oldContentIdByPath.put(newPath, (Integer) entry.get("oldContentId"));
|
||||
scanFutureByPath.put(newPath, SettableFuture.create());
|
||||
}
|
||||
MediaScannerConnection.scanFile(context, oldPaths, mimeTypes, null);
|
||||
MediaScannerConnection.scanFile(context, newPaths, mimeTypes, (path, rawUri) -> {
|
||||
SettableFuture<Map<String, Object>> future = scanFutureByPath.get(path);
|
||||
if (future == null) {
|
||||
Log.e(LOG_TAG, "no future for path=" + path);
|
||||
return;
|
||||
}
|
||||
|
||||
if (rawUri == null) {
|
||||
future.setException(new Exception("failed to get URI of item at path=" + path));
|
||||
return;
|
||||
}
|
||||
|
||||
// newURI is possibly a file media URI (e.g. "content://media/12a9-8b42/file/62872")
|
||||
// but we need an image/video media URI (e.g. "content://media/external/images/media/62872")
|
||||
long contentId = ContentUris.parseId(rawUri);
|
||||
Uri contentUri = ContentUris.withAppendedId(baseContentUri, contentId);
|
||||
Map<String, Object> newFields = new HashMap<String, Object>() {{
|
||||
put("path", path);
|
||||
put("uri", contentUri.toString());
|
||||
put("contentId", contentId);
|
||||
put("oldContentId", oldContentIdByPath.get(path));
|
||||
}};
|
||||
future.set(newFields);
|
||||
});
|
||||
|
||||
scanFutures.addAll(scanFutureByPath.values());
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
callback.onSuccess(Futures.allAsList(scanFutures).get());
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
callback.onFailure(e);
|
||||
}
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> listContentEntries(Context context, Uri contentUri, String dirPath) {
|
||||
List<Map<String, Object>> entries = new ArrayList<>();
|
||||
String[] projection = {
|
||||
MediaStore.MediaColumns._ID,
|
||||
MediaStore.MediaColumns.DISPLAY_NAME,
|
||||
MediaStore.MediaColumns.MIME_TYPE,
|
||||
};
|
||||
String selection = MediaStore.MediaColumns.DATA + " like ?";
|
||||
|
||||
try {
|
||||
Cursor cursor = context.getContentResolver().query(contentUri, projection, selection, new String[]{dirPath + "%"}, null);
|
||||
if (cursor != null) {
|
||||
int idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID);
|
||||
int displayNameColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME);
|
||||
int mimeTypeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.MIME_TYPE);
|
||||
while (cursor.moveToNext()) {
|
||||
entries.add(new HashMap<String, Object>() {{
|
||||
put("oldContentId", cursor.getInt(idColumn));
|
||||
put("displayName", cursor.getString(displayNameColumn));
|
||||
put("mimeType", cursor.getString(mimeTypeColumn));
|
||||
}});
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "failed to list entries in contentUri=" + contentUri, e);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
public void rotate(final Context context, final String path, final Uri uri, final String mimeType, final boolean clockwise, final ImageOpCallback callback) {
|
||||
switch (mimeType) {
|
||||
case MimeTypes.JPEG:
|
||||
|
@ -416,10 +294,4 @@ public abstract class ImageProvider {
|
|||
|
||||
void onFailure(Throwable throwable);
|
||||
}
|
||||
|
||||
public interface AlbumRenameOpCallback {
|
||||
void onSuccess(List<Map<String, Object>> fieldsByEntry);
|
||||
|
||||
void onFailure(Throwable throwable);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import 'package:aves/model/source/album.dart';
|
|||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/location.dart';
|
||||
import 'package:aves/model/source/tag.dart';
|
||||
import 'package:aves/services/image_file_service.dart';
|
||||
import 'package:event_bus/event_bus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
|
@ -108,20 +109,53 @@ class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin {
|
|||
}
|
||||
|
||||
void updateAfterMove({
|
||||
@required Iterable<ImageEntry> entries,
|
||||
@required Set<String> fromAlbums,
|
||||
@required String toAlbum,
|
||||
@required List<ImageEntry> selection,
|
||||
@required bool copy,
|
||||
}) {
|
||||
@required String destinationAlbum,
|
||||
@required Iterable<MoveOpEvent> movedOps,
|
||||
}) async {
|
||||
if (movedOps.isEmpty) return;
|
||||
|
||||
final fromAlbums = <String>{};
|
||||
final movedEntries = <ImageEntry>[];
|
||||
if (copy) {
|
||||
addAll(entries);
|
||||
movedOps.forEach((movedOp) {
|
||||
final sourceUri = movedOp.uri;
|
||||
final newFields = movedOp.newFields;
|
||||
final sourceEntry = selection.firstWhere((entry) => entry.uri == sourceUri, orElse: () => null);
|
||||
fromAlbums.add(sourceEntry.directory);
|
||||
movedEntries.add(sourceEntry?.copyWith(
|
||||
uri: newFields['uri'] as String,
|
||||
path: newFields['path'] as String,
|
||||
contentId: newFields['contentId'] as int,
|
||||
dateModifiedSecs: newFields['dateModifiedSecs'] as int,
|
||||
));
|
||||
});
|
||||
await metadataDb.saveEntries(movedEntries);
|
||||
await metadataDb.saveMetadata(movedEntries.map((entry) => entry.catalogMetadata));
|
||||
await metadataDb.saveAddresses(movedEntries.map((entry) => entry.addressDetails));
|
||||
} else {
|
||||
await Future.forEach<MoveOpEvent>(movedOps, (movedOp) async {
|
||||
final sourceUri = movedOp.uri;
|
||||
final newFields = movedOp.newFields;
|
||||
final entry = selection.firstWhere((entry) => entry.uri == sourceUri, orElse: () => null);
|
||||
if (entry != null) {
|
||||
fromAlbums.add(entry.directory);
|
||||
movedEntries.add(entry);
|
||||
await moveEntry(entry, newFields);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (copy) {
|
||||
addAll(movedEntries);
|
||||
} else {
|
||||
cleanEmptyAlbums(fromAlbums);
|
||||
addFolderPath({toAlbum});
|
||||
addFolderPath({destinationAlbum});
|
||||
}
|
||||
updateAlbums();
|
||||
invalidateFilterEntryCounts();
|
||||
eventBus.fire(EntryMovedEvent(entries));
|
||||
eventBus.fire(EntryMovedEvent(movedEntries));
|
||||
}
|
||||
|
||||
int count(CollectionFilter filter) {
|
||||
|
|
|
@ -194,19 +194,6 @@ class ImageFileService {
|
|||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
static Future<List<Map>> renameDirectory(String path, String newName) async {
|
||||
try {
|
||||
final result = await platform.invokeMethod('renameDirectory', <String, dynamic>{
|
||||
'path': path,
|
||||
'newName': newName,
|
||||
});
|
||||
return (result as List).cast<Map>();
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('renameDirectory failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/model/metadata_db.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/services/android_app_service.dart';
|
||||
|
@ -108,7 +106,6 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
|||
selection: selection,
|
||||
opStream: ImageFileService.move(selection, copy: copy, destinationAlbum: destinationAlbum),
|
||||
onDone: (processed) async {
|
||||
debugPrint('$runtimeType _moveSelection onDone');
|
||||
final movedOps = processed.where((e) => e.success);
|
||||
final movedCount = movedOps.length;
|
||||
final selectionCount = selection.length;
|
||||
|
@ -119,44 +116,12 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
|||
final count = movedCount;
|
||||
showFeedback(context, '${copy ? 'Copied' : 'Moved'} ${Intl.plural(count, one: '$count item', other: '$count items')}');
|
||||
}
|
||||
if (movedCount > 0) {
|
||||
final fromAlbums = <String>{};
|
||||
final movedEntries = <ImageEntry>[];
|
||||
if (copy) {
|
||||
movedOps.forEach((movedOp) {
|
||||
final sourceUri = movedOp.uri;
|
||||
final newFields = movedOp.newFields;
|
||||
final sourceEntry = selection.firstWhere((entry) => entry.uri == sourceUri, orElse: () => null);
|
||||
fromAlbums.add(sourceEntry.directory);
|
||||
movedEntries.add(sourceEntry?.copyWith(
|
||||
uri: newFields['uri'] as String,
|
||||
path: newFields['path'] as String,
|
||||
contentId: newFields['contentId'] as int,
|
||||
dateModifiedSecs: newFields['dateModifiedSecs'] as int,
|
||||
));
|
||||
});
|
||||
await metadataDb.saveEntries(movedEntries);
|
||||
await metadataDb.saveMetadata(movedEntries.map((entry) => entry.catalogMetadata));
|
||||
await metadataDb.saveAddresses(movedEntries.map((entry) => entry.addressDetails));
|
||||
} else {
|
||||
await Future.forEach<MoveOpEvent>(movedOps, (movedOp) async {
|
||||
final sourceUri = movedOp.uri;
|
||||
final newFields = movedOp.newFields;
|
||||
final entry = selection.firstWhere((entry) => entry.uri == sourceUri, orElse: () => null);
|
||||
if (entry != null) {
|
||||
fromAlbums.add(entry.directory);
|
||||
movedEntries.add(entry);
|
||||
await source.moveEntry(entry, newFields);
|
||||
}
|
||||
});
|
||||
}
|
||||
source.updateAfterMove(
|
||||
entries: movedEntries,
|
||||
fromAlbums: fromAlbums,
|
||||
toAlbum: destinationAlbum,
|
||||
copy: copy,
|
||||
);
|
||||
}
|
||||
await source.updateAfterMove(
|
||||
selection: selection,
|
||||
copy: copy,
|
||||
destinationAlbum: destinationAlbum,
|
||||
movedOps: movedOps,
|
||||
);
|
||||
collection.clearSelection();
|
||||
collection.browse();
|
||||
},
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/services/image_file_service.dart';
|
||||
|
@ -113,30 +112,34 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per
|
|||
|
||||
if (!await checkStoragePermissionForAlbums(context, {album})) return;
|
||||
|
||||
final result = await ImageFileService.renameDirectory(album, newName);
|
||||
final selection = source.rawEntries.where(filter.filter).toList();
|
||||
final destinationAlbum = path.join(path.dirname(album), newName);
|
||||
|
||||
final albumEntries = source.rawEntries.where(filter.filter);
|
||||
final movedEntries = <ImageEntry>[];
|
||||
await Future.forEach<Map>(result, (newFields) async {
|
||||
final oldContentId = newFields['oldContentId'];
|
||||
final entry = albumEntries.firstWhere((entry) => entry.contentId == oldContentId, orElse: () => null);
|
||||
if (entry != null) {
|
||||
movedEntries.add(entry);
|
||||
await source.moveEntry(entry, newFields);
|
||||
}
|
||||
});
|
||||
final newAlbum = path.join(path.dirname(album), newName);
|
||||
source.updateAfterMove(
|
||||
entries: movedEntries,
|
||||
fromAlbums: {album},
|
||||
toAlbum: newAlbum,
|
||||
copy: false,
|
||||
showOpReport<MoveOpEvent>(
|
||||
context: context,
|
||||
selection: selection,
|
||||
opStream: ImageFileService.move(selection, copy: false, destinationAlbum: destinationAlbum),
|
||||
onDone: (processed) async {
|
||||
final movedOps = processed.where((e) => e.success);
|
||||
final movedCount = movedOps.length;
|
||||
final selectionCount = selection.length;
|
||||
if (movedCount < selectionCount) {
|
||||
final count = selectionCount - movedCount;
|
||||
showFeedback(context, 'Failed to move ${Intl.plural(count, one: '$count item', other: '$count items')}');
|
||||
} else {
|
||||
showFeedback(context, 'Done!');
|
||||
}
|
||||
await source.updateAfterMove(
|
||||
selection: selection,
|
||||
copy: false,
|
||||
destinationAlbum: destinationAlbum,
|
||||
movedOps: movedOps,
|
||||
);
|
||||
final newFilter = AlbumFilter(destinationAlbum, source.getUniqueAlbumName(destinationAlbum));
|
||||
settings.pinnedFilters = settings.pinnedFilters
|
||||
..remove(filter)
|
||||
..add(newFilter);
|
||||
},
|
||||
);
|
||||
final newFilter = AlbumFilter(newAlbum, source.getUniqueAlbumName(newAlbum));
|
||||
settings.pinnedFilters = settings.pinnedFilters
|
||||
..remove(filter)
|
||||
..add(newFilter);
|
||||
|
||||
showFeedback(context, 'Done!');
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue