source init scope review
This commit is contained in:
parent
687ca5eb41
commit
33ffb1cd1a
10 changed files with 83 additions and 73 deletions
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
@ -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) {
|
||||
await _loadEssentials();
|
||||
}
|
||||
if (_scope != SourceScope.full) {
|
||||
_scope = albumFilter != null ? SourceScope.album : SourceScope.full;
|
||||
}
|
||||
_targetScope = scope;
|
||||
await reportService.log('$runtimeType init target scope=$scope');
|
||||
await _loadEssentials();
|
||||
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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -685,11 +685,9 @@ 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();
|
||||
}
|
||||
await _mediaStoreSource.loadCatalogMetadata();
|
||||
await _mediaStoreSource.loadAddresses();
|
||||
_mediaStoreSource.updateDerivedFilters();
|
||||
}
|
||||
|
||||
void _onError(String? error) => reportService.recordError(error, null);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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>(
|
||||
|
|
|
@ -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,38 +306,38 @@ 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();
|
||||
void _onSourceStateChanged() {
|
||||
if (source.state != SourceState.loading) {
|
||||
source.stateNotifier.removeListener(_onSourceStateChanged);
|
||||
completer.complete();
|
||||
}
|
||||
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 (stateNotifier.value != SourceState.loading) {
|
||||
stateNotifier.removeListener(_onSourceStateChanged);
|
||||
completer.complete();
|
||||
}
|
||||
}
|
||||
|
||||
source.stateNotifier.addListener(_onSourceStateChanged);
|
||||
await completer.future;
|
||||
stateNotifier.addListener(_onSourceStateChanged);
|
||||
_onSourceStateChanged();
|
||||
await completer.future;
|
||||
|
||||
collection = CollectionLens(
|
||||
source: source,
|
||||
filters: {AlbumFilter(album, source.getAlbumDisplayName(context, album))},
|
||||
listenToSource: false,
|
||||
// if we group bursts, opening a burst sub-entry should:
|
||||
// - identify and select the containing main entry,
|
||||
// - select the sub-entry in the Viewer page.
|
||||
stackBursts: false,
|
||||
);
|
||||
final viewerEntryPath = viewerEntry.path;
|
||||
final collectionEntry = collection.sortedEntries.firstWhereOrNull((entry) => entry.path == viewerEntryPath);
|
||||
if (collectionEntry != null) {
|
||||
viewerEntry = collectionEntry;
|
||||
} else {
|
||||
debugPrint('collection does not contain viewerEntry=$viewerEntry');
|
||||
collection = null;
|
||||
}
|
||||
collection = CollectionLens(
|
||||
source: source,
|
||||
filters: {AlbumFilter(album, source.getAlbumDisplayName(context, album))},
|
||||
listenToSource: false,
|
||||
// if we group bursts, opening a burst sub-entry should:
|
||||
// - identify and select the containing main entry,
|
||||
// - select the sub-entry in the Viewer page.
|
||||
stackBursts: false,
|
||||
);
|
||||
final viewerEntryPath = viewerEntry.path;
|
||||
final collectionEntry = collection.sortedEntries.firstWhereOrNull((entry) => entry.path == viewerEntryPath);
|
||||
if (collectionEntry != null) {
|
||||
viewerEntry = collectionEntry;
|
||||
} else {
|
||||
debugPrint('collection does not contain viewerEntry=$viewerEntry');
|
||||
collection = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
await source.removeEntries({targetEntry.uri}, includeTrash: true);
|
||||
EntryDeletedNotification({targetEntry}).dispatch(context);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
Loading…
Reference in a new issue