#170 view: load dir entries only, prevent navigation from filters, do not group bursts, do not listen to source

This commit is contained in:
Thibault Deckers 2022-02-21 21:37:12 +09:00
parent 6b4d9c0bc3
commit b1bf026ffd
27 changed files with 273 additions and 162 deletions

View file

@ -20,11 +20,13 @@ class MediaStoreStreamHandler(private val context: Context, arguments: Any?) : E
private lateinit var handler: Handler
private var knownEntries: Map<Int?, Int?>? = null
private var directory: String? = null
init {
if (arguments is Map<*, *>) {
@Suppress("unchecked_cast")
knownEntries = arguments["knownEntries"] as Map<Int?, Int?>?
directory = arguments["directory"] as String?
}
}
@ -58,7 +60,7 @@ class MediaStoreStreamHandler(private val context: Context, arguments: Any?) : E
}
private fun fetchAll() {
MediaStoreImageProvider().fetchAll(context, knownEntries ?: emptyMap()) { success(it) }
MediaStoreImageProvider().fetchAll(context, knownEntries ?: emptyMap(), directory) { success(it) }
endOfStream()
}

View file

@ -37,13 +37,24 @@ import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
class MediaStoreImageProvider : ImageProvider() {
fun fetchAll(context: Context, knownEntries: Map<Int?, Int?>, handleNewEntry: NewEntryHandler) {
fun fetchAll(
context: Context,
knownEntries: Map<Int?, Int?>,
directory: String?,
handleNewEntry: NewEntryHandler,
) {
val isModified = fun(contentId: Int, dateModifiedSecs: Int): Boolean {
val knownDate = knownEntries[contentId]
return knownDate == null || knownDate < dateModifiedSecs
}
fetchFrom(context, isModified, handleNewEntry, IMAGE_CONTENT_URI, IMAGE_PROJECTION)
fetchFrom(context, isModified, handleNewEntry, VIDEO_CONTENT_URI, VIDEO_PROJECTION)
var selection: String? = null
var selectionArgs: Array<String>? = null
if (directory != null) {
selection = "${MediaColumns.PATH} LIKE ?"
selectionArgs = arrayOf("${StorageUtils.ensureTrailingSeparator(directory)}%")
}
fetchFrom(context, isModified, handleNewEntry, IMAGE_CONTENT_URI, IMAGE_PROJECTION, selection = selection, selectionArgs = selectionArgs)
fetchFrom(context, isModified, handleNewEntry, VIDEO_CONTENT_URI, VIDEO_PROJECTION, selection = selection, selectionArgs = selectionArgs)
}
// the provided URI can point to the wrong media collection,
@ -138,12 +149,14 @@ class MediaStoreImageProvider : ImageProvider() {
handleNewEntry: NewEntryHandler,
contentUri: Uri,
projection: Array<String>,
selection: String? = null,
selectionArgs: Array<String>? = null,
fileMimeType: String? = null,
): Boolean {
var found = false
val orderBy = "${MediaStore.MediaColumns.DATE_MODIFIED} DESC"
try {
val cursor = context.contentResolver.query(contentUri, projection, null, null, orderBy)
val cursor = context.contentResolver.query(contentUri, projection, selection, selectionArgs, orderBy)
if (cursor != null) {
val contentUriContainsId = when (contentUri) {
IMAGE_CONTENT_URI, VIDEO_CONTENT_URI -> false

View file

@ -16,13 +16,15 @@ abstract class MetadataDb {
Future<void> reset();
Future<void> removeIds(Set<int> ids, {Set<EntryDataType>? dataTypes});
Future<void> removeIds(Iterable<int> ids, {Set<EntryDataType>? dataTypes});
// entries
Future<void> clearEntries();
Future<Set<AvesEntry>> loadAllEntries();
Future<Set<AvesEntry>> loadEntries({String? directory});
Future<Set<AvesEntry>> loadEntriesById(Iterable<int> ids);
Future<void> saveEntries(Iterable<AvesEntry> entries);
@ -30,8 +32,6 @@ abstract class MetadataDb {
Future<Set<AvesEntry>> searchEntries(String query, {int? limit});
Future<Set<AvesEntry>> loadEntries(List<int> ids);
// date taken
Future<void> clearDates();
@ -40,19 +40,23 @@ abstract class MetadataDb {
// catalog metadata
Future<void> clearMetadataEntries();
Future<void> clearCatalogMetadata();
Future<List<CatalogMetadata>> loadAllMetadataEntries();
Future<Set<CatalogMetadata>> loadCatalogMetadata();
Future<void> saveMetadata(Set<CatalogMetadata> metadataEntries);
Future<Set<CatalogMetadata>> loadCatalogMetadataById(Iterable<int> ids);
Future<void> updateMetadata(int id, CatalogMetadata? metadata);
Future<void> saveCatalogMetadata(Set<CatalogMetadata> metadataEntries);
Future<void> updateCatalogMetadata(int id, CatalogMetadata? metadata);
// address
Future<void> clearAddresses();
Future<Set<AddressDetails>> loadAllAddresses();
Future<Set<AddressDetails>> loadAddresses();
Future<Set<AddressDetails>> loadAddressesById(Iterable<int> ids);
Future<void> saveAddresses(Set<AddressDetails> addresses);
@ -100,5 +104,5 @@ abstract class MetadataDb {
Future<void> addVideoPlayback(Set<VideoPlaybackRow> rows);
Future<void> removeVideoPlayback(Set<int> ids);
Future<void> removeVideoPlayback(Iterable<int> ids);
}

View file

@ -119,7 +119,7 @@ class SqfliteMetadataDb implements MetadataDb {
}
@override
Future<void> removeIds(Set<int> ids, {Set<EntryDataType>? dataTypes}) async {
Future<void> removeIds(Iterable<int> ids, {Set<EntryDataType>? dataTypes}) async {
if (ids.isEmpty) return;
final _dataTypes = dataTypes ?? EntryDataType.values.toSet();
@ -159,27 +159,19 @@ class SqfliteMetadataDb implements MetadataDb {
}
@override
Future<Set<AvesEntry>> loadAllEntries() async {
final rows = await _db.query(entryTable);
Future<Set<AvesEntry>> loadEntries({String? directory}) async {
String? where;
List<Object?>? whereArgs;
if (directory != null) {
where = 'path LIKE ?';
whereArgs = ['$directory%'];
}
final rows = await _db.query(entryTable, where: where, whereArgs: whereArgs);
return rows.map(AvesEntry.fromMap).toSet();
}
@override
Future<Set<AvesEntry>> loadEntries(List<int> ids) async {
if (ids.isEmpty) return {};
final entries = <AvesEntry>{};
await Future.forEach(ids, (id) async {
final rows = await _db.query(
entryTable,
where: 'id = ?',
whereArgs: [id],
);
if (rows.isNotEmpty) {
entries.add(AvesEntry.fromMap(rows.first));
}
});
return entries;
}
Future<Set<AvesEntry>> loadEntriesById(Iterable<int> ids) => _getByIds(ids, entryTable, AvesEntry.fromMap);
@override
Future<void> saveEntries(Iterable<AvesEntry> entries) async {
@ -236,19 +228,22 @@ class SqfliteMetadataDb implements MetadataDb {
// catalog metadata
@override
Future<void> clearMetadataEntries() async {
Future<void> clearCatalogMetadata() async {
final count = await _db.delete(metadataTable, where: '1');
debugPrint('$runtimeType clearMetadataEntries deleted $count rows');
}
@override
Future<List<CatalogMetadata>> loadAllMetadataEntries() async {
Future<Set<CatalogMetadata>> loadCatalogMetadata() async {
final rows = await _db.query(metadataTable);
return rows.map(CatalogMetadata.fromMap).toList();
return rows.map(CatalogMetadata.fromMap).toSet();
}
@override
Future<void> saveMetadata(Set<CatalogMetadata> metadataEntries) async {
Future<Set<CatalogMetadata>> loadCatalogMetadataById(Iterable<int> ids) => _getByIds(ids, metadataTable, CatalogMetadata.fromMap);
@override
Future<void> saveCatalogMetadata(Set<CatalogMetadata> metadataEntries) async {
if (metadataEntries.isEmpty) return;
final stopwatch = Stopwatch()..start();
try {
@ -262,7 +257,7 @@ class SqfliteMetadataDb implements MetadataDb {
}
@override
Future<void> updateMetadata(int id, CatalogMetadata? metadata) async {
Future<void> updateCatalogMetadata(int id, CatalogMetadata? metadata) async {
final batch = _db.batch();
batch.delete(dateTakenTable, where: 'id = ?', whereArgs: [id]);
batch.delete(metadataTable, where: 'id = ?', whereArgs: [id]);
@ -298,11 +293,14 @@ class SqfliteMetadataDb implements MetadataDb {
}
@override
Future<Set<AddressDetails>> loadAllAddresses() async {
Future<Set<AddressDetails>> loadAddresses() async {
final rows = await _db.query(addressTable);
return rows.map(AddressDetails.fromMap).toSet();
}
@override
Future<Set<AddressDetails>> loadAddressesById(Iterable<int> ids) => _getByIds(ids, addressTable, AddressDetails.fromMap);
@override
Future<void> saveAddresses(Set<AddressDetails> addresses) async {
if (addresses.isEmpty) return;
@ -502,7 +500,7 @@ class SqfliteMetadataDb implements MetadataDb {
}
@override
Future<void> removeVideoPlayback(Set<int> ids) async {
Future<void> removeVideoPlayback(Iterable<int> ids) async {
if (ids.isEmpty) return;
// using array in `whereArgs` and using it with `where filter IN ?` is a pain, so we prefer `batch` instead
@ -510,4 +508,15 @@ class SqfliteMetadataDb implements MetadataDb {
ids.forEach((id) => batch.delete(videoPlaybackTable, where: 'id = ?', whereArgs: [id]));
await batch.commit(noResult: true);
}
// convenience methods
Future<Set<T>> _getByIds<T>(Iterable<int> ids, String table, T Function(Map<String, Object?> row) mapRow) async {
if (ids.isEmpty) return {};
final rows = await _db.query(
table,
where: 'id IN (${ids.join(',')})',
);
return rows.map(mapRow).toSet();
}
}

View file

@ -643,7 +643,7 @@ class AvesEntry {
if (persist) {
await metadataDb.saveEntries({this});
if (catalogMetadata != null) await metadataDb.saveMetadata({catalogMetadata!});
if (catalogMetadata != null) await metadataDb.saveCatalogMetadata({catalogMetadata!});
}
await _onVisualFieldChanged(oldMimeType, oldDateModifiedSecs, oldRotationDegrees, oldIsFlipped);

View file

@ -259,7 +259,7 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
);
}
// convenience
// convenience methods
// This method checks whether the item already has a metadata date,
// and adds a date (the file modified date) via Exif if possible.

View file

@ -32,7 +32,7 @@ class CollectionLens with ChangeNotifier {
final AChangeNotifier filterChangeNotifier = AChangeNotifier(), sortSectionChangeNotifier = AChangeNotifier();
final List<StreamSubscription> _subscriptions = [];
int? id;
bool listenToSource;
bool listenToSource, groupBursts;
List<AvesEntry>? fixedSelection;
List<AvesEntry> _filteredSortedEntries = [];
@ -44,6 +44,7 @@ class CollectionLens with ChangeNotifier {
Set<CollectionFilter?>? filters,
this.id,
this.listenToSource = true,
this.groupBursts = true,
this.fixedSelection,
}) : filters = (filters ?? {}).whereNotNull().toSet(),
sectionFactor = settings.collectionSectionFactor,
@ -174,8 +175,6 @@ class CollectionLens with ChangeNotifier {
filterChangeNotifier.notifyListeners();
}
final bool groupBursts = true;
void _applyFilters() {
final entries = fixedSelection ?? (filters.contains(TrashFilter.instance) ? source.trashedEntries : source.visibleEntries);
_filteredSortedEntries = List.of(filters.isEmpty ? entries : entries.where((entry) => filters.every((filter) => filter.test(entry))));

View file

@ -25,6 +25,8 @@ import 'package:collection/collection.dart';
import 'package:event_bus/event_bus.dart';
import 'package:flutter/foundation.dart';
enum SourceInitializationState { none, directory, full }
mixin SourceBase {
EventBus get eventBus;
@ -222,7 +224,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
if (persist) {
final id = entry.id;
await metadataDb.updateEntry(id, entry);
await metadataDb.updateMetadata(id, entry.catalogMetadata);
await metadataDb.updateCatalogMetadata(id, entry.catalogMetadata);
await metadataDb.updateAddress(id, entry.addressDetails);
await metadataDb.updateTrash(id, entry.trashDetails);
}
@ -315,7 +317,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
}
});
await metadataDb.saveEntries(movedEntries);
await metadataDb.saveMetadata(movedEntries.map((entry) => entry.catalogMetadata).whereNotNull().toSet());
await metadataDb.saveCatalogMetadata(movedEntries.map((entry) => entry.catalogMetadata).whereNotNull().toSet());
await metadataDb.saveAddresses(movedEntries.map((entry) => entry.addressDetails).whereNotNull().toSet());
} else {
await Future.forEach<MoveOpEvent>(movedOps, (movedOp) async {
@ -349,11 +351,13 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
eventBus.fire(EntryMovedEvent(moveType, movedEntries));
}
bool get initialized => false;
SourceInitializationState get initState => SourceInitializationState.none;
Future<void> init();
Future<void> refresh({AnalysisController? analysisController});
Future<void> init({
AnalysisController? analysisController,
String? directory,
bool loadTopEntriesFirst = false,
});
Future<Set<String>> refreshUris(Set<String> changedUris, {AnalysisController? analysisController});
@ -363,7 +367,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
// update/delete in DB
final id = entry.id;
if (dataTypes.contains(EntryDataType.catalog)) {
await metadataDb.updateMetadata(id, entry.catalogMetadata);
await metadataDb.updateCatalogMetadata(id, entry.catalogMetadata);
onCatalogMetadataChanged();
}
if (dataTypes.contains(EntryDataType.address)) {

View file

@ -20,8 +20,8 @@ mixin LocationMixin on SourceBase {
List<String> sortedCountries = List.unmodifiable([]);
List<String> sortedPlaces = List.unmodifiable([]);
Future<void> loadAddresses() async {
final saved = await metadataDb.loadAllAddresses();
Future<void> loadAddresses({Set<int>? ids}) async {
final saved = await (ids != null ? metadataDb.loadAddressesById(ids) : metadataDb.loadAddresses());
final idMap = entryById;
saved.forEach((metadata) => idMap[metadata.id]?.addressDetails = metadata);
onAddressMetadataChanged();

View file

@ -4,7 +4,6 @@ import 'dart:math';
import 'package:aves/model/covers.dart';
import 'package:aves/model/entry.dart';
import 'package:aves/model/favourites.dart';
import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/analysis_controller.dart';
import 'package:aves/model/source/collection_source.dart';
@ -15,13 +14,31 @@ import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
class MediaStoreSource extends CollectionSource {
bool _initialized = false;
SourceInitializationState _initState = SourceInitializationState.none;
@override
bool get initialized => _initialized;
SourceInitializationState get initState => _initState;
@override
Future<void> init() async {
Future<void> init({
AnalysisController? analysisController,
String? directory,
bool loadTopEntriesFirst = false,
}) async {
if (_initState == SourceInitializationState.none) {
await _loadEssentials();
}
if (_initState != SourceInitializationState.full) {
_initState = directory != null ? SourceInitializationState.directory : SourceInitializationState.full;
}
unawaited(_loadEntries(
analysisController: analysisController,
directory: directory,
loadTopEntriesFirst: loadTopEntriesFirst,
));
}
Future<void> _loadEssentials() async {
final stopwatch = Stopwatch()..start();
stateNotifier.value = SourceState.loading;
await metadataDb.init();
@ -34,35 +51,36 @@ class MediaStoreSource extends CollectionSource {
// 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');
await metadataDb.clearDates();
await metadataDb.clearMetadataEntries();
await metadataDb.clearCatalogMetadata();
settings.catalogTimeZone = currentTimeZone;
}
}
await loadDates();
_initialized = true;
debugPrint('$runtimeType init complete in ${stopwatch.elapsed.inMilliseconds}ms');
debugPrint('$runtimeType load essentials complete in ${stopwatch.elapsed.inMilliseconds}ms');
}
@override
Future<void> refresh({AnalysisController? analysisController}) async {
assert(_initialized);
Future<void> _loadEntries({
AnalysisController? analysisController,
String? directory,
required bool loadTopEntriesFirst,
}) async {
debugPrint('$runtimeType refresh start');
final stopwatch = Stopwatch()..start();
stateNotifier.value = SourceState.loading;
clearEntries();
final Set<AvesEntry> topEntries = {};
if (settings.homePage == HomePageSetting.collection) {
if (loadTopEntriesFirst) {
final topIds = settings.topEntryIds;
if (topIds != null) {
debugPrint('$runtimeType refresh ${stopwatch.elapsed} load ${topIds.length} top entries');
topEntries.addAll(await metadataDb.loadEntries(topIds));
topEntries.addAll(await metadataDb.loadEntriesById(topIds));
addEntries(topEntries);
}
}
debugPrint('$runtimeType refresh ${stopwatch.elapsed} fetch known entries');
final knownEntries = await metadataDb.loadAllEntries();
final knownEntries = await metadataDb.loadEntries(directory: directory);
final knownLiveEntries = knownEntries.where((entry) => !entry.trashed).toSet();
debugPrint('$runtimeType refresh ${stopwatch.elapsed} check obsolete entries');
@ -79,25 +97,33 @@ class MediaStoreSource extends CollectionSource {
addEntries(knownEntries);
debugPrint('$runtimeType refresh ${stopwatch.elapsed} load metadata');
await loadCatalogMetadata();
await loadAddresses();
updateDerivedFilters();
if (directory != null) {
final ids = knownLiveEntries.map((entry) => entry.id).toSet();
await loadCatalogMetadata(ids: ids);
await loadAddresses(ids: ids);
} else {
await loadCatalogMetadata();
await loadAddresses();
updateDerivedFilters();
}
// clean up obsolete entries
debugPrint('$runtimeType refresh ${stopwatch.elapsed} remove obsolete entries');
await metadataDb.removeIds(obsoleteContentIds);
// trash
await loadTrashDetails();
unawaited(deleteExpiredTrash().then(
(deletedUris) {
if (deletedUris.isNotEmpty) {
debugPrint('evicted ${deletedUris.length} expired items from the trash');
removeEntries(deletedUris, includeTrash: true);
}
},
onError: (error) => debugPrint('failed to evict expired trash error=$error'),
));
if (directory != null) {
// trash
await loadTrashDetails();
unawaited(deleteExpiredTrash().then(
(deletedUris) {
if (deletedUris.isNotEmpty) {
debugPrint('evicted ${deletedUris.length} expired items from the trash');
removeEntries(deletedUris, includeTrash: true);
}
},
onError: (error) => debugPrint('failed to evict expired trash error=$error'),
));
}
// verify paths because some apps move files without updating their `last modified date`
debugPrint('$runtimeType refresh ${stopwatch.elapsed} check obsolete paths');
@ -120,7 +146,7 @@ class MediaStoreSource extends CollectionSource {
pendingNewEntries.clear();
}
mediaStoreService.getEntries(knownDateByContentId).listen(
mediaStoreService.getEntries(knownDateByContentId, directory: directory).listen(
(entry) {
entry.id = metadataDb.nextId;
pendingNewEntries.add(entry);
@ -162,7 +188,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 (!_initialized || !isMonitoring) return changedUris;
if (_initState == SourceInitializationState.none || !isMonitoring) return changedUris;
debugPrint('$runtimeType refreshUris ${changedUris.length} uris');
final uriByContentId = Map.fromEntries(changedUris.map((uri) {

View file

@ -14,8 +14,8 @@ mixin TagMixin on SourceBase {
List<String> sortedTags = List.unmodifiable([]);
Future<void> loadCatalogMetadata() async {
final saved = await metadataDb.loadAllMetadataEntries();
Future<void> loadCatalogMetadata({Set<int>? ids}) async {
final saved = await (ids != null ? metadataDb.loadCatalogMetadataById(ids) : metadataDb.loadCatalogMetadata());
final idMap = entryById;
saved.forEach((metadata) => idMap[metadata.id]?.catalogMetadata = metadata);
onCatalogMetadataChanged();
@ -42,7 +42,7 @@ mixin TagMixin on SourceBase {
if (entry.isCatalogued) {
newMetadata.add(entry.catalogMetadata!);
if (newMetadata.length >= commitCountThreshold) {
await metadataDb.saveMetadata(Set.unmodifiable(newMetadata));
await metadataDb.saveCatalogMetadata(Set.unmodifiable(newMetadata));
onCatalogMetadataChanged();
newMetadata.clear();
}
@ -53,7 +53,7 @@ mixin TagMixin on SourceBase {
}
setProgress(done: ++progressDone, total: progressTotal);
}
await metadataDb.saveMetadata(Set.unmodifiable(newMetadata));
await metadataDb.saveCatalogMetadata(Set.unmodifiable(newMetadata));
onCatalogMetadataChanged();
}

View file

@ -115,8 +115,7 @@ class Analyzer {
settings.systemLocalesFallback = await deviceService.getLocales();
_l10n = await AppLocalizations.delegate.load(settings.appliedLocale);
_serviceStateNotifier.value = AnalyzerState.running;
await _source.init();
unawaited(_source.refresh(analysisController: _controller));
await _source.init(analysisController: _controller);
_notificationUpdateTimer = Timer.periodic(notificationUpdateInterval, (_) async {
if (!isRunning) return;

View file

@ -11,7 +11,7 @@ abstract class MediaStoreService {
Future<List<int>> checkObsoletePaths(Map<int?, String?> knownPathById);
// knownEntries: map of contentId -> dateModifiedSecs
Stream<AvesEntry> getEntries(Map<int?, int?> knownEntries);
Stream<AvesEntry> getEntries(Map<int?, int?> knownEntries, {String? directory});
// returns media URI
Future<Uri?> scanFile(String path, String mimeType);
@ -48,11 +48,12 @@ class PlatformMediaStoreService implements MediaStoreService {
}
@override
Stream<AvesEntry> getEntries(Map<int?, int?> knownEntries) {
Stream<AvesEntry> getEntries(Map<int?, int?> knownEntries, {String? directory}) {
try {
return _streamChannel
.receiveBroadcastStream(<String, dynamic>{
'knownEntries': knownEntries,
'directory': directory,
})
.where((event) => event is Map)
.map((event) => AvesEntry.fromMap(event as Map));

View file

@ -168,7 +168,16 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
debugPrint('$runtimeType lifecycle ${state.name}');
switch (state) {
case AppLifecycleState.inactive:
_saveTopEntries();
switch (appModeNotifier.value) {
case AppMode.main:
case AppMode.pickMediaExternal:
_saveTopEntries();
break;
case AppMode.pickMediaInternal:
case AppMode.pickFilterInternal:
case AppMode.view:
break;
}
break;
case AppLifecycleState.paused:
case AppLifecycleState.detached:

View file

@ -96,35 +96,46 @@ class _CollectionGridContent extends StatelessWidget {
final scrollableWidth = c.item1;
final columnCount = c.item2;
final tileSpacing = c.item3;
// do not listen for animation delay change
final target = context.read<DurationsData>().staggeredAnimationPageTarget;
final tileAnimationDelay = context.read<TileExtentController>().getTileAnimationDelay(target);
return GridTheme(
extent: thumbnailExtent,
child: EntryListDetailsTheme(
extent: thumbnailExtent,
child: SectionedEntryListLayoutProvider(
collection: collection,
scrollableWidth: scrollableWidth,
tileLayout: tileLayout,
columnCount: columnCount,
spacing: tileSpacing,
tileExtent: thumbnailExtent,
tileBuilder: (entry) => AnimatedBuilder(
animation: favourites,
builder: (context, child) {
return InteractiveTile(
key: ValueKey(entry.id),
collection: collection,
entry: entry,
thumbnailExtent: thumbnailExtent,
tileLayout: tileLayout,
isScrollingNotifier: _isScrollingNotifier,
);
},
),
tileAnimationDelay: tileAnimationDelay,
child: child!,
child: ValueListenableBuilder<SourceState>(
valueListenable: collection.source.stateNotifier,
builder: (context, sourceState, child) {
late final Duration tileAnimationDelay;
if (sourceState == SourceState.ready) {
// do not listen for animation delay change
final target = context.read<DurationsData>().staggeredAnimationPageTarget;
tileAnimationDelay = context.read<TileExtentController>().getTileAnimationDelay(target);
} else {
tileAnimationDelay = Duration.zero;
}
return SectionedEntryListLayoutProvider(
collection: collection,
scrollableWidth: scrollableWidth,
tileLayout: tileLayout,
columnCount: columnCount,
spacing: tileSpacing,
tileExtent: thumbnailExtent,
tileBuilder: (entry) => AnimatedBuilder(
animation: favourites,
builder: (context, child) {
return InteractiveTile(
key: ValueKey(entry.id),
collection: collection,
entry: entry,
thumbnailExtent: thumbnailExtent,
tileLayout: tileLayout,
isScrollingNotifier: _isScrollingNotifier,
);
},
),
tileAnimationDelay: tileAnimationDelay,
child: child!,
);
},
child: child,
),
),
);

View file

@ -51,10 +51,9 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
}
final source = context.read<CollectionSource>();
if (!source.initialized) {
if (source.initState != SourceInitializationState.full) {
// source may be uninitialized in viewer mode
await source.init();
unawaited(source.refresh());
}
final entriesByDestination = <String, Set<AvesEntry>>{};

View file

@ -8,6 +8,7 @@ import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/services/analysis_service.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/widgets/common/basic/menu.dart';
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
@ -131,11 +132,16 @@ class _AppDebugPageState extends State<AppDebugPage> {
title: const Text('Show tasks overlay'),
),
ElevatedButton(
onPressed: () async {
await source.init();
await source.refresh();
},
child: const Text('Source full refresh'),
onPressed: () => source.init(loadTopEntriesFirst: false),
child: const Text('Source refresh (top off)'),
),
ElevatedButton(
onPressed: () => source.init(loadTopEntriesFirst: true),
child: const Text('Source refresh (top on)'),
),
ElevatedButton(
onPressed: () => source.init(directory: '${androidFileUtils.dcimPath}/Camera'),
child: const Text('Source refresh (camera)'),
),
ElevatedButton(
onPressed: () => AnalysisService.startService(force: false),

View file

@ -21,7 +21,7 @@ class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with
late Future<int> _dbFileSizeLoader;
late Future<Set<AvesEntry>> _dbEntryLoader;
late Future<Map<int?, int?>> _dbDateLoader;
late Future<List<CatalogMetadata>> _dbMetadataLoader;
late Future<Set<CatalogMetadata>> _dbMetadataLoader;
late Future<Set<AddressDetails>> _dbAddressLoader;
late Future<Set<TrashDetails>> _dbTrashLoader;
late Future<Set<FavouriteRow>> _dbFavouritesLoader;
@ -108,7 +108,7 @@ class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with
);
},
),
FutureBuilder<List>(
FutureBuilder<Set>(
future: _dbMetadataLoader,
builder: (context, snapshot) {
if (snapshot.hasError) return Text(snapshot.error.toString());
@ -122,7 +122,7 @@ class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () => metadataDb.clearMetadataEntries().then((_) => _startDbReport()),
onPressed: () => metadataDb.clearCatalogMetadata().then((_) => _startDbReport()),
child: const Text('Clear'),
),
],
@ -243,10 +243,10 @@ class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with
void _startDbReport() {
_dbFileSizeLoader = metadataDb.dbFileSize();
_dbEntryLoader = metadataDb.loadAllEntries();
_dbEntryLoader = metadataDb.loadEntries();
_dbDateLoader = metadataDb.loadDates();
_dbMetadataLoader = metadataDb.loadAllMetadataEntries();
_dbAddressLoader = metadataDb.loadAllAddresses();
_dbMetadataLoader = metadataDb.loadCatalogMetadata();
_dbAddressLoader = metadataDb.loadAddresses();
_dbTrashLoader = metadataDb.loadAllTrashDetails();
_dbFavouritesLoader = metadataDb.loadAllFavourites();
_dbCoversLoader = metadataDb.loadAllCovers();

View file

@ -4,6 +4,7 @@ import 'package:aves/app_mode.dart';
import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/settings/enums/home_page.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart';
@ -116,14 +117,33 @@ class _HomePageState extends State<HomePage> {
}
context.read<ValueNotifier<AppMode>>().value = appMode;
unawaited(reportService.setCustomKey('app_mode', appMode.toString()));
debugPrint('Storage check complete in ${stopwatch.elapsed.inMilliseconds}ms');
if (appMode != AppMode.view || _isViewerSourceable(_viewerEntry!)) {
debugPrint('Storage check complete in ${stopwatch.elapsed.inMilliseconds}ms');
unawaited(GlobalSearch.registerCallback());
unawaited(AnalysisService.registerCallback());
final source = context.read<CollectionSource>();
await source.init();
unawaited(source.refresh());
switch (appMode) {
case AppMode.main:
case AppMode.pickMediaExternal:
unawaited(GlobalSearch.registerCallback());
unawaited(AnalysisService.registerCallback());
final source = context.read<CollectionSource>();
await source.init(
loadTopEntriesFirst: settings.homePage == HomePageSetting.collection,
);
break;
case AppMode.view:
if (_isViewerSourceable(_viewerEntry)) {
final directory = _viewerEntry?.directory;
if (directory != null) {
unawaited(AnalysisService.registerCallback());
final source = context.read<CollectionSource>();
await source.init(
directory: directory,
);
}
}
break;
case AppMode.pickMediaInternal:
case AppMode.pickFilterInternal:
break;
}
// `pushReplacement` is not enough in some edge cases
@ -135,7 +155,9 @@ class _HomePageState extends State<HomePage> {
));
}
bool _isViewerSourceable(AvesEntry viewerEntry) => viewerEntry.directory != null && !settings.hiddenFilters.any((filter) => filter.test(viewerEntry));
bool _isViewerSourceable(AvesEntry? viewerEntry) {
return viewerEntry != null && viewerEntry.directory != null && !settings.hiddenFilters.any((filter) => filter.test(viewerEntry));
}
Future<AvesEntry?> _initViewerEntry({required String uri, required String? mimeType}) async {
if (uri.startsWith('/')) {
@ -156,7 +178,7 @@ class _HomePageState extends State<HomePage> {
CollectionLens? collection;
final source = context.read<CollectionSource>();
if (source.initialized) {
if (source.initState != SourceInitializationState.none) {
final album = viewerEntry.directory;
if (album != null) {
// wait for collection to pass the `loading` state
@ -174,6 +196,11 @@ class _HomePageState extends State<HomePage> {
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.
groupBursts: false,
);
final viewerEntryPath = viewerEntry.path;
final collectionEntry = collection.sortedEntries.firstWhereOrNull((entry) => entry.path == viewerEntryPath);

View file

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:aves/app_mode.dart';
import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/coordinate.dart';
import 'package:aves/model/filters/filters.dart';
@ -371,6 +372,9 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
}
void _goToCollection(CollectionFilter filter) {
final isMainMode = context.read<ValueNotifier<AppMode>>().value == AppMode.main;
if (!isMainMode) return;
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(

View file

@ -188,7 +188,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
showFeedback(context, l10n.genericFailureFeedback);
} else {
final source = context.read<CollectionSource>();
if (source.initialized) {
if (source.initState != SourceInitializationState.none) {
await source.removeEntries({entry.uri}, includeTrash: true);
}
EntryRemovedNotification(entry).dispatch(context);
@ -203,9 +203,8 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
if (options == null) return;
final source = context.read<CollectionSource>();
if (!source.initialized) {
if (source.initState != SourceInitializationState.full) {
await source.init();
unawaited(source.refresh());
}
final destinationAlbum = await pickAlbum(context: context, moveType: MoveType.export);
if (destinationAlbum == null) return;

View file

@ -39,9 +39,9 @@ class _DbTabState extends State<DbTab> {
void _loadDatabase() {
final id = entry.id;
_dbDateLoader = metadataDb.loadDates().then((values) => values[id]);
_dbEntryLoader = metadataDb.loadAllEntries().then((values) => values.firstWhereOrNull((row) => row.id == id));
_dbMetadataLoader = metadataDb.loadAllMetadataEntries().then((values) => values.firstWhereOrNull((row) => row.id == id));
_dbAddressLoader = metadataDb.loadAllAddresses().then((values) => values.firstWhereOrNull((row) => row.id == id));
_dbEntryLoader = metadataDb.loadEntries().then((values) => values.firstWhereOrNull((row) => row.id == id));
_dbMetadataLoader = metadataDb.loadCatalogMetadata().then((values) => values.firstWhereOrNull((row) => row.id == id));
_dbAddressLoader = metadataDb.loadAddresses().then((values) => values.firstWhereOrNull((row) => row.id == id));
_dbTrashDetailsLoader = metadataDb.loadAllTrashDetails().then((values) => values.firstWhereOrNull((row) => row.id == id));
_dbVideoPlaybackLoader = metadataDb.loadVideoPlayback(id);
setState(() {});

View file

@ -1,5 +1,6 @@
import 'dart:math';
import 'package:aves/app_mode.dart';
import 'package:aves/model/device.dart';
import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/filters.dart';
@ -399,8 +400,12 @@ class _EntryViewerStackState extends State<EntryViewerStack> with FeedbackMixin,
}
void _goToCollection(CollectionFilter filter) {
final isMainMode = context.read<ValueNotifier<AppMode>>().value == AppMode.main;
if (!isMainMode) return;
final baseCollection = collection;
if (baseCollection == null) return;
_onLeave();
Navigator.pushAndRemoveUntil(
context,

View file

@ -1,6 +1,5 @@
import 'dart:async';
import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/entry_info_actions.dart';
import 'package:aves/model/actions/events.dart';
import 'package:aves/model/entry.dart';
@ -199,7 +198,7 @@ class _InfoPageContentState extends State<_InfoPageContent> {
collection: collection,
actionDelegate: _actionDelegate,
isEditingMetadataNotifier: _isEditingMetadataNotifier,
onFilter: _goToCollection,
onFilter: _onFilter,
);
final locationAtTop = widget.split && entry.hasGps;
final locationSection = LocationSection(
@ -207,7 +206,7 @@ class _InfoPageContentState extends State<_InfoPageContent> {
entry: entry,
showTitle: !locationAtTop,
isScrollingNotifier: widget.isScrollingNotifier,
onFilter: _goToCollection,
onFilter: _onFilter,
);
final basicAndLocationSliver = locationAtTop
? SliverToBoxAdapter(
@ -265,9 +264,5 @@ class _InfoPageContentState extends State<_InfoPageContent> {
});
}
void _goToCollection(CollectionFilter filter) {
final isMainMode = context.read<ValueNotifier<AppMode>>().value == AppMode.main;
if (!isMainMode || collection == null) return;
FilterSelectedNotification(filter).dispatch(context);
}
void _onFilter(CollectionFilter filter) => FilterSelectedNotification(filter).dispatch(context);
}

View file

@ -15,7 +15,7 @@ class FakeMediaStoreService extends Fake implements MediaStoreService {
Future<List<int>> checkObsoletePaths(Map<int?, String?> knownPathById) => SynchronousFuture([]);
@override
Stream<AvesEntry> getEntries(Map<int?, int?> knownEntries) => Stream.fromIterable(entries);
Stream<AvesEntry> getEntries(Map<int?, int?> knownEntries, {String? directory}) => Stream.fromIterable(entries);
static var _lastId = 1;

View file

@ -19,12 +19,12 @@ class FakeMetadataDb extends Fake implements MetadataDb {
Future<void> init() => SynchronousFuture(null);
@override
Future<void> removeIds(Set<int> ids, {Set<EntryDataType>? dataTypes}) => SynchronousFuture(null);
Future<void> removeIds(Iterable<int> ids, {Set<EntryDataType>? dataTypes}) => SynchronousFuture(null);
// entries
@override
Future<Set<AvesEntry>> loadAllEntries() => SynchronousFuture({});
Future<Set<AvesEntry>> loadEntries({String? directory}) => SynchronousFuture({});
@override
Future<void> saveEntries(Iterable<AvesEntry> entries) => SynchronousFuture(null);
@ -40,18 +40,18 @@ class FakeMetadataDb extends Fake implements MetadataDb {
// catalog metadata
@override
Future<List<CatalogMetadata>> loadAllMetadataEntries() => SynchronousFuture([]);
Future<Set<CatalogMetadata>> loadCatalogMetadata() => SynchronousFuture({});
@override
Future<void> saveMetadata(Set<CatalogMetadata> metadataEntries) => SynchronousFuture(null);
Future<void> saveCatalogMetadata(Set<CatalogMetadata> metadataEntries) => SynchronousFuture(null);
@override
Future<void> updateMetadata(int id, CatalogMetadata? metadata) => SynchronousFuture(null);
Future<void> updateCatalogMetadata(int id, CatalogMetadata? metadata) => SynchronousFuture(null);
// address
@override
Future<Set<AddressDetails>> loadAllAddresses() => SynchronousFuture({});
Future<Set<AddressDetails>> loadAddresses() => SynchronousFuture({});
@override
Future<void> saveAddresses(Set<AddressDetails> addresses) => SynchronousFuture(null);
@ -101,5 +101,5 @@ class FakeMetadataDb extends Fake implements MetadataDb {
// video playback
@override
Future<void> removeVideoPlayback(Set<int> ids) => SynchronousFuture(null);
Future<void> removeVideoPlayback(Iterable<int> ids) => SynchronousFuture(null);
}

View file

@ -83,7 +83,6 @@ void main() {
}
});
await source.init();
await source.refresh();
await readyCompleter.future;
return source;
}