move: update source, DB, lenses
This commit is contained in:
parent
cae7e6570d
commit
a437c2fe9a
10 changed files with 156 additions and 71 deletions
|
@ -25,6 +25,7 @@ import java.io.File;
|
|||
import java.io.FileNotFoundException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import deckers.thibault.aves.model.ImageEntry;
|
||||
|
@ -263,7 +264,14 @@ public class MediaStoreImageProvider extends ImageProvider {
|
|||
DocumentFileCompat destination = DocumentFileCompat.fromSingleUri(activity, destinationUri);
|
||||
source.copyTo(destination);
|
||||
|
||||
// TODO TLAD delete source when it is a `move`
|
||||
if (!copy) {
|
||||
// delete original entry
|
||||
try {
|
||||
delete(activity, sourcePath, sourceUri).get();
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
Log.w(LOG_TAG, "failed to delete entry with path=" + sourcePath, e);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> newFields = new HashMap<>();
|
||||
newFields.put("uri", destinationUri.toString());
|
||||
|
|
|
@ -31,10 +31,11 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
|
|||
}) : filters = {if (filters != null) ...filters.where((f) => f != null)},
|
||||
groupFactor = groupFactor ?? GroupFactor.month,
|
||||
sortFactor = sortFactor ?? SortFactor.date {
|
||||
_subscriptions.add(source.eventBus.on<EntryAddedEvent>().listen((e) => onEntryAdded()));
|
||||
_subscriptions.add(source.eventBus.on<EntryAddedEvent>().listen((e) => _refresh()));
|
||||
_subscriptions.add(source.eventBus.on<EntryRemovedEvent>().listen((e) => onEntryRemoved(e.entries)));
|
||||
_subscriptions.add(source.eventBus.on<CatalogMetadataChangedEvent>().listen((e) => onMetadataChanged()));
|
||||
onEntryAdded();
|
||||
_subscriptions.add(source.eventBus.on<EntryMovedEvent>().listen((e) => _refresh()));
|
||||
_subscriptions.add(source.eventBus.on<CatalogMetadataChangedEvent>().listen((e) => _refresh()));
|
||||
_refresh();
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -107,9 +108,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
|
|||
}
|
||||
|
||||
void onFilterChanged() {
|
||||
_applyFilters();
|
||||
_applySort();
|
||||
_applyGroup();
|
||||
_refresh();
|
||||
filterChangeNotifier.notifyListeners();
|
||||
}
|
||||
|
||||
|
@ -180,7 +179,9 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
|
|||
notifyListeners();
|
||||
}
|
||||
|
||||
void onEntryAdded() {
|
||||
// metadata change should also trigger a full refresh
|
||||
// as dates impact sorting and grouping
|
||||
void _refresh() {
|
||||
_applyFilters();
|
||||
_applySort();
|
||||
_applyGroup();
|
||||
|
@ -194,13 +195,6 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
|
|||
selection.removeAll(entries);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void onMetadataChanged() {
|
||||
_applyFilters();
|
||||
// metadata dates impact sorting and grouping
|
||||
_applySort();
|
||||
_applyGroup();
|
||||
}
|
||||
}
|
||||
|
||||
enum SortFactor { date, size, name }
|
||||
|
|
|
@ -146,9 +146,24 @@ class CollectionSource {
|
|||
void removeEntries(Iterable<ImageEntry> entries) async {
|
||||
entries.forEach((entry) => entry.removeFromFavourites());
|
||||
_rawEntries.removeWhere(entries.contains);
|
||||
cleanEmptyAlbums(entries.map((entry) => entry.directory).toSet());
|
||||
eventBus.fire(EntryRemovedEvent(entries));
|
||||
}
|
||||
|
||||
void notifyMovedEntries(Iterable<ImageEntry> movedEntries) {
|
||||
eventBus.fire(EntryMovedEvent(entries));
|
||||
}
|
||||
|
||||
void cleanEmptyAlbums(Set<String> albums) {
|
||||
final emptyAlbums = albums.where(_isEmptyAlbum);
|
||||
if (emptyAlbums.isNotEmpty) {
|
||||
_folderPaths.removeAll(emptyAlbums);
|
||||
updateAlbums();
|
||||
}
|
||||
}
|
||||
|
||||
bool _isEmptyAlbum(String album) => !_rawEntries.any((entry) => entry.directory == album);
|
||||
|
||||
String getUniqueAlbumName(String album) {
|
||||
final otherAlbums = _folderPaths.where((item) => item != album);
|
||||
final parts = album.split(separator);
|
||||
|
@ -231,3 +246,9 @@ class EntryRemovedEvent {
|
|||
|
||||
const EntryRemovedEvent(this.entries);
|
||||
}
|
||||
|
||||
class EntryMovedEvent {
|
||||
final Iterable<ImageEntry> entries;
|
||||
|
||||
const EntryMovedEvent(this.entries);
|
||||
}
|
||||
|
|
|
@ -17,8 +17,9 @@ import 'mime_types.dart';
|
|||
|
||||
class ImageEntry {
|
||||
String uri;
|
||||
String path;
|
||||
String directory;
|
||||
String _path;
|
||||
String _directory;
|
||||
String _filename;
|
||||
int contentId;
|
||||
final String mimeType;
|
||||
int width;
|
||||
|
@ -38,7 +39,7 @@ class ImageEntry {
|
|||
|
||||
ImageEntry({
|
||||
this.uri,
|
||||
this.path,
|
||||
String path,
|
||||
this.contentId,
|
||||
this.mimeType,
|
||||
this.width,
|
||||
|
@ -49,7 +50,8 @@ class ImageEntry {
|
|||
this.dateModifiedSecs,
|
||||
this.sourceDateTakenMillis,
|
||||
this.durationMillis,
|
||||
}) : directory = path != null ? dirname(path) : null {
|
||||
}) {
|
||||
this.path = path;
|
||||
isFavouriteNotifier.value = isFavourite;
|
||||
}
|
||||
|
||||
|
@ -126,7 +128,23 @@ class ImageEntry {
|
|||
return 'ImageEntry{uri=$uri, path=$path}';
|
||||
}
|
||||
|
||||
String get filename => basenameWithoutExtension(path);
|
||||
set path(String path) {
|
||||
_path = path;
|
||||
_directory = null;
|
||||
_filename = null;
|
||||
}
|
||||
|
||||
String get path => _path;
|
||||
|
||||
String get directory {
|
||||
_directory ??= path != null ? dirname(path) : null;
|
||||
return _directory;
|
||||
}
|
||||
|
||||
String get filenameWithoutExtension {
|
||||
_filename ??= path != null ? basenameWithoutExtension(path) : null;
|
||||
return _filename;
|
||||
}
|
||||
|
||||
String get mimeTypeAnySubtype => mimeType.replaceAll(RegExp('/.*'), '/*');
|
||||
|
||||
|
@ -286,7 +304,7 @@ class ImageEntry {
|
|||
}
|
||||
|
||||
Future<bool> rename(String newName) async {
|
||||
if (newName == filename) return true;
|
||||
if (newName == filenameWithoutExtension) return true;
|
||||
|
||||
final newFields = await ImageFileService.rename(this, '$newName${extension(this.path)}');
|
||||
if (newFields.isEmpty) return false;
|
||||
|
|
|
@ -106,7 +106,21 @@ class MetadataDb {
|
|||
final stopwatch = Stopwatch()..start();
|
||||
final db = await _database;
|
||||
final batch = db.batch();
|
||||
metadataEntries.where((metadata) => metadata != null).forEach((metadata) {
|
||||
metadataEntries.where((metadata) => metadata != null).forEach((metadata) => _batchInsertMetadata(batch, metadata));
|
||||
await batch.commit(noResult: true);
|
||||
debugPrint('$runtimeType saveMetadata complete in ${stopwatch.elapsed.inMilliseconds}ms for ${metadataEntries.length} entries');
|
||||
}
|
||||
|
||||
Future<void> updateMetadataId(int oldId, CatalogMetadata metadata) async {
|
||||
final db = await _database;
|
||||
final batch = db.batch();
|
||||
batch.delete(dateTakenTable, where: 'contentId = ?', whereArgs: [oldId]);
|
||||
batch.delete(metadataTable, where: 'contentId = ?', whereArgs: [oldId]);
|
||||
_batchInsertMetadata(batch, metadata);
|
||||
await batch.commit(noResult: true);
|
||||
}
|
||||
|
||||
void _batchInsertMetadata(Batch batch, CatalogMetadata metadata) {
|
||||
if (metadata.dateMillis != 0) {
|
||||
batch.insert(
|
||||
dateTakenTable,
|
||||
|
@ -119,9 +133,6 @@ class MetadataDb {
|
|||
metadata.toMap(boolAsInteger: true),
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
});
|
||||
await batch.commit(noResult: true);
|
||||
debugPrint('$runtimeType saveMetadata complete in ${stopwatch.elapsed.inMilliseconds}ms for ${metadataEntries.length} entries');
|
||||
}
|
||||
|
||||
// address
|
||||
|
@ -146,13 +157,25 @@ class MetadataDb {
|
|||
final stopwatch = Stopwatch()..start();
|
||||
final db = await _database;
|
||||
final batch = db.batch();
|
||||
addresses.where((address) => address != null).forEach((address) => batch.insert(
|
||||
addresses.where((address) => address != null).forEach((address) => _batchInsertAddress(batch, address));
|
||||
await batch.commit(noResult: true);
|
||||
debugPrint('$runtimeType saveAddresses complete in ${stopwatch.elapsed.inMilliseconds}ms for ${addresses.length} entries');
|
||||
}
|
||||
|
||||
Future<void> updateAddressId(int oldId, AddressDetails address) async {
|
||||
final db = await _database;
|
||||
final batch = db.batch();
|
||||
batch.delete(addressTable, where: 'contentId = ?', whereArgs: [oldId]);
|
||||
_batchInsertAddress(batch, address);
|
||||
await batch.commit(noResult: true);
|
||||
}
|
||||
|
||||
void _batchInsertAddress(Batch batch, AddressDetails address) {
|
||||
batch.insert(
|
||||
addressTable,
|
||||
address.toMap(),
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
));
|
||||
await batch.commit(noResult: true);
|
||||
debugPrint('$runtimeType saveAddresses complete in ${stopwatch.elapsed.inMilliseconds}ms for ${addresses.length} entries');
|
||||
);
|
||||
}
|
||||
|
||||
// favourites
|
||||
|
@ -177,13 +200,25 @@ class MetadataDb {
|
|||
// final stopwatch = Stopwatch()..start();
|
||||
final db = await _database;
|
||||
final batch = db.batch();
|
||||
favouriteRows.where((row) => row != null).forEach((row) => batch.insert(
|
||||
favouriteRows.where((row) => row != null).forEach((row) => _batchInsertFavourite(batch, row));
|
||||
await batch.commit(noResult: true);
|
||||
// debugPrint('$runtimeType addFavourites complete in ${stopwatch.elapsed.inMilliseconds}ms for ${favouriteRows.length} entries');
|
||||
}
|
||||
|
||||
Future<void> updateFavouriteId(int oldId, FavouriteRow row) async {
|
||||
final db = await _database;
|
||||
final batch = db.batch();
|
||||
batch.delete(favouriteTable, where: 'contentId = ?', whereArgs: [oldId]);
|
||||
_batchInsertFavourite(batch, row);
|
||||
await batch.commit(noResult: true);
|
||||
}
|
||||
|
||||
void _batchInsertFavourite(Batch batch, FavouriteRow row) {
|
||||
batch.insert(
|
||||
favouriteTable,
|
||||
row.toMap(),
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
));
|
||||
await batch.commit(noResult: true);
|
||||
// debugPrint('$runtimeType addFavourites complete in ${stopwatch.elapsed.inMilliseconds}ms for ${favouriteRows.length} entries');
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> removeFavourites(Iterable<FavouriteRow> favouriteRows) async {
|
||||
|
@ -195,11 +230,7 @@ class MetadataDb {
|
|||
// final stopwatch = Stopwatch()..start();
|
||||
final db = await _database;
|
||||
final batch = db.batch();
|
||||
ids.forEach((id) => batch.delete(
|
||||
favouriteTable,
|
||||
where: 'contentId = ?',
|
||||
whereArgs: [id],
|
||||
));
|
||||
ids.forEach((id) => batch.delete(favouriteTable, where: 'contentId = ?', whereArgs: [id]));
|
||||
await batch.commit(noResult: true);
|
||||
// debugPrint('$runtimeType removeFavourites complete in ${stopwatch.elapsed.inMilliseconds}ms for ${favouriteRows.length} entries');
|
||||
}
|
||||
|
|
|
@ -84,7 +84,7 @@ class ImageFileService {
|
|||
|
||||
static Future<T> resumeThumbnail<T>(Object taskKey) => servicePolicy.resume<T>(taskKey, thumbnailPriority);
|
||||
|
||||
static Stream<ImageOpEvent> delete(List<ImageEntry> entries) {
|
||||
static Stream<ImageOpEvent> delete(Iterable<ImageEntry> entries) {
|
||||
try {
|
||||
return opChannel.receiveBroadcastStream(<String, dynamic>{
|
||||
'op': 'delete',
|
||||
|
@ -96,14 +96,14 @@ class ImageFileService {
|
|||
}
|
||||
}
|
||||
|
||||
static Stream<MoveOpEvent> move(List<ImageEntry> entries, {@required bool copy, @required String destinationPath}) {
|
||||
static Stream<MoveOpEvent> move(Iterable<ImageEntry> entries, {@required bool copy, @required String destinationAlbum}) {
|
||||
debugPrint('move ${entries.length} entries');
|
||||
try {
|
||||
return opChannel.receiveBroadcastStream(<String, dynamic>{
|
||||
'op': 'move',
|
||||
'entries': entries.map((e) => e.toMap()).toList(),
|
||||
'copy': copy,
|
||||
'destinationPath': destinationPath,
|
||||
'destinationPath': destinationAlbum,
|
||||
}).map((event) => MoveOpEvent.fromMap(event));
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('move failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
|
|
|
@ -82,7 +82,7 @@ class _LicensesState extends State<Licenses> {
|
|||
setState(() {});
|
||||
},
|
||||
tooltip: 'Sort',
|
||||
icon: Icon(AIcons.sort),
|
||||
icon: const Icon(AIcons.sort),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -188,8 +188,6 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
),
|
||||
const PopupMenuItem(
|
||||
value: CollectionAction.move,
|
||||
// TODO TLAD enable when handled on native side
|
||||
enabled: false,
|
||||
child: MenuRow(text: 'Move to album'),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
|
|
|
@ -167,7 +167,7 @@ class EntryActionDelegate with PermissionAwareMixin {
|
|||
}
|
||||
|
||||
Future<void> _showRenameDialog(BuildContext context, ImageEntry entry) async {
|
||||
final currentName = entry.filename ?? entry.sourceTitle;
|
||||
final currentName = entry.filenameWithoutExtension ?? entry.sourceTitle;
|
||||
final controller = TextEditingController(text: currentName);
|
||||
final newName = await showDialog<String>(
|
||||
context: context,
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'dart:async';
|
|||
import 'package:aves/model/collection_lens.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/model/image_metadata.dart';
|
||||
import 'package:aves/model/metadata_db.dart';
|
||||
import 'package:aves/services/android_app_service.dart';
|
||||
import 'package:aves/services/image_file_service.dart';
|
||||
|
@ -87,7 +88,7 @@ class SelectionActionDelegate with PermissionAwareMixin {
|
|||
_showOpReport(
|
||||
context: context,
|
||||
selection: selection,
|
||||
opStream: ImageFileService.move(selection, copy: copy, destinationPath: filter.album),
|
||||
opStream: ImageFileService.move(selection, copy: copy, destinationAlbum: filter.album),
|
||||
onDone: (Set<MoveOpEvent> processed) {
|
||||
debugPrint('$runtimeType _moveSelection onDone');
|
||||
final movedOps = processed.where((e) => e.success);
|
||||
|
@ -98,30 +99,44 @@ class SelectionActionDelegate with PermissionAwareMixin {
|
|||
_showFeedback(context, 'Failed to move ${Intl.plural(count, one: '${count} item', other: '${count} items')}');
|
||||
}
|
||||
if (movedCount > 0) {
|
||||
final source = collection.source;
|
||||
if (copy) {
|
||||
final newEntries = <ImageEntry>[];
|
||||
final newFavs = <ImageEntry>[];
|
||||
movedOps.forEach((movedOp) {
|
||||
final newEntries = movedOps.map((movedOp) {
|
||||
final sourceUri = movedOp.uri;
|
||||
final newFields = movedOp.newFields;
|
||||
final sourceEntry = selection.firstWhere((entry) => entry.uri == sourceUri, orElse: () => null);
|
||||
final copy = sourceEntry?.copyWith(
|
||||
return sourceEntry?.copyWith(
|
||||
uri: newFields['uri'] as String,
|
||||
path: newFields['path'] as String,
|
||||
contentId: newFields['contentId'] as int,
|
||||
);
|
||||
newEntries.add(copy);
|
||||
if (sourceEntry.isFavourite) {
|
||||
newFavs.add(copy);
|
||||
}
|
||||
});
|
||||
collection.source.addAll(newEntries);
|
||||
}).toList();
|
||||
source.addAll(newEntries);
|
||||
metadataDb.saveMetadata(newEntries.map((entry) => entry.catalogMetadata));
|
||||
metadataDb.saveAddresses(newEntries.map((entry) => entry.addressDetails));
|
||||
newFavs.forEach((entry) => entry.addToFavourites());
|
||||
} else {
|
||||
// TODO TLAD update old entries path/dir/ID
|
||||
// TODO TLAD update DB for catalog/address/fav
|
||||
final movedEntries = <ImageEntry>[];
|
||||
final fromAlbums = <String>{};
|
||||
movedOps.forEach((movedOp) {
|
||||
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);
|
||||
final oldContentId = entry.contentId;
|
||||
final newContentId = newFields['contentId'] as int;
|
||||
entry.uri = newFields['uri'] as String;
|
||||
entry.path = newFields['path'] as String;
|
||||
entry.contentId = newContentId;
|
||||
|
||||
metadataDb.updateMetadataId(oldContentId, entry.catalogMetadata);
|
||||
metadataDb.updateAddressId(oldContentId, entry.addressDetails);
|
||||
metadataDb.updateFavouriteId(oldContentId, FavouriteRow(contentId: entry.contentId, path: entry.path));
|
||||
}
|
||||
movedEntries.add(entry);
|
||||
});
|
||||
source.cleanEmptyAlbums(fromAlbums);
|
||||
source.notifyMovedEntries(movedEntries);
|
||||
}
|
||||
}
|
||||
collection.clearSelection();
|
||||
|
|
Loading…
Reference in a new issue