improved metadata refreshing to include initial store data

This commit is contained in:
Thibault Deckers 2020-11-18 14:53:48 +09:00
parent dea00555e9
commit d28ea44ff2
15 changed files with 47 additions and 68 deletions

View file

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

View file

@ -143,7 +143,7 @@ class MetadataDb {
await init();
}
void removeIds(List<int> contentIds) async {
void removeIds(Set<int> 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');

View file

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

View file

@ -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<ImageEntry> get sortedEntriesForFilterList => CollectionLens(
source: this,
@ -109,7 +109,7 @@ class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin {
}
void updateAfterMove({
@required List<ImageEntry> selection,
@required Set<ImageEntry> selection,
@required bool copy,
@required String destinationAlbum,
@required Iterable<MoveOpEvent> 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<void> refresh();
Future<void> refreshMetadata(Set<ImageEntry> entries);
}
enum SourceState { loading, cataloguing, locating, ready }

View file

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

View file

@ -128,14 +128,14 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin {
}
Future<void> _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<void> _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');
}

View file

@ -31,7 +31,7 @@ mixin FeedbackMixin {
void showOpReport<T extends ImageOpEvent>({
@required BuildContext context,
@required List<ImageEntry> selection,
@required Set<ImageEntry> selection,
@required Stream<T> opStream,
@required void Function(Set<T> processed) onDone,
}) {

View file

@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
import '../aves_dialog.dart';
mixin PermissionAwareMixin {
Future<bool> checkStoragePermission(BuildContext context, Iterable<ImageEntry> entries) {
Future<bool> checkStoragePermission(BuildContext context, Set<ImageEntry> entries) {
return checkStoragePermissionForAlbums(context, entries.where((e) => e.path != null).map((e) => e.directory).toSet());
}

View file

@ -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<ImageEntry> 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<void> _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<void> _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<void> _showDeleteDialog(BuildContext context) async {
final selection = collection.selection.toList();
final count = selection.length;
final confirmed = await showDialog<bool>(
@ -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();

View file

@ -11,7 +11,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
mixin SizeAwareMixin {
Future<bool> checkFreeSpaceForMove(BuildContext context, List<ImageEntry> selection, String destinationAlbum, bool copy) async {
Future<bool> checkFreeSpaceForMove(BuildContext context, Set<ImageEntry> selection, String destinationAlbum, bool copy) async {
final destinationVolume = androidFileUtils.getStorageVolume(destinationAlbum);
final free = await AndroidFileService.getFreeSpace(destinationVolume);
int needed;

View file

@ -31,14 +31,16 @@ class MediaStoreSource extends CollectionSource {
debugPrint('$runtimeType init done, elapsed=${stopwatch.elapsed}');
}
@override
Future<void> 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<void> refreshMetadata(Set<ImageEntry> entries) {
final contentIds = entries.map((entry) => entry.contentId).toSet();
metadataDb.removeIds(contentIds, updateFavourites: false);
return refresh();
}
}

View file

@ -57,7 +57,7 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per
}
Future<void> _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<bool>(
@ -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;

View file

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

View file

@ -41,21 +41,6 @@ class _DbTabState extends State<DbTab> {
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<DateMetadata>(
future: _dbDateLoader,
builder: (context, snapshot) {

View file

@ -72,7 +72,9 @@ class _ImageViewState extends State<ImageView> {
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<ImageView> {
} 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.