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.commonsware.cwac.document.DocumentFileCompat;
|
||||||
import com.google.common.util.concurrent.Futures;
|
import com.google.common.util.concurrent.Futures;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import com.google.common.util.concurrent.SettableFuture;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
|
@ -26,6 +27,7 @@ import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
import deckers.thibault.aves.model.AvesImageEntry;
|
import deckers.thibault.aves.model.AvesImageEntry;
|
||||||
import deckers.thibault.aves.utils.MetadataHelper;
|
import deckers.thibault.aves.utils.MetadataHelper;
|
||||||
|
@ -87,6 +89,7 @@ public abstract class ImageProvider {
|
||||||
scanNewPath(context, newFile.getPath(), mimeType, callback);
|
scanNewPath(context, newFile.getPath(), mimeType, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("UnstableApiUsage")
|
||||||
public void renameDirectory(Context context, String oldDirPath, String newDirName, final AlbumRenameOpCallback callback) {
|
public void renameDirectory(Context context, String oldDirPath, String newDirName, final AlbumRenameOpCallback callback) {
|
||||||
if (!oldDirPath.endsWith(File.separator)) {
|
if (!oldDirPath.endsWith(File.separator)) {
|
||||||
oldDirPath += File.separator;
|
oldDirPath += File.separator;
|
||||||
|
@ -98,7 +101,7 @@ public abstract class ImageProvider {
|
||||||
return;
|
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.Images.Media.EXTERNAL_CONTENT_URI, oldDirPath));
|
||||||
entries.addAll(listContentEntries(context, MediaStore.Video.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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<SettableFuture<Map<String, Object>>> scanFutures = new ArrayList<>();
|
||||||
String newDirPath = new File(oldDirPath).getParent() + File.separator + newDirName + File.separator;
|
String newDirPath = new File(oldDirPath).getParent() + File.separator + newDirName + File.separator;
|
||||||
for (Map<String, Object> entry : entries) {
|
for (Map<String, Object> entry : entries) {
|
||||||
String displayName = (String) entry.get("displayName");
|
String displayName = (String) entry.get("displayName");
|
||||||
|
@ -123,27 +127,35 @@ public abstract class ImageProvider {
|
||||||
String oldEntryPath = oldDirPath + displayName;
|
String oldEntryPath = oldDirPath + displayName;
|
||||||
MediaScannerConnection.scanFile(context, new String[]{oldEntryPath}, new String[]{mimeType}, null);
|
MediaScannerConnection.scanFile(context, new String[]{oldEntryPath}, new String[]{mimeType}, null);
|
||||||
|
|
||||||
|
SettableFuture<Map<String, Object>> scanFuture = SettableFuture.create();
|
||||||
|
scanFutures.add(scanFuture);
|
||||||
String newEntryPath = newDirPath + displayName;
|
String newEntryPath = newDirPath + displayName;
|
||||||
scanNewPath(context, newEntryPath, mimeType, new ImageProvider.ImageOpCallback() {
|
scanNewPath(context, newEntryPath, mimeType, new ImageProvider.ImageOpCallback() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(Map<String, Object> newFields) {
|
public void onSuccess(Map<String, Object> newFields) {
|
||||||
// TODO TLAD process ID and report success
|
|
||||||
entry.putAll(newFields);
|
entry.putAll(newFields);
|
||||||
Log.d(LOG_TAG, "success with entry=" + entry);
|
entry.put("success", true);
|
||||||
|
scanFuture.set(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(Throwable throwable) {
|
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) {
|
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 = {
|
String[] projection = {
|
||||||
MediaStore.MediaColumns._ID,
|
MediaStore.MediaColumns._ID,
|
||||||
MediaStore.MediaColumns.DISPLAY_NAME,
|
MediaStore.MediaColumns.DISPLAY_NAME,
|
||||||
|
@ -295,8 +307,8 @@ public abstract class ImageProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
// update fields in media store
|
// update fields in media store
|
||||||
@SuppressWarnings("SuspiciousNameCombination") int rotatedWidth = originalImage.getHeight();
|
int rotatedWidth = originalImage.getHeight();
|
||||||
@SuppressWarnings("SuspiciousNameCombination") int rotatedHeight = originalImage.getWidth();
|
int rotatedHeight = originalImage.getWidth();
|
||||||
Map<String, Object> newFields = new HashMap<>();
|
Map<String, Object> newFields = new HashMap<>();
|
||||||
newFields.put("width", rotatedWidth);
|
newFields.put("width", rotatedWidth);
|
||||||
newFields.put("height", rotatedHeight);
|
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) {
|
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) -> {
|
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;
|
long contentId = 0;
|
||||||
Uri contentUri = null;
|
Uri contentUri = null;
|
||||||
if (newUri != null) {
|
if (newUri != null) {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:aves/model/favourite_repo.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/image_metadata.dart';
|
import 'package:aves/model/image_metadata.dart';
|
||||||
|
@ -87,7 +88,23 @@ class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin {
|
||||||
invalidateFilterEntryCounts();
|
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 Iterable<ImageEntry> entries,
|
||||||
@required Set<String> fromAlbums,
|
@required Set<String> fromAlbums,
|
||||||
@required String toAlbum,
|
@required String toAlbum,
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:aves/model/favourite_repo.dart';
|
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/metadata_db.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);
|
final entry = selection.firstWhere((entry) => entry.uri == sourceUri, orElse: () => null);
|
||||||
if (entry != null) {
|
if (entry != null) {
|
||||||
fromAlbums.add(entry.directory);
|
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);
|
movedEntries.add(entry);
|
||||||
|
await source.moveEntry(entry, newFields);
|
||||||
await metadataDb.updateEntryId(oldContentId, entry);
|
|
||||||
await metadataDb.updateMetadataId(oldContentId, entry.catalogMetadata);
|
|
||||||
await metadataDb.updateAddressId(oldContentId, entry.addressDetails);
|
|
||||||
await favourites.move(oldContentId, entry);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
source.applyMove(
|
source.updateAfterMove(
|
||||||
entries: movedEntries,
|
entries: movedEntries,
|
||||||
fromAlbums: fromAlbums,
|
fromAlbums: fromAlbums,
|
||||||
toAlbum: destinationAlbum,
|
toAlbum: destinationAlbum,
|
||||||
|
|
|
@ -22,9 +22,6 @@ class AlbumListPage extends StatelessWidget {
|
||||||
|
|
||||||
final CollectionSource source;
|
final CollectionSource source;
|
||||||
|
|
||||||
static final ChipSetActionDelegate setActionDelegate = AlbumChipSetActionDelegate();
|
|
||||||
static final ChipActionDelegate actionDelegate = AlbumChipActionDelegate();
|
|
||||||
|
|
||||||
const AlbumListPage({@required this.source});
|
const AlbumListPage({@required this.source});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -39,8 +36,8 @@ class AlbumListPage extends StatelessWidget {
|
||||||
builder: (context, snapshot) => FilterNavigationPage(
|
builder: (context, snapshot) => FilterNavigationPage(
|
||||||
source: source,
|
source: source,
|
||||||
title: 'Albums',
|
title: 'Albums',
|
||||||
chipSetActionDelegate: setActionDelegate,
|
chipSetActionDelegate: AlbumChipSetActionDelegate(),
|
||||||
chipActionDelegate: actionDelegate,
|
chipActionDelegate: AlbumChipActionDelegate(source: source),
|
||||||
chipActionsBuilder: (filter) => [
|
chipActionsBuilder: (filter) => [
|
||||||
settings.pinnedFilters.contains(filter) ? ChipAction.unpin : ChipAction.pin,
|
settings.pinnedFilters.contains(filter) ? ChipAction.unpin : ChipAction.pin,
|
||||||
ChipAction.rename,
|
ChipAction.rename,
|
||||||
|
|
|
@ -1,14 +1,19 @@
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/filters.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/settings/settings.dart';
|
||||||
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/services/image_file_service.dart';
|
import 'package:aves/services/image_file_service.dart';
|
||||||
import 'package:aves/utils/durations.dart';
|
import 'package:aves/utils/durations.dart';
|
||||||
import 'package:aves/widgets/common/action_delegates/feedback.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/permission_aware.dart';
|
||||||
import 'package:aves/widgets/common/action_delegates/rename_album_dialog.dart';
|
import 'package:aves/widgets/common/action_delegates/rename_album_dialog.dart';
|
||||||
import 'package:aves/widgets/filter_grids/common/chip_actions.dart';
|
import 'package:aves/widgets/filter_grids/common/chip_actions.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
import 'package:pedantic/pedantic.dart';
|
import 'package:pedantic/pedantic.dart';
|
||||||
|
|
||||||
class ChipActionDelegate {
|
class ChipActionDelegate {
|
||||||
|
@ -32,20 +37,26 @@ class ChipActionDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
||||||
|
final CollectionSource source;
|
||||||
|
|
||||||
|
AlbumChipActionDelegate({
|
||||||
|
@required this.source,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onActionSelected(BuildContext context, CollectionFilter filter, ChipAction action) async {
|
Future<void> onActionSelected(BuildContext context, CollectionFilter filter, ChipAction action) async {
|
||||||
await super.onActionSelected(context, filter, action);
|
await super.onActionSelected(context, filter, action);
|
||||||
final album = (filter as AlbumFilter).album;
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case ChipAction.rename:
|
case ChipAction.rename:
|
||||||
unawaited(_showRenameDialog(context, album));
|
unawaited(_showRenameDialog(context, filter as AlbumFilter));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
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>(
|
final newName = await showDialog<String>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => RenameAlbumDialog(album),
|
builder: (context) => RenameAlbumDialog(album),
|
||||||
|
@ -54,8 +65,31 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per
|
||||||
|
|
||||||
if (!await checkStoragePermissionForAlbums(context, {album})) return;
|
if (!await checkStoragePermissionForAlbums(context, {album})) return;
|
||||||
|
|
||||||
// TODO TLAD rename album
|
|
||||||
final result = await ImageFileService.renameDirectory(album, newName);
|
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;
|
final CollectionSource source;
|
||||||
|
|
||||||
static final ChipSetActionDelegate setActionDelegate = CountryChipSetActionDelegate();
|
|
||||||
static final ChipActionDelegate actionDelegate = ChipActionDelegate();
|
|
||||||
|
|
||||||
const CountryListPage({@required this.source});
|
const CountryListPage({@required this.source});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -36,8 +33,8 @@ class CountryListPage extends StatelessWidget {
|
||||||
builder: (context, snapshot) => FilterNavigationPage(
|
builder: (context, snapshot) => FilterNavigationPage(
|
||||||
source: source,
|
source: source,
|
||||||
title: 'Countries',
|
title: 'Countries',
|
||||||
chipSetActionDelegate: setActionDelegate,
|
chipSetActionDelegate: CountryChipSetActionDelegate(),
|
||||||
chipActionDelegate: actionDelegate,
|
chipActionDelegate: ChipActionDelegate(),
|
||||||
chipActionsBuilder: (filter) => [
|
chipActionsBuilder: (filter) => [
|
||||||
settings.pinnedFilters.contains(filter) ? ChipAction.unpin : ChipAction.pin,
|
settings.pinnedFilters.contains(filter) ? ChipAction.unpin : ChipAction.pin,
|
||||||
],
|
],
|
||||||
|
|
|
@ -21,9 +21,6 @@ class TagListPage extends StatelessWidget {
|
||||||
|
|
||||||
final CollectionSource source;
|
final CollectionSource source;
|
||||||
|
|
||||||
static final ChipSetActionDelegate setActionDelegate = TagChipSetActionDelegate();
|
|
||||||
static final ChipActionDelegate actionDelegate = ChipActionDelegate();
|
|
||||||
|
|
||||||
const TagListPage({@required this.source});
|
const TagListPage({@required this.source});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -36,8 +33,8 @@ class TagListPage extends StatelessWidget {
|
||||||
builder: (context, snapshot) => FilterNavigationPage(
|
builder: (context, snapshot) => FilterNavigationPage(
|
||||||
source: source,
|
source: source,
|
||||||
title: 'Tags',
|
title: 'Tags',
|
||||||
chipSetActionDelegate: setActionDelegate,
|
chipSetActionDelegate: TagChipSetActionDelegate(),
|
||||||
chipActionDelegate: actionDelegate,
|
chipActionDelegate: ChipActionDelegate(),
|
||||||
chipActionsBuilder: (filter) => [
|
chipActionsBuilder: (filter) => [
|
||||||
settings.pinnedFilters.contains(filter) ? ChipAction.unpin : ChipAction.pin,
|
settings.pinnedFilters.contains(filter) ? ChipAction.unpin : ChipAction.pin,
|
||||||
],
|
],
|
||||||
|
|
Loading…
Reference in a new issue