move: update source, DB, lenses

This commit is contained in:
Thibault Deckers 2020-05-31 10:38:24 +09:00
parent cae7e6570d
commit a437c2fe9a
10 changed files with 156 additions and 71 deletions

View file

@ -25,6 +25,7 @@ import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.stream.Stream; import java.util.stream.Stream;
import deckers.thibault.aves.model.ImageEntry; import deckers.thibault.aves.model.ImageEntry;
@ -263,7 +264,14 @@ public class MediaStoreImageProvider extends ImageProvider {
DocumentFileCompat destination = DocumentFileCompat.fromSingleUri(activity, destinationUri); DocumentFileCompat destination = DocumentFileCompat.fromSingleUri(activity, destinationUri);
source.copyTo(destination); 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<>(); Map<String, Object> newFields = new HashMap<>();
newFields.put("uri", destinationUri.toString()); newFields.put("uri", destinationUri.toString());

View file

@ -31,10 +31,11 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
}) : filters = {if (filters != null) ...filters.where((f) => f != null)}, }) : filters = {if (filters != null) ...filters.where((f) => f != null)},
groupFactor = groupFactor ?? GroupFactor.month, groupFactor = groupFactor ?? GroupFactor.month,
sortFactor = sortFactor ?? SortFactor.date { 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<EntryRemovedEvent>().listen((e) => onEntryRemoved(e.entries)));
_subscriptions.add(source.eventBus.on<CatalogMetadataChangedEvent>().listen((e) => onMetadataChanged())); _subscriptions.add(source.eventBus.on<EntryMovedEvent>().listen((e) => _refresh()));
onEntryAdded(); _subscriptions.add(source.eventBus.on<CatalogMetadataChangedEvent>().listen((e) => _refresh()));
_refresh();
} }
@override @override
@ -107,9 +108,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
} }
void onFilterChanged() { void onFilterChanged() {
_applyFilters(); _refresh();
_applySort();
_applyGroup();
filterChangeNotifier.notifyListeners(); filterChangeNotifier.notifyListeners();
} }
@ -180,7 +179,9 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
notifyListeners(); notifyListeners();
} }
void onEntryAdded() { // metadata change should also trigger a full refresh
// as dates impact sorting and grouping
void _refresh() {
_applyFilters(); _applyFilters();
_applySort(); _applySort();
_applyGroup(); _applyGroup();
@ -194,13 +195,6 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
selection.removeAll(entries); selection.removeAll(entries);
notifyListeners(); notifyListeners();
} }
void onMetadataChanged() {
_applyFilters();
// metadata dates impact sorting and grouping
_applySort();
_applyGroup();
}
} }
enum SortFactor { date, size, name } enum SortFactor { date, size, name }

View file

@ -146,9 +146,24 @@ class CollectionSource {
void removeEntries(Iterable<ImageEntry> entries) async { void removeEntries(Iterable<ImageEntry> entries) async {
entries.forEach((entry) => entry.removeFromFavourites()); entries.forEach((entry) => entry.removeFromFavourites());
_rawEntries.removeWhere(entries.contains); _rawEntries.removeWhere(entries.contains);
cleanEmptyAlbums(entries.map((entry) => entry.directory).toSet());
eventBus.fire(EntryRemovedEvent(entries)); 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) { String getUniqueAlbumName(String album) {
final otherAlbums = _folderPaths.where((item) => item != album); final otherAlbums = _folderPaths.where((item) => item != album);
final parts = album.split(separator); final parts = album.split(separator);
@ -231,3 +246,9 @@ class EntryRemovedEvent {
const EntryRemovedEvent(this.entries); const EntryRemovedEvent(this.entries);
} }
class EntryMovedEvent {
final Iterable<ImageEntry> entries;
const EntryMovedEvent(this.entries);
}

View file

@ -17,8 +17,9 @@ import 'mime_types.dart';
class ImageEntry { class ImageEntry {
String uri; String uri;
String path; String _path;
String directory; String _directory;
String _filename;
int contentId; int contentId;
final String mimeType; final String mimeType;
int width; int width;
@ -38,7 +39,7 @@ class ImageEntry {
ImageEntry({ ImageEntry({
this.uri, this.uri,
this.path, String path,
this.contentId, this.contentId,
this.mimeType, this.mimeType,
this.width, this.width,
@ -49,7 +50,8 @@ class ImageEntry {
this.dateModifiedSecs, this.dateModifiedSecs,
this.sourceDateTakenMillis, this.sourceDateTakenMillis,
this.durationMillis, this.durationMillis,
}) : directory = path != null ? dirname(path) : null { }) {
this.path = path;
isFavouriteNotifier.value = isFavourite; isFavouriteNotifier.value = isFavourite;
} }
@ -126,7 +128,23 @@ class ImageEntry {
return 'ImageEntry{uri=$uri, path=$path}'; 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('/.*'), '/*'); String get mimeTypeAnySubtype => mimeType.replaceAll(RegExp('/.*'), '/*');
@ -286,7 +304,7 @@ class ImageEntry {
} }
Future<bool> rename(String newName) async { 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)}'); final newFields = await ImageFileService.rename(this, '$newName${extension(this.path)}');
if (newFields.isEmpty) return false; if (newFields.isEmpty) return false;

View file

@ -106,7 +106,21 @@ class MetadataDb {
final stopwatch = Stopwatch()..start(); final stopwatch = Stopwatch()..start();
final db = await _database; final db = await _database;
final batch = db.batch(); 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) { if (metadata.dateMillis != 0) {
batch.insert( batch.insert(
dateTakenTable, dateTakenTable,
@ -119,9 +133,6 @@ class MetadataDb {
metadata.toMap(boolAsInteger: true), metadata.toMap(boolAsInteger: true),
conflictAlgorithm: ConflictAlgorithm.replace, conflictAlgorithm: ConflictAlgorithm.replace,
); );
});
await batch.commit(noResult: true);
debugPrint('$runtimeType saveMetadata complete in ${stopwatch.elapsed.inMilliseconds}ms for ${metadataEntries.length} entries');
} }
// address // address
@ -146,13 +157,25 @@ class MetadataDb {
final stopwatch = Stopwatch()..start(); final stopwatch = Stopwatch()..start();
final db = await _database; final db = await _database;
final batch = db.batch(); 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, addressTable,
address.toMap(), address.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace, conflictAlgorithm: ConflictAlgorithm.replace,
)); );
await batch.commit(noResult: true);
debugPrint('$runtimeType saveAddresses complete in ${stopwatch.elapsed.inMilliseconds}ms for ${addresses.length} entries');
} }
// favourites // favourites
@ -177,13 +200,25 @@ class MetadataDb {
// final stopwatch = Stopwatch()..start(); // final stopwatch = Stopwatch()..start();
final db = await _database; final db = await _database;
final batch = db.batch(); 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, favouriteTable,
row.toMap(), row.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace, 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 { Future<void> removeFavourites(Iterable<FavouriteRow> favouriteRows) async {
@ -195,11 +230,7 @@ class MetadataDb {
// final stopwatch = Stopwatch()..start(); // final stopwatch = Stopwatch()..start();
final db = await _database; final db = await _database;
final batch = db.batch(); final batch = db.batch();
ids.forEach((id) => batch.delete( ids.forEach((id) => batch.delete(favouriteTable, where: 'contentId = ?', whereArgs: [id]));
favouriteTable,
where: 'contentId = ?',
whereArgs: [id],
));
await batch.commit(noResult: true); await batch.commit(noResult: true);
// debugPrint('$runtimeType removeFavourites complete in ${stopwatch.elapsed.inMilliseconds}ms for ${favouriteRows.length} entries'); // debugPrint('$runtimeType removeFavourites complete in ${stopwatch.elapsed.inMilliseconds}ms for ${favouriteRows.length} entries');
} }

View file

@ -84,7 +84,7 @@ class ImageFileService {
static Future<T> resumeThumbnail<T>(Object taskKey) => servicePolicy.resume<T>(taskKey, thumbnailPriority); 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 { try {
return opChannel.receiveBroadcastStream(<String, dynamic>{ return opChannel.receiveBroadcastStream(<String, dynamic>{
'op': 'delete', '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'); debugPrint('move ${entries.length} entries');
try { try {
return opChannel.receiveBroadcastStream(<String, dynamic>{ return opChannel.receiveBroadcastStream(<String, dynamic>{
'op': 'move', 'op': 'move',
'entries': entries.map((e) => e.toMap()).toList(), 'entries': entries.map((e) => e.toMap()).toList(),
'copy': copy, 'copy': copy,
'destinationPath': destinationPath, 'destinationPath': destinationAlbum,
}).map((event) => MoveOpEvent.fromMap(event)); }).map((event) => MoveOpEvent.fromMap(event));
} on PlatformException catch (e) { } on PlatformException catch (e) {
debugPrint('move failed with code=${e.code}, exception=${e.message}, details=${e.details}'); debugPrint('move failed with code=${e.code}, exception=${e.message}, details=${e.details}');

View file

@ -82,7 +82,7 @@ class _LicensesState extends State<Licenses> {
setState(() {}); setState(() {});
}, },
tooltip: 'Sort', tooltip: 'Sort',
icon: Icon(AIcons.sort), icon: const Icon(AIcons.sort),
), ),
], ],
), ),

View file

@ -188,8 +188,6 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
), ),
const PopupMenuItem( const PopupMenuItem(
value: CollectionAction.move, value: CollectionAction.move,
// TODO TLAD enable when handled on native side
enabled: false,
child: MenuRow(text: 'Move to album'), child: MenuRow(text: 'Move to album'),
), ),
const PopupMenuItem( const PopupMenuItem(

View file

@ -167,7 +167,7 @@ class EntryActionDelegate with PermissionAwareMixin {
} }
Future<void> _showRenameDialog(BuildContext context, ImageEntry entry) async { 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 controller = TextEditingController(text: currentName);
final newName = await showDialog<String>( final newName = await showDialog<String>(
context: context, context: context,

View file

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:aves/model/collection_lens.dart'; import 'package:aves/model/collection_lens.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/image_metadata.dart';
import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/metadata_db.dart';
import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/android_app_service.dart';
import 'package:aves/services/image_file_service.dart'; import 'package:aves/services/image_file_service.dart';
@ -87,7 +88,7 @@ class SelectionActionDelegate with PermissionAwareMixin {
_showOpReport( _showOpReport(
context: context, context: context,
selection: selection, selection: selection,
opStream: ImageFileService.move(selection, copy: copy, destinationPath: filter.album), opStream: ImageFileService.move(selection, copy: copy, destinationAlbum: filter.album),
onDone: (Set<MoveOpEvent> processed) { onDone: (Set<MoveOpEvent> processed) {
debugPrint('$runtimeType _moveSelection onDone'); debugPrint('$runtimeType _moveSelection onDone');
final movedOps = processed.where((e) => e.success); 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')}'); _showFeedback(context, 'Failed to move ${Intl.plural(count, one: '${count} item', other: '${count} items')}');
} }
if (movedCount > 0) { if (movedCount > 0) {
final source = collection.source;
if (copy) { if (copy) {
final newEntries = <ImageEntry>[]; final newEntries = movedOps.map((movedOp) {
final newFavs = <ImageEntry>[];
movedOps.forEach((movedOp) {
final sourceUri = movedOp.uri; final sourceUri = movedOp.uri;
final newFields = movedOp.newFields; final newFields = movedOp.newFields;
final sourceEntry = selection.firstWhere((entry) => entry.uri == sourceUri, orElse: () => null); final sourceEntry = selection.firstWhere((entry) => entry.uri == sourceUri, orElse: () => null);
final copy = sourceEntry?.copyWith( return sourceEntry?.copyWith(
uri: newFields['uri'] as String, uri: newFields['uri'] as String,
path: newFields['path'] as String, path: newFields['path'] as String,
contentId: newFields['contentId'] as int, contentId: newFields['contentId'] as int,
); );
newEntries.add(copy); }).toList();
if (sourceEntry.isFavourite) { source.addAll(newEntries);
newFavs.add(copy);
}
});
collection.source.addAll(newEntries);
metadataDb.saveMetadata(newEntries.map((entry) => entry.catalogMetadata)); metadataDb.saveMetadata(newEntries.map((entry) => entry.catalogMetadata));
metadataDb.saveAddresses(newEntries.map((entry) => entry.addressDetails)); metadataDb.saveAddresses(newEntries.map((entry) => entry.addressDetails));
newFavs.forEach((entry) => entry.addToFavourites());
} else { } else {
// TODO TLAD update old entries path/dir/ID final movedEntries = <ImageEntry>[];
// TODO TLAD update DB for catalog/address/fav 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(); collection.clearSelection();