album: rename
This commit is contained in:
parent
917b14ce6d
commit
e93d46cc8d
7 changed files with 85 additions and 46 deletions
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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!');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
],
|
||||
|
|
|
@ -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,
|
||||
],
|
||||
|
|
Loading…
Reference in a new issue