From d28ea44ff22970e8acffaea69b166826c8b9da55 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 18 Nov 2020 14:53:48 +0900 Subject: [PATCH] improved metadata refreshing to include initial store data --- lib/model/image_entry.dart | 1 + lib/model/metadata_db.dart | 6 +++-- lib/model/source/collection_lens.dart | 9 ------- lib/model/source/collection_source.dart | 8 ++++-- lib/widgets/collection/app_bar.dart | 6 +---- .../entry_action_delegate.dart | 8 +++--- .../common/action_delegates/feedback.dart | 2 +- .../action_delegates/permission_aware.dart | 2 +- .../selection_action_delegate.dart | 26 +++++++------------ .../common/action_delegates/size_aware.dart | 2 +- .../media_store_collection_provider.dart | 13 ++++++++-- .../common/chip_action_delegate.dart | 4 +-- .../common/chip_set_action_delegate.dart | 6 +---- lib/widgets/fullscreen/debug/db.dart | 15 ----------- lib/widgets/fullscreen/image_view.dart | 7 ++--- 15 files changed, 47 insertions(+), 68 deletions(-) diff --git a/lib/model/image_entry.dart b/lib/model/image_entry.dart index 07b64f7e7..ba7d520cc 100644 --- a/lib/model/image_entry.dart +++ b/lib/model/image_entry.dart @@ -23,6 +23,7 @@ class ImageEntry { String _path, _directory, _filename, _extension; int contentId; final String sourceMimeType; + // TODO TLAD use SVG viewport as width/height int width; int height; diff --git a/lib/model/metadata_db.dart b/lib/model/metadata_db.dart index 721659cd1..681a38605 100644 --- a/lib/model/metadata_db.dart +++ b/lib/model/metadata_db.dart @@ -143,7 +143,7 @@ class MetadataDb { await init(); } - void removeIds(List contentIds) async { + void removeIds(Set contentIds, {@required bool updateFavourites}) async { if (contentIds == null || contentIds.isEmpty) return; final stopwatch = Stopwatch()..start(); @@ -157,7 +157,9 @@ class MetadataDb { batch.delete(dateTakenTable, where: where, whereArgs: whereArgs); batch.delete(metadataTable, where: where, whereArgs: whereArgs); batch.delete(addressTable, where: where, whereArgs: whereArgs); - batch.delete(favouriteTable, where: where, whereArgs: whereArgs); + if (updateFavourites) { + batch.delete(favouriteTable, where: where, whereArgs: whereArgs); + } }); await batch.commit(noResult: true); debugPrint('$runtimeType removeIds complete in ${stopwatch.elapsed.inMilliseconds}ms for ${contentIds.length} entries'); diff --git a/lib/model/source/collection_lens.dart b/lib/model/source/collection_lens.dart index 21b169c91..f70a7dd4b 100644 --- a/lib/model/source/collection_lens.dart +++ b/lib/model/source/collection_lens.dart @@ -4,7 +4,6 @@ import 'dart:collection'; 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/model/source/tag.dart'; import 'package:aves/utils/change_notifier.dart'; @@ -50,14 +49,6 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel super.dispose(); } - factory CollectionLens.empty() { - return CollectionLens( - source: CollectionSource(), - groupFactor: settings.collectionGroupFactor, - sortFactor: settings.collectionSortFactor, - ); - } - CollectionLens derive(CollectionFilter filter) { return CollectionLens( source: source, diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart index 7c99427bd..3c179ab25 100644 --- a/lib/model/source/collection_source.dart +++ b/lib/model/source/collection_source.dart @@ -37,7 +37,7 @@ mixin SourceBase { void setProgress({@required int done, @required int total}) => _progressStreamController.add(ProgressEvent(done: done, total: total)); } -class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin { +abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin { @override List get sortedEntriesForFilterList => CollectionLens( source: this, @@ -109,7 +109,7 @@ class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin { } void updateAfterMove({ - @required List selection, + @required Set selection, @required bool copy, @required String destinationAlbum, @required Iterable movedOps, @@ -163,6 +163,10 @@ class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin { int count(CollectionFilter filter) { return _filterEntryCountMap.putIfAbsent(filter, () => _rawEntries.where((entry) => filter.filter(entry)).length); } + + Future refresh(); + + Future refreshMetadata(Set entries); } enum SourceState { loading, cataloguing, locating, ready } diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index 6329c68f4..7642e7cf3 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -14,7 +14,6 @@ import 'package:aves/widgets/common/action_delegates/selection_action_delegate.d import 'package:aves/widgets/common/app_bar_subtitle.dart'; import 'package:aves/widgets/common/app_bar_title.dart'; import 'package:aves/widgets/common/aves_selection_dialog.dart'; -import 'package:aves/widgets/common/data_providers/media_store_collection_provider.dart'; import 'package:aves/widgets/common/entry_actions.dart'; import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/common/menu_row.dart'; @@ -290,10 +289,7 @@ class _CollectionAppBarState extends State with SingleTickerPr _actionDelegate.onCollectionActionSelected(context, action); break; case CollectionAction.refresh: - if (source is MediaStoreSource) { - source.clearEntries(); - unawaited((source as MediaStoreSource).refresh()); - } + unawaited(source.refresh()); break; case CollectionAction.select: collection.select(); diff --git a/lib/widgets/common/action_delegates/entry_action_delegate.dart b/lib/widgets/common/action_delegates/entry_action_delegate.dart index de7a06885..ec4ccd357 100644 --- a/lib/widgets/common/action_delegates/entry_action_delegate.dart +++ b/lib/widgets/common/action_delegates/entry_action_delegate.dart @@ -128,14 +128,14 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin { } Future _flip(BuildContext context, ImageEntry entry) async { - if (!await checkStoragePermission(context, [entry])) return; + if (!await checkStoragePermission(context, {entry})) return; final success = await entry.flip(); if (!success) showFeedback(context, 'Failed'); } Future _rotate(BuildContext context, ImageEntry entry, {@required bool clockwise}) async { - if (!await checkStoragePermission(context, [entry])) return; + if (!await checkStoragePermission(context, {entry})) return; final success = await entry.rotate(clockwise: clockwise); if (!success) showFeedback(context, 'Failed'); @@ -162,7 +162,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin { ); if (confirmed == null || !confirmed) return; - if (!await checkStoragePermission(context, [entry])) return; + if (!await checkStoragePermission(context, {entry})) return; if (!await entry.delete()) { showFeedback(context, 'Failed'); @@ -185,7 +185,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin { ); if (newName == null || newName.isEmpty) return; - if (!await checkStoragePermission(context, [entry])) return; + if (!await checkStoragePermission(context, {entry})) return; showFeedback(context, await entry.rename(newName) ? 'Done!' : 'Failed'); } diff --git a/lib/widgets/common/action_delegates/feedback.dart b/lib/widgets/common/action_delegates/feedback.dart index f103df3cd..4f87464b0 100644 --- a/lib/widgets/common/action_delegates/feedback.dart +++ b/lib/widgets/common/action_delegates/feedback.dart @@ -31,7 +31,7 @@ mixin FeedbackMixin { void showOpReport({ @required BuildContext context, - @required List selection, + @required Set selection, @required Stream opStream, @required void Function(Set processed) onDone, }) { diff --git a/lib/widgets/common/action_delegates/permission_aware.dart b/lib/widgets/common/action_delegates/permission_aware.dart index 18ba08630..13098a4d4 100644 --- a/lib/widgets/common/action_delegates/permission_aware.dart +++ b/lib/widgets/common/action_delegates/permission_aware.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import '../aves_dialog.dart'; mixin PermissionAwareMixin { - Future checkStoragePermission(BuildContext context, Iterable entries) { + Future checkStoragePermission(BuildContext context, Set entries) { return checkStoragePermissionForAlbums(context, entries.where((e) => e.path != null).map((e) => e.directory).toSet()); } diff --git a/lib/widgets/common/action_delegates/selection_action_delegate.dart b/lib/widgets/common/action_delegates/selection_action_delegate.dart index 966ed4272..b089d1f89 100644 --- a/lib/widgets/common/action_delegates/selection_action_delegate.dart +++ b/lib/widgets/common/action_delegates/selection_action_delegate.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:aves/model/filters/album.dart'; +import 'package:aves/model/image_entry.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; @@ -29,6 +30,10 @@ import 'package:provider/provider.dart'; class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { final CollectionLens collection; + CollectionSource get source => collection.source; + + Set get selection => collection.selection; + SelectionActionDelegate({ @required this.collection, }); @@ -39,7 +44,7 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwar _showDeleteDialog(context); break; case EntryAction.share: - AndroidAppService.share(collection.selection); + AndroidAppService.share(selection); break; default: break; @@ -55,7 +60,9 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwar _moveSelection(context, copy: false); break; case CollectionAction.refreshMetadata: - _refreshSelectionMetadata(); + source.refreshMetadata(selection); + collection.clearSelection(); + collection.browse(); break; default: break; @@ -63,7 +70,6 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwar } Future _moveSelection(BuildContext context, {@required bool copy}) async { - final source = collection.source; final chipSetActionDelegate = AlbumChipSetActionDelegate(source: source); final destinationAlbum = await Navigator.push( context, @@ -114,7 +120,6 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwar if (destinationAlbum == null || destinationAlbum.isEmpty) return; if (!await checkStoragePermissionForAlbums(context, {destinationAlbum})) return; - final selection = collection.selection.toList(); if (!await checkStoragePermission(context, selection)) return; if (!await checkFreeSpaceForMove(context, selection, destinationAlbum, copy)) return; @@ -146,18 +151,7 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwar ); } - Future _refreshSelectionMetadata() async { - collection.selection.forEach((entry) => entry.clearMetadata()); - final source = collection.source; - source.stateNotifier.value = SourceState.cataloguing; - await source.catalogEntries(); - source.stateNotifier.value = SourceState.locating; - await source.locateEntries(); - source.stateNotifier.value = SourceState.ready; - } - Future _showDeleteDialog(BuildContext context) async { - final selection = collection.selection.toList(); final count = selection.length; final confirmed = await showDialog( @@ -195,7 +189,7 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwar showFeedback(context, 'Failed to delete ${Intl.plural(count, one: '$count item', other: '$count items')}'); } if (deletedCount > 0) { - collection.source.removeEntries(selection.where((e) => deletedUris.contains(e.uri)).toList()); + source.removeEntries(selection.where((e) => deletedUris.contains(e.uri)).toList()); } collection.clearSelection(); collection.browse(); diff --git a/lib/widgets/common/action_delegates/size_aware.dart b/lib/widgets/common/action_delegates/size_aware.dart index 4198846bb..f9af342c7 100644 --- a/lib/widgets/common/action_delegates/size_aware.dart +++ b/lib/widgets/common/action_delegates/size_aware.dart @@ -11,7 +11,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; mixin SizeAwareMixin { - Future checkFreeSpaceForMove(BuildContext context, List selection, String destinationAlbum, bool copy) async { + Future checkFreeSpaceForMove(BuildContext context, Set selection, String destinationAlbum, bool copy) async { final destinationVolume = androidFileUtils.getStorageVolume(destinationAlbum); final free = await AndroidFileService.getFreeSpace(destinationVolume); int needed; diff --git a/lib/widgets/common/data_providers/media_store_collection_provider.dart b/lib/widgets/common/data_providers/media_store_collection_provider.dart index ec6ab0e35..c0afae1aa 100644 --- a/lib/widgets/common/data_providers/media_store_collection_provider.dart +++ b/lib/widgets/common/data_providers/media_store_collection_provider.dart @@ -31,14 +31,16 @@ class MediaStoreSource extends CollectionSource { debugPrint('$runtimeType init done, elapsed=${stopwatch.elapsed}'); } + @override Future refresh() async { debugPrint('$runtimeType refresh start'); final stopwatch = Stopwatch()..start(); stateNotifier.value = SourceState.loading; + clearEntries(); final oldEntries = await metadataDb.loadEntries(); // 400ms for 5500 entries final knownEntryMap = Map.fromEntries(oldEntries.map((entry) => MapEntry(entry.contentId, entry.dateModifiedSecs))); - final obsoleteEntries = await ImageFileService.getObsoleteEntries(knownEntryMap.keys.toList()); + final obsoleteEntries = (await ImageFileService.getObsoleteEntries(knownEntryMap.keys.toList())).toSet(); oldEntries.removeWhere((entry) => obsoleteEntries.contains(entry.contentId)); // show known entries @@ -48,7 +50,7 @@ class MediaStoreSource extends CollectionSource { debugPrint('$runtimeType refresh loaded ${oldEntries.length} known entries, elapsed=${stopwatch.elapsed}'); // clean up obsolete entries - metadataDb.removeIds(obsoleteEntries); + metadataDb.removeIds(obsoleteEntries, updateFavourites: true); // fetch new entries var refreshCount = 10; @@ -92,4 +94,11 @@ class MediaStoreSource extends CollectionSource { onError: (error) => debugPrint('$runtimeType stream error=$error'), ); } + + @override + Future refreshMetadata(Set entries) { + final contentIds = entries.map((entry) => entry.contentId).toSet(); + metadataDb.removeIds(contentIds, updateFavourites: false); + return refresh(); + } } diff --git a/lib/widgets/filter_grids/common/chip_action_delegate.dart b/lib/widgets/filter_grids/common/chip_action_delegate.dart index 248e966da..5b842db7b 100644 --- a/lib/widgets/filter_grids/common/chip_action_delegate.dart +++ b/lib/widgets/filter_grids/common/chip_action_delegate.dart @@ -57,7 +57,7 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per } Future _showDeleteDialog(BuildContext context, AlbumFilter filter) async { - final selection = source.rawEntries.where(filter.filter).toList(); + final selection = source.rawEntries.where(filter.filter).toSet(); final count = selection.length; final confirmed = await showDialog( @@ -111,7 +111,7 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per if (!await checkStoragePermissionForAlbums(context, {album})) return; - final selection = source.rawEntries.where(filter.filter).toList(); + final selection = source.rawEntries.where(filter.filter).toSet(); final destinationAlbum = path.join(path.dirname(album), newName); if (!await checkFreeSpaceForMove(context, selection, destinationAlbum, false)) return; diff --git a/lib/widgets/filter_grids/common/chip_set_action_delegate.dart b/lib/widgets/filter_grids/common/chip_set_action_delegate.dart index 29911ebd0..39efb54a6 100644 --- a/lib/widgets/filter_grids/common/chip_set_action_delegate.dart +++ b/lib/widgets/filter_grids/common/chip_set_action_delegate.dart @@ -4,7 +4,6 @@ import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/enums.dart'; import 'package:aves/utils/durations.dart'; import 'package:aves/widgets/common/aves_selection_dialog.dart'; -import 'package:aves/widgets/common/data_providers/media_store_collection_provider.dart'; import 'package:aves/widgets/filter_grids/common/chip_actions.dart'; import 'package:aves/widgets/stats/stats.dart'; import 'package:flutter/material.dart'; @@ -27,10 +26,7 @@ abstract class ChipSetActionDelegate { await _showSortDialog(context); break; case ChipSetAction.refresh: - if (source is MediaStoreSource) { - source.clearEntries(); - unawaited((source as MediaStoreSource).refresh()); - } + unawaited(source.refresh()); break; case ChipSetAction.stats: _goToStats(context); diff --git a/lib/widgets/fullscreen/debug/db.dart b/lib/widgets/fullscreen/debug/db.dart index ce2b89699..4abe39a68 100644 --- a/lib/widgets/fullscreen/debug/db.dart +++ b/lib/widgets/fullscreen/debug/db.dart @@ -41,21 +41,6 @@ class _DbTabState extends State { return ListView( padding: EdgeInsets.all(16), children: [ - Row( - children: [ - Expanded( - child: Text('DB'), - ), - SizedBox(width: 8), - ElevatedButton( - onPressed: () async { - await metadataDb.removeIds([entry.contentId]); - _loadDatabase(); - }, - child: Text('Remove from DB'), - ), - ], - ), FutureBuilder( future: _dbDateLoader, builder: (context, snapshot) { diff --git a/lib/widgets/fullscreen/image_view.dart b/lib/widgets/fullscreen/image_view.dart index 327cc27c2..f1e57d2ca 100644 --- a/lib/widgets/fullscreen/image_view.dart +++ b/lib/widgets/fullscreen/image_view.dart @@ -72,7 +72,9 @@ class _ImageViewState extends State { Widget build(BuildContext context) { Widget child; if (entry.isVideo) { - child = _buildVideoView(); + if (entry.width > 0 && entry.height > 0) { + child = _buildVideoView(); + } } else if (entry.isSvg) { child = _buildSvgView(); } else if (entry.canDecode) { @@ -81,9 +83,8 @@ class _ImageViewState extends State { } else { child = _buildImageView(); } - } else { - child = _buildError(); } + child ??= _buildError(); // if the hero tag is defined in the `loadingBuilder` and also set by the `heroAttributes`, // the route transition becomes visible if the final image is loaded before the hero animation is done.