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

View file

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

View file

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

View file

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

View file

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

View file

@ -498,7 +498,7 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge
_checkingStoragePermission = false;
_isStoragePermissionGranted.then((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,
}) async {
final source = context.read<CollectionSource>();
if (source.scope != SourceScope.full) {
if (source.targetScope != CollectionSource.fullScope) {
await reportService.log('Complete source initialization to pick album');
// source may not be fully initialized in view mode
source.canAnalyze = true;
await source.init();
await source.init(scope: CollectionSource.fullScope);
}
final filter = await Navigator.maybeOf(context)?.push(
MaterialPageRoute<AlbumFilter>(

View file

@ -222,16 +222,17 @@ class _HomePageState extends State<HomePage> {
unawaited(GlobalSearch.registerCallback());
unawaited(AnalysisService.registerCallback());
final source = context.read<CollectionSource>();
if (source.scope != SourceScope.full) {
await reportService.log('Initialize source (init state=${source.scope.name}) to start app with mode=$appMode');
if (source.loadedScope != CollectionSource.fullScope) {
await reportService.log('Initialize source to start app with mode=$appMode, loaded scope=${source.loadedScope}');
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:
await reportService.log('Initialize source to start screen saver');
final source = context.read<CollectionSource>();
source.canAnalyze = false;
await source.init();
await source.init(scope: settings.screenSaverCollectionFilters);
case AppMode.view:
if (_isViewerSourceable(_viewerEntry) && _secureUris == null) {
final directory = _viewerEntry?.directory;
@ -240,7 +241,7 @@ class _HomePageState extends State<HomePage> {
await reportService.log('Initialize source to view item in directory $directory');
final source = context.read<CollectionSource>();
source.canAnalyze = false;
await source.init(albumFilter: AlbumFilter(directory, null));
await source.init(scope: {AlbumFilter(directory, null)});
}
} else {
await _initViewerEssentials();
@ -305,19 +306,20 @@ class _HomePageState extends State<HomePage> {
CollectionLens? collection;
final source = context.read<CollectionSource>();
if (source.scope != SourceScope.none) {
final album = viewerEntry.directory;
if (album != null) {
// wait for collection to pass the `loading` state
final completer = Completer();
final stateNotifier = source.stateNotifier;
void _onSourceStateChanged() {
if (source.state != SourceState.loading) {
source.stateNotifier.removeListener(_onSourceStateChanged);
if (stateNotifier.value != SourceState.loading) {
stateNotifier.removeListener(_onSourceStateChanged);
completer.complete();
}
}
source.stateNotifier.addListener(_onSourceStateChanged);
stateNotifier.addListener(_onSourceStateChanged);
_onSourceStateChanged();
await completer.future;
collection = CollectionLens(
@ -338,7 +340,6 @@ class _HomePageState extends State<HomePage> {
collection = null;
}
}
}
return DirectMaterialPageRoute(
settings: const RouteSettings(name: EntryViewerPage.routeName),

View file

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

View file

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