source init scope review

This commit is contained in:
Thibault Deckers 2024-10-29 00:04:53 +01:00
parent 687ca5eb41
commit 33ffb1cd1a
10 changed files with 83 additions and 73 deletions

View file

@ -32,7 +32,7 @@ import 'package:event_bus/event_bus.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:leak_tracker/leak_tracker.dart'; import 'package:leak_tracker/leak_tracker.dart';
enum SourceScope { none, album, full } typedef SourceScope = Set<CollectionFilter>?;
mixin SourceBase { mixin SourceBase {
EventBus get eventBus; EventBus get eventBus;
@ -63,6 +63,8 @@ mixin SourceBase {
} }
abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, PlaceMixin, StateMixin, LocationMixin, TagMixin, TrashMixin { abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, PlaceMixin, StateMixin, LocationMixin, TagMixin, TrashMixin {
static const fullScope = <CollectionFilter>{};
CollectionSource() { CollectionSource() {
if (kFlutterMemoryAllocationsEnabled) { if (kFlutterMemoryAllocationsEnabled) {
LeakTracking.dispatchObjectCreated( LeakTracking.dispatchObjectCreated(
@ -428,11 +430,13 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
eventBus.fire(EntryMovedEvent(MoveType.move, movedEntries)); eventBus.fire(EntryMovedEvent(MoveType.move, movedEntries));
} }
SourceScope get scope => SourceScope.none; SourceScope get loadedScope;
SourceScope get targetScope;
Future<void> init({ Future<void> init({
required SourceScope scope,
AnalysisController? analysisController, AnalysisController? analysisController,
AlbumFilter? albumFilter,
bool loadTopEntriesFirst = false, bool loadTopEntriesFirst = false,
}); });

View file

@ -21,38 +21,40 @@ class MediaStoreSource extends CollectionSource {
final Debouncer _changeDebouncer = Debouncer(delay: ADurations.mediaContentChangeDebounceDelay); final Debouncer _changeDebouncer = Debouncer(delay: ADurations.mediaContentChangeDebounceDelay);
final Set<String> _changedUris = {}; final Set<String> _changedUris = {};
int? _lastGeneration; int? _lastGeneration;
SourceScope _scope = SourceScope.none; SourceScope _loadedScope, _targetScope;
bool _canAnalyze = true; bool _canAnalyze = true;
@override @override
set canAnalyze(bool enabled) => _canAnalyze = enabled; set canAnalyze(bool enabled) => _canAnalyze = enabled;
@override @override
SourceScope get scope => _scope; SourceScope get loadedScope => _loadedScope;
@override
SourceScope get targetScope => _targetScope;
@override @override
Future<void> init({ Future<void> init({
required SourceScope scope,
AnalysisController? analysisController, AnalysisController? analysisController,
AlbumFilter? albumFilter,
bool loadTopEntriesFirst = false, bool loadTopEntriesFirst = false,
}) async { }) async {
await reportService.log('$runtimeType init album=${albumFilter?.album}'); _targetScope = scope;
if (_scope == SourceScope.none) { await reportService.log('$runtimeType init target scope=$scope');
await _loadEssentials(); await _loadEssentials();
}
if (_scope != SourceScope.full) {
_scope = albumFilter != null ? SourceScope.album : SourceScope.full;
}
addDirectories(albums: settings.pinnedFilters.whereType<AlbumFilter>().map((v) => v.album).toSet()); addDirectories(albums: settings.pinnedFilters.whereType<AlbumFilter>().map((v) => v.album).toSet());
await updateGeneration(); await updateGeneration();
unawaited(_loadEntries( unawaited(_loadEntries(
analysisController: analysisController, analysisController: analysisController,
directory: albumFilter?.album,
loadTopEntriesFirst: loadTopEntriesFirst, loadTopEntriesFirst: loadTopEntriesFirst,
)); ));
} }
bool _areEssentialsLoaded = false;
Future<void> _loadEssentials() async { Future<void> _loadEssentials() async {
if (_areEssentialsLoaded) return;
final stopwatch = Stopwatch()..start(); final stopwatch = Stopwatch()..start();
state = SourceState.loading; state = SourceState.loading;
await localMediaDb.init(); await localMediaDb.init();
@ -63,20 +65,19 @@ class MediaStoreSource extends CollectionSource {
if (currentTimeZoneOffset != null) { if (currentTimeZoneOffset != null) {
final catalogTimeZoneOffset = settings.catalogTimeZoneRawOffsetMillis; final catalogTimeZoneOffset = settings.catalogTimeZoneRawOffsetMillis;
if (currentTimeZoneOffset != catalogTimeZoneOffset) { if (currentTimeZoneOffset != catalogTimeZoneOffset) {
// clear catalog metadata to get correct date/times when moving to a different time zone unawaited(reportService.log('Time zone offset change: $currentTimeZoneOffset -> $catalogTimeZoneOffset. Clear catalog metadata to get correct date/times.'));
debugPrint('$runtimeType clear catalog metadata to get correct date/times');
await localMediaDb.clearDates(); await localMediaDb.clearDates();
await localMediaDb.clearCatalogMetadata(); await localMediaDb.clearCatalogMetadata();
settings.catalogTimeZoneRawOffsetMillis = currentTimeZoneOffset; settings.catalogTimeZoneRawOffsetMillis = currentTimeZoneOffset;
} }
} }
await loadDates(); await loadDates();
_areEssentialsLoaded = true;
debugPrint('$runtimeType load essentials complete in ${stopwatch.elapsed.inMilliseconds}ms'); debugPrint('$runtimeType load essentials complete in ${stopwatch.elapsed.inMilliseconds}ms');
} }
Future<void> _loadEntries({ Future<void> _loadEntries({
AnalysisController? analysisController, AnalysisController? analysisController,
String? directory,
required bool loadTopEntriesFirst, required bool loadTopEntriesFirst,
}) async { }) async {
unawaited(reportService.log('$runtimeType load (known) start')); unawaited(reportService.log('$runtimeType load (known) start'));
@ -84,6 +85,9 @@ class MediaStoreSource extends CollectionSource {
state = SourceState.loading; state = SourceState.loading;
clearEntries(); clearEntries();
final scopeAlbumFilters = _targetScope?.whereType<AlbumFilter>();
final scopeDirectory = scopeAlbumFilters != null && scopeAlbumFilters.length == 1 ? scopeAlbumFilters.first.album : null;
final Set<AvesEntry> topEntries = {}; final Set<AvesEntry> topEntries = {};
if (loadTopEntriesFirst) { if (loadTopEntriesFirst) {
final topIds = settings.topEntryIds?.toSet(); final topIds = settings.topEntryIds?.toSet();
@ -95,7 +99,7 @@ class MediaStoreSource extends CollectionSource {
} }
debugPrint('$runtimeType load ${stopwatch.elapsed} fetch known entries'); debugPrint('$runtimeType load ${stopwatch.elapsed} fetch known entries');
final knownEntries = await localMediaDb.loadEntries(origin: EntryOrigins.mediaStoreContent, directory: directory); final knownEntries = await localMediaDb.loadEntries(origin: EntryOrigins.mediaStoreContent, directory: scopeDirectory);
final knownLiveEntries = knownEntries.where((entry) => !entry.trashed).toSet(); final knownLiveEntries = knownEntries.where((entry) => !entry.trashed).toSet();
debugPrint('$runtimeType load ${stopwatch.elapsed} check obsolete entries'); debugPrint('$runtimeType load ${stopwatch.elapsed} check obsolete entries');
@ -114,18 +118,20 @@ class MediaStoreSource extends CollectionSource {
// add entries without notifying, so that the collection is not refreshed // add entries without notifying, so that the collection is not refreshed
// with items that may be hidden right away because of their metadata // with items that may be hidden right away because of their metadata
addEntries(knownEntries, notify: false); addEntries(knownEntries, notify: false);
// but use album notification without waiting for cataloguing
// so that it is more reactive when picking an album in view mode
notifyAlbumsChanged();
await _loadVaultEntries(directory); await _loadVaultEntries(scopeDirectory);
debugPrint('$runtimeType load ${stopwatch.elapsed} load metadata'); debugPrint('$runtimeType load ${stopwatch.elapsed} load metadata');
if (directory != null) { if (scopeDirectory != null) {
final ids = knownLiveEntries.map((entry) => entry.id).toSet(); final ids = knownLiveEntries.map((entry) => entry.id).toSet();
await loadCatalogMetadata(ids: ids); await loadCatalogMetadata(ids: ids);
await loadAddresses(ids: ids); await loadAddresses(ids: ids);
} else { } else {
await loadCatalogMetadata(); await loadCatalogMetadata();
await loadAddresses(); await loadAddresses();
updateDerivedFilters();
// trash // trash
await loadTrashDetails(); await loadTrashDetails();
@ -139,6 +145,7 @@ class MediaStoreSource extends CollectionSource {
onError: (error) => debugPrint('failed to evict expired trash error=$error'), onError: (error) => debugPrint('failed to evict expired trash error=$error'),
)); ));
} }
updateDerivedFilters();
// clean up obsolete entries // clean up obsolete entries
if (removedEntries.isNotEmpty) { if (removedEntries.isNotEmpty) {
@ -146,13 +153,14 @@ class MediaStoreSource extends CollectionSource {
await localMediaDb.removeIds(removedEntries.map((entry) => entry.id).toSet()); await localMediaDb.removeIds(removedEntries.map((entry) => entry.id).toSet());
} }
_loadedScope = _targetScope;
unawaited(reportService.log('$runtimeType load (known) done in ${stopwatch.elapsed.inSeconds}s for ${knownEntries.length} known, ${removedEntries.length} removed')); unawaited(reportService.log('$runtimeType load (known) done in ${stopwatch.elapsed.inSeconds}s for ${knownEntries.length} known, ${removedEntries.length} removed'));
if (_canAnalyze) { if (_canAnalyze) {
// it can discover new entries only if it can analyze them // it can discover new entries only if it can analyze them
await _loadNewEntries( await _loadNewEntries(
analysisController: analysisController, analysisController: analysisController,
directory: directory, directory: scopeDirectory,
knownLiveEntries: knownLiveEntries, knownLiveEntries: knownLiveEntries,
knownDateByContentId: knownDateByContentId, knownDateByContentId: knownDateByContentId,
); );
@ -252,7 +260,7 @@ class MediaStoreSource extends CollectionSource {
// sometimes yields an entry with its temporary path: `/data/sec/camera/!@#$%^..._temp.jpg` // sometimes yields an entry with its temporary path: `/data/sec/camera/!@#$%^..._temp.jpg`
@override @override
Future<Set<String>> refreshUris(Set<String> changedUris, {AnalysisController? analysisController}) async { Future<Set<String>> refreshUris(Set<String> changedUris, {AnalysisController? analysisController}) async {
if (_scope == SourceScope.none || !canRefresh || !isReady) return changedUris; if (!canRefresh || !_areEssentialsLoaded || !isReady) return changedUris;
state = SourceState.loading; state = SourceState.loading;

View file

@ -5,6 +5,7 @@ import 'package:aves/l10n/l10n.dart';
import 'package:aves/model/device.dart'; import 'package:aves/model/device.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/analysis_controller.dart'; import 'package:aves/model/source/analysis_controller.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/media_store_source.dart'; import 'package:aves/model/source/media_store_source.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
@ -148,7 +149,7 @@ class Analyzer with WidgetsBindingObserver {
settings.systemLocalesFallback = await deviceService.getLocales(); settings.systemLocalesFallback = await deviceService.getLocales();
_l10n = await AppLocalizations.delegate.load(settings.appliedLocale); _l10n = await AppLocalizations.delegate.load(settings.appliedLocale);
_serviceStateNotifier.value = AnalyzerState.running; _serviceStateNotifier.value = AnalyzerState.running;
await _source.init(analysisController: _controller); await _source.init(scope: CollectionSource.fullScope, analysisController: _controller);
_notificationUpdateTimer = Timer.periodic(notificationUpdateInterval, (_) async { _notificationUpdateTimer = Timer.periodic(notificationUpdateInterval, (_) async {
if (!isRunning) return; if (!isRunning) return;

View file

@ -97,7 +97,7 @@ Future<AvesEntry?> _getWidgetEntry(int widgetId, bool reuseEntry) async {
} }
}); });
source.canAnalyze = false; source.canAnalyze = false;
await source.init(); await source.init(scope: filters);
await readyCompleter.future; await readyCompleter.future;
final entries = CollectionLens(source: source, filters: filters).sortedEntries; final entries = CollectionLens(source: source, filters: filters).sortedEntries;

View file

@ -685,11 +685,9 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
Future<void> _onAnalysisCompletion() async { Future<void> _onAnalysisCompletion() async {
debugPrint('Analysis completed'); debugPrint('Analysis completed');
if (_mediaStoreSource.scope != SourceScope.none) { await _mediaStoreSource.loadCatalogMetadata();
await _mediaStoreSource.loadCatalogMetadata(); await _mediaStoreSource.loadAddresses();
await _mediaStoreSource.loadAddresses(); _mediaStoreSource.updateDerivedFilters();
_mediaStoreSource.updateDerivedFilters();
}
} }
void _onError(String? error) => reportService.recordError(error, null); void _onError(String? error) => reportService.recordError(error, null);

View file

@ -498,7 +498,7 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge
_checkingStoragePermission = false; _checkingStoragePermission = false;
_isStoragePermissionGranted.then((granted) { _isStoragePermissionGranted.then((granted) {
if (granted) { if (granted) {
widget.collection.source.init(); widget.collection.source.init(scope: CollectionSource.fullScope);
} }
}); });
} }

View file

@ -35,11 +35,11 @@ Future<String?> pickAlbum({
required MoveType? moveType, required MoveType? moveType,
}) async { }) async {
final source = context.read<CollectionSource>(); final source = context.read<CollectionSource>();
if (source.scope != SourceScope.full) { if (source.targetScope != CollectionSource.fullScope) {
await reportService.log('Complete source initialization to pick album'); await reportService.log('Complete source initialization to pick album');
// source may not be fully initialized in view mode // source may not be fully initialized in view mode
source.canAnalyze = true; source.canAnalyze = true;
await source.init(); await source.init(scope: CollectionSource.fullScope);
} }
final filter = await Navigator.maybeOf(context)?.push( final filter = await Navigator.maybeOf(context)?.push(
MaterialPageRoute<AlbumFilter>( MaterialPageRoute<AlbumFilter>(

View file

@ -222,16 +222,17 @@ class _HomePageState extends State<HomePage> {
unawaited(GlobalSearch.registerCallback()); unawaited(GlobalSearch.registerCallback());
unawaited(AnalysisService.registerCallback()); unawaited(AnalysisService.registerCallback());
final source = context.read<CollectionSource>(); final source = context.read<CollectionSource>();
if (source.scope != SourceScope.full) { if (source.loadedScope != CollectionSource.fullScope) {
await reportService.log('Initialize source (init state=${source.scope.name}) to start app with mode=$appMode'); await reportService.log('Initialize source to start app with mode=$appMode, loaded scope=${source.loadedScope}');
final loadTopEntriesFirst = settings.homePage == HomePageSetting.collection && settings.homeCustomCollection.isEmpty; final loadTopEntriesFirst = settings.homePage == HomePageSetting.collection && settings.homeCustomCollection.isEmpty;
await source.init(loadTopEntriesFirst: loadTopEntriesFirst); source.canAnalyze = true;
await source.init(scope: CollectionSource.fullScope, loadTopEntriesFirst: loadTopEntriesFirst);
} }
case AppMode.screenSaver: case AppMode.screenSaver:
await reportService.log('Initialize source to start screen saver'); await reportService.log('Initialize source to start screen saver');
final source = context.read<CollectionSource>(); final source = context.read<CollectionSource>();
source.canAnalyze = false; source.canAnalyze = false;
await source.init(); await source.init(scope: settings.screenSaverCollectionFilters);
case AppMode.view: case AppMode.view:
if (_isViewerSourceable(_viewerEntry) && _secureUris == null) { if (_isViewerSourceable(_viewerEntry) && _secureUris == null) {
final directory = _viewerEntry?.directory; final directory = _viewerEntry?.directory;
@ -240,7 +241,7 @@ class _HomePageState extends State<HomePage> {
await reportService.log('Initialize source to view item in directory $directory'); await reportService.log('Initialize source to view item in directory $directory');
final source = context.read<CollectionSource>(); final source = context.read<CollectionSource>();
source.canAnalyze = false; source.canAnalyze = false;
await source.init(albumFilter: AlbumFilter(directory, null)); await source.init(scope: {AlbumFilter(directory, null)});
} }
} else { } else {
await _initViewerEssentials(); await _initViewerEssentials();
@ -305,38 +306,38 @@ class _HomePageState extends State<HomePage> {
CollectionLens? collection; CollectionLens? collection;
final source = context.read<CollectionSource>(); final source = context.read<CollectionSource>();
if (source.scope != SourceScope.none) { final album = viewerEntry.directory;
final album = viewerEntry.directory; if (album != null) {
if (album != null) { // wait for collection to pass the `loading` state
// wait for collection to pass the `loading` state final completer = Completer();
final completer = Completer(); final stateNotifier = source.stateNotifier;
void _onSourceStateChanged() { void _onSourceStateChanged() {
if (source.state != SourceState.loading) { if (stateNotifier.value != SourceState.loading) {
source.stateNotifier.removeListener(_onSourceStateChanged); stateNotifier.removeListener(_onSourceStateChanged);
completer.complete(); completer.complete();
}
} }
}
source.stateNotifier.addListener(_onSourceStateChanged); stateNotifier.addListener(_onSourceStateChanged);
await completer.future; _onSourceStateChanged();
await completer.future;
collection = CollectionLens( collection = CollectionLens(
source: source, source: source,
filters: {AlbumFilter(album, source.getAlbumDisplayName(context, album))}, filters: {AlbumFilter(album, source.getAlbumDisplayName(context, album))},
listenToSource: false, listenToSource: false,
// if we group bursts, opening a burst sub-entry should: // if we group bursts, opening a burst sub-entry should:
// - identify and select the containing main entry, // - identify and select the containing main entry,
// - select the sub-entry in the Viewer page. // - select the sub-entry in the Viewer page.
stackBursts: false, stackBursts: false,
); );
final viewerEntryPath = viewerEntry.path; final viewerEntryPath = viewerEntry.path;
final collectionEntry = collection.sortedEntries.firstWhereOrNull((entry) => entry.path == viewerEntryPath); final collectionEntry = collection.sortedEntries.firstWhereOrNull((entry) => entry.path == viewerEntryPath);
if (collectionEntry != null) { if (collectionEntry != null) {
viewerEntry = collectionEntry; viewerEntry = collectionEntry;
} else { } else {
debugPrint('collection does not contain viewerEntry=$viewerEntry'); debugPrint('collection does not contain viewerEntry=$viewerEntry');
collection = null; collection = null;
}
} }
} }

View file

@ -442,9 +442,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
showFeedback(context, FeedbackType.warn, l10n.genericFailureFeedback); showFeedback(context, FeedbackType.warn, l10n.genericFailureFeedback);
} else { } else {
final source = context.read<CollectionSource>(); final source = context.read<CollectionSource>();
if (source.scope != SourceScope.none) { await source.removeEntries({targetEntry.uri}, includeTrash: true);
await source.removeEntries({targetEntry.uri}, includeTrash: true);
}
EntryDeletedNotification({targetEntry}).dispatch(context); EntryDeletedNotification({targetEntry}).dispatch(context);
} }
} }

View file

@ -91,7 +91,7 @@ void main() {
readyCompleter.complete(); readyCompleter.complete();
} }
}); });
await source.init(); await source.init(scope: CollectionSource.fullScope);
await readyCompleter.future; await readyCompleter.future;
return source; return source;
} }
@ -107,9 +107,9 @@ void main() {
(mediaFetchService as FakeMediaFetchService).entries = {refreshEntry}; (mediaFetchService as FakeMediaFetchService).entries = {refreshEntry};
final source = MediaStoreSource(); final source = MediaStoreSource();
unawaited(source.init()); unawaited(source.init(scope: CollectionSource.fullScope));
await Future.delayed(const Duration(milliseconds: 10)); await Future.delayed(const Duration(milliseconds: 10));
expect(source.scope, SourceScope.full); expect(source.targetScope, CollectionSource.fullScope);
await source.refreshUris({refreshEntry.uri}); await source.refreshUris({refreshEntry.uri});
await Future.delayed(const Duration(seconds: 1)); await Future.delayed(const Duration(seconds: 1));