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.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());

View file

@ -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 }

View file

@ -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);
}

View file

@ -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;

View file

@ -106,24 +106,35 @@ class MetadataDb {
final stopwatch = Stopwatch()..start();
final db = await _database;
final batch = db.batch();
metadataEntries.where((metadata) => metadata != null).forEach((metadata) {
if (metadata.dateMillis != 0) {
batch.insert(
dateTakenTable,
DateMetadata(contentId: metadata.contentId, dateMillis: metadata.dateMillis).toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
batch.insert(
metadataTable,
metadata.toMap(boolAsInteger: true),
conflictAlgorithm: ConflictAlgorithm.replace,
);
});
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,
DateMetadata(contentId: metadata.contentId, dateMillis: metadata.dateMillis).toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
batch.insert(
metadataTable,
metadata.toMap(boolAsInteger: true),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
// address
Future<void> clearAddresses() async {
@ -146,15 +157,27 @@ class MetadataDb {
final stopwatch = Stopwatch()..start();
final db = await _database;
final batch = db.batch();
addresses.where((address) => address != null).forEach((address) => batch.insert(
addressTable,
address.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
));
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,
);
}
// favourites
Future<void> clearFavourites() async {
@ -177,15 +200,27 @@ class MetadataDb {
// final stopwatch = Stopwatch()..start();
final db = await _database;
final batch = db.batch();
favouriteRows.where((row) => row != null).forEach((row) => batch.insert(
favouriteTable,
row.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
));
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,
);
}
Future<void> removeFavourites(Iterable<FavouriteRow> favouriteRows) async {
if (favouriteRows == null || favouriteRows.isEmpty) return;
final ids = favouriteRows.where((row) => row != null).map((row) => row.contentId);
@ -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');
}

View file

@ -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}');

View file

@ -82,7 +82,7 @@ class _LicensesState extends State<Licenses> {
setState(() {});
},
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(
value: CollectionAction.move,
// TODO TLAD enable when handled on native side
enabled: false,
child: MenuRow(text: 'Move to album'),
),
const PopupMenuItem(

View file

@ -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,

View file

@ -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();