From e93d46cc8d7e229d80877fd195eb4a2296a7f8db Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 21 Sep 2020 22:00:32 +0900 Subject: [PATCH] album: rename --- .../aves/model/provider/ImageProvider.java | 30 ++++++++----- lib/model/source/collection_source.dart | 19 +++++++- .../selection_action_delegate.dart | 17 +------ lib/widgets/filter_grids/albums_page.dart | 7 +-- .../common/chip_action_delegate.dart | 44 ++++++++++++++++--- lib/widgets/filter_grids/countries_page.dart | 7 +-- lib/widgets/filter_grids/tags_page.dart | 7 +-- 7 files changed, 85 insertions(+), 46 deletions(-) diff --git a/android/app/src/main/java/deckers/thibault/aves/model/provider/ImageProvider.java b/android/app/src/main/java/deckers/thibault/aves/model/provider/ImageProvider.java index def2cf250..14a0a620c 100644 --- a/android/app/src/main/java/deckers/thibault/aves/model/provider/ImageProvider.java +++ b/android/app/src/main/java/deckers/thibault/aves/model/provider/ImageProvider.java @@ -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> entries = new ArrayList<>(); + List> 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>> scanFutures = new ArrayList<>(); String newDirPath = new File(oldDirPath).getParent() + File.separator + newDirName + File.separator; for (Map 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> scanFuture = SettableFuture.create(); + scanFutures.add(scanFuture); String newEntryPath = newDirPath + displayName; scanNewPath(context, newEntryPath, mimeType, new ImageProvider.ImageOpCallback() { @Override public void onSuccess(Map 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> listContentEntries(Context context, Uri contentUri, String dirPath) { - final ArrayList> entries = new ArrayList<>(); + List> 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 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) { diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart index 55b7aec41..3f2c173d7 100644 --- a/lib/model/source/collection_source.dart +++ b/lib/model/source/collection_source.dart @@ -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 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 entries, @required Set fromAlbums, @required String toAlbum, diff --git a/lib/widgets/common/action_delegates/selection_action_delegate.dart b/lib/widgets/common/action_delegates/selection_action_delegate.dart index 126569a7e..d9320a80a 100644 --- a/lib/widgets/common/action_delegates/selection_action_delegate.dart +++ b/lib/widgets/common/action_delegates/selection_action_delegate.dart @@ -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, diff --git a/lib/widgets/filter_grids/albums_page.dart b/lib/widgets/filter_grids/albums_page.dart index 34b406322..b736fcd5f 100644 --- a/lib/widgets/filter_grids/albums_page.dart +++ b/lib/widgets/filter_grids/albums_page.dart @@ -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, diff --git a/lib/widgets/filter_grids/common/chip_action_delegate.dart b/lib/widgets/filter_grids/common/chip_action_delegate.dart index 5909a1de0..4fdf14035 100644 --- a/lib/widgets/filter_grids/common/chip_action_delegate.dart +++ b/lib/widgets/filter_grids/common/chip_action_delegate.dart @@ -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 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 _showRenameDialog(BuildContext context, String album) async { + Future _showRenameDialog(BuildContext context, AlbumFilter filter) async { + final album = filter.album; final newName = await showDialog( 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(result, (fields) => fields['success']); + + final albumEntries = source.rawEntries.where(filter.filter); + final movedEntries = []; + await Future.forEach(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!'); + } } } diff --git a/lib/widgets/filter_grids/countries_page.dart b/lib/widgets/filter_grids/countries_page.dart index ad505976f..482a477ed 100644 --- a/lib/widgets/filter_grids/countries_page.dart +++ b/lib/widgets/filter_grids/countries_page.dart @@ -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, ], diff --git a/lib/widgets/filter_grids/tags_page.dart b/lib/widgets/filter_grids/tags_page.dart index af9179d83..366e4d8ca 100644 --- a/lib/widgets/filter_grids/tags_page.dart +++ b/lib/widgets/filter_grids/tags_page.dart @@ -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, ],