improved metadata refreshing to include initial store data
This commit is contained in:
parent
dea00555e9
commit
d28ea44ff2
15 changed files with 47 additions and 68 deletions
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}) {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in a new issue