album: rename

This commit is contained in:
Thibault Deckers 2020-09-21 22:00:32 +09:00
parent 917b14ce6d
commit e93d46cc8d
7 changed files with 85 additions and 46 deletions

View file

@ -17,6 +17,7 @@ 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;
@ -26,6 +27,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import deckers.thibault.aves.model.AvesImageEntry;
import deckers.thibault.aves.utils.MetadataHelper;
@ -87,6 +89,7 @@ public abstract class ImageProvider {
scanNewPath(context, newFile.getPath(), mimeType, callback);
}
@SuppressWarnings("UnstableApiUsage")
public void renameDirectory(Context context, String oldDirPath, String newDirName, final AlbumRenameOpCallback callback) {
if (!oldDirPath.endsWith(File.separator)) {
oldDirPath += File.separator;
@ -98,7 +101,7 @@ public abstract class ImageProvider {
return;
}
final ArrayList<Map<String, Object>> entries = new ArrayList<>();
List<Map<String, Object>> entries = new ArrayList<>();
entries.addAll(listContentEntries(context, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, oldDirPath));
entries.addAll(listContentEntries(context, MediaStore.Video.Media.EXTERNAL_CONTENT_URI, oldDirPath));
@ -115,6 +118,7 @@ public abstract class ImageProvider {
return;
}
List<SettableFuture<Map<String, Object>>> scanFutures = new ArrayList<>();
String newDirPath = new File(oldDirPath).getParent() + File.separator + newDirName + File.separator;
for (Map<String, Object> entry : entries) {
String displayName = (String) entry.get("displayName");
@ -123,27 +127,35 @@ public abstract class ImageProvider {
String oldEntryPath = oldDirPath + displayName;
MediaScannerConnection.scanFile(context, new String[]{oldEntryPath}, new String[]{mimeType}, null);
SettableFuture<Map<String, Object>> scanFuture = SettableFuture.create();
scanFutures.add(scanFuture);
String newEntryPath = newDirPath + displayName;
scanNewPath(context, newEntryPath, mimeType, new ImageProvider.ImageOpCallback() {
@Override
public void onSuccess(Map<String, Object> newFields) {
// TODO TLAD process ID and report success
entry.putAll(newFields);
Log.d(LOG_TAG, "success with entry=" + entry);
entry.put("success", true);
scanFuture.set(entry);
}
@Override
public void onFailure(Throwable throwable) {
// TODO TLAD report failure
Log.w(LOG_TAG, "failed to scan entry=" + displayName + " in new directory=" + newDirPath, throwable);
entry.put("success", false);
scanFuture.set(entry);
}
});
}
callback.onSuccess(entries);
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) {
final ArrayList<Map<String, Object>> entries = new ArrayList<>();
List<Map<String, Object>> entries = new ArrayList<>();
String[] projection = {
MediaStore.MediaColumns._ID,
MediaStore.MediaColumns.DISPLAY_NAME,
@ -295,8 +307,8 @@ public abstract class ImageProvider {
}
// update fields in media store
@SuppressWarnings("SuspiciousNameCombination") int rotatedWidth = originalImage.getHeight();
@SuppressWarnings("SuspiciousNameCombination") int rotatedHeight = originalImage.getWidth();
int rotatedWidth = originalImage.getHeight();
int rotatedHeight = originalImage.getWidth();
Map<String, Object> newFields = new HashMap<>();
newFields.put("width", rotatedWidth);
newFields.put("height", rotatedHeight);
@ -325,8 +337,6 @@ public abstract class ImageProvider {
protected void scanNewPath(final Context context, final String path, final String mimeType, final ImageOpCallback callback) {
MediaScannerConnection.scanFile(context, new String[]{path}, new String[]{mimeType}, (newPath, newUri) -> {
Log.d(LOG_TAG, "scanNewPath onScanCompleted with newPath=" + newPath + ", newUri=" + newUri);
long contentId = 0;
Uri contentUri = null;
if (newUri != null) {

View file

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:aves/model/favourite_repo.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/image_entry.dart';
import 'package:aves/model/image_metadata.dart';
@ -87,7 +88,23 @@ class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin {
invalidateFilterEntryCounts();
}
void applyMove({
Future<void> moveEntry(ImageEntry entry, Map newFields) async {
final oldContentId = entry.contentId;
final newContentId = newFields['contentId'] as int;
entry.uri = newFields['uri'] as String;
entry.path = newFields['path'] as String;
entry.dateModifiedSecs = newFields['dateModifiedSecs'] as int;
entry.contentId = newContentId;
entry.catalogMetadata = entry.catalogMetadata?.copyWith(contentId: newContentId);
entry.addressDetails = entry.addressDetails?.copyWith(contentId: newContentId);
await metadataDb.updateEntryId(oldContentId, entry);
await metadataDb.updateMetadataId(oldContentId, entry.catalogMetadata);
await metadataDb.updateAddressId(oldContentId, entry.addressDetails);
await favourites.move(oldContentId, entry);
}
void updateAfterMove({
@required Iterable<ImageEntry> entries,
@required Set<String> fromAlbums,
@required String toAlbum,

View file

@ -1,6 +1,5 @@
import 'dart:async';
import 'package:aves/model/favourite_repo.dart';
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/image_entry.dart';
import 'package:aves/model/metadata_db.dart';
@ -149,24 +148,12 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin {
final entry = selection.firstWhere((entry) => entry.uri == sourceUri, orElse: () => null);
if (entry != null) {
fromAlbums.add(entry.directory);
final oldContentId = entry.contentId;
final newContentId = newFields['contentId'] as int;
entry.uri = newFields['uri'] as String;
entry.path = newFields['path'] as String;
entry.dateModifiedSecs = newFields['dateModifiedSecs'] as int;
entry.contentId = newContentId;
entry.catalogMetadata = entry.catalogMetadata?.copyWith(contentId: newContentId);
entry.addressDetails = entry.addressDetails?.copyWith(contentId: newContentId);
movedEntries.add(entry);
await metadataDb.updateEntryId(oldContentId, entry);
await metadataDb.updateMetadataId(oldContentId, entry.catalogMetadata);
await metadataDb.updateAddressId(oldContentId, entry.addressDetails);
await favourites.move(oldContentId, entry);
await source.moveEntry(entry, newFields);
}
});
}
source.applyMove(
source.updateAfterMove(
entries: movedEntries,
fromAlbums: fromAlbums,
toAlbum: destinationAlbum,

View file

@ -22,9 +22,6 @@ class AlbumListPage extends StatelessWidget {
final CollectionSource source;
static final ChipSetActionDelegate setActionDelegate = AlbumChipSetActionDelegate();
static final ChipActionDelegate actionDelegate = AlbumChipActionDelegate();
const AlbumListPage({@required this.source});
@override
@ -39,8 +36,8 @@ class AlbumListPage extends StatelessWidget {
builder: (context, snapshot) => FilterNavigationPage(
source: source,
title: 'Albums',
chipSetActionDelegate: setActionDelegate,
chipActionDelegate: actionDelegate,
chipSetActionDelegate: AlbumChipSetActionDelegate(),
chipActionDelegate: AlbumChipActionDelegate(source: source),
chipActionsBuilder: (filter) => [
settings.pinnedFilters.contains(filter) ? ChipAction.unpin : ChipAction.pin,
ChipAction.rename,

View file

@ -1,14 +1,19 @@
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';
import 'package:aves/utils/durations.dart';
import 'package:aves/widgets/common/action_delegates/feedback.dart';
import 'package:aves/widgets/common/action_delegates/permission_aware.dart';
import 'package:aves/widgets/common/action_delegates/rename_album_dialog.dart';
import 'package:aves/widgets/filter_grids/common/chip_actions.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:intl/intl.dart';
import 'package:path/path.dart' as path;
import 'package:pedantic/pedantic.dart';
class ChipActionDelegate {
@ -32,20 +37,26 @@ class ChipActionDelegate {
}
class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, PermissionAwareMixin {
final CollectionSource source;
AlbumChipActionDelegate({
@required this.source,
});
@override
Future<void> onActionSelected(BuildContext context, CollectionFilter filter, ChipAction action) async {
await super.onActionSelected(context, filter, action);
final album = (filter as AlbumFilter).album;
switch (action) {
case ChipAction.rename:
unawaited(_showRenameDialog(context, album));
unawaited(_showRenameDialog(context, filter as AlbumFilter));
break;
default:
break;
}
}
Future<void> _showRenameDialog(BuildContext context, String album) async {
Future<void> _showRenameDialog(BuildContext context, AlbumFilter filter) async {
final album = filter.album;
final newName = await showDialog<String>(
context: context,
builder: (context) => RenameAlbumDialog(album),
@ -54,8 +65,31 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per
if (!await checkStoragePermissionForAlbums(context, {album})) return;
// TODO TLAD rename album
final result = await ImageFileService.renameDirectory(album, newName);
showFeedback(context, result != null ? 'Done!' : 'Failed');
final bySuccess = groupBy<Map, bool>(result, (fields) => fields['success']);
final albumEntries = source.rawEntries.where(filter.filter);
final movedEntries = <ImageEntry>[];
await Future.forEach<Map>(bySuccess[true], (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);
}
});
source.updateAfterMove(
entries: movedEntries,
fromAlbums: {album},
toAlbum: path.join(path.dirname(album), newName),
copy: false,
);
final failed = bySuccess[false]?.length ?? 0;
if (failed > 0) {
showFeedback(context, 'Failed to move ${Intl.plural(failed, one: '$failed item', other: '$failed items')}');
} else {
showFeedback(context, 'Done!');
}
}
}

View file

@ -21,9 +21,6 @@ class CountryListPage extends StatelessWidget {
final CollectionSource source;
static final ChipSetActionDelegate setActionDelegate = CountryChipSetActionDelegate();
static final ChipActionDelegate actionDelegate = ChipActionDelegate();
const CountryListPage({@required this.source});
@override
@ -36,8 +33,8 @@ class CountryListPage extends StatelessWidget {
builder: (context, snapshot) => FilterNavigationPage(
source: source,
title: 'Countries',
chipSetActionDelegate: setActionDelegate,
chipActionDelegate: actionDelegate,
chipSetActionDelegate: CountryChipSetActionDelegate(),
chipActionDelegate: ChipActionDelegate(),
chipActionsBuilder: (filter) => [
settings.pinnedFilters.contains(filter) ? ChipAction.unpin : ChipAction.pin,
],

View file

@ -21,9 +21,6 @@ class TagListPage extends StatelessWidget {
final CollectionSource source;
static final ChipSetActionDelegate setActionDelegate = TagChipSetActionDelegate();
static final ChipActionDelegate actionDelegate = ChipActionDelegate();
const TagListPage({@required this.source});
@override
@ -36,8 +33,8 @@ class TagListPage extends StatelessWidget {
builder: (context, snapshot) => FilterNavigationPage(
source: source,
title: 'Tags',
chipSetActionDelegate: setActionDelegate,
chipActionDelegate: actionDelegate,
chipSetActionDelegate: TagChipSetActionDelegate(),
chipActionDelegate: ChipActionDelegate(),
chipActionsBuilder: (filter) => [
settings.pinnedFilters.contains(filter) ? ChipAction.unpin : ChipAction.pin,
],