diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt index c4b8e3377..a813d1943 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt @@ -67,7 +67,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { val packages = HashMap() fun addPackageDetails(intent: Intent) { - // apps tend to use their name in English when creating folders + // apps tend to use their name in English when creating directories // so we get their names in English as well as the current locale val englishConfig = Configuration().apply { setLocale(Locale.ENGLISH) } diff --git a/lib/model/filters/location.dart b/lib/model/filters/location.dart index c739f62be..79e515ce1 100644 --- a/lib/model/filters/location.dart +++ b/lib/model/filters/location.dart @@ -42,6 +42,8 @@ class LocationFilter extends CollectionFilter { String get countryNameAndCode => '$_location$locationSeparator$_countryCode'; + String get countryCode => _countryCode; + @override EntryFilter get filter => _filter; diff --git a/lib/model/filters/query.dart b/lib/model/filters/query.dart index ad740e41f..5d84812e0 100644 --- a/lib/model/filters/query.dart +++ b/lib/model/filters/query.dart @@ -1,5 +1,6 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; @@ -55,7 +56,7 @@ class QueryFilter extends CollectionFilter { Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => Icon(AIcons.text, size: size); @override - Future color(BuildContext context) => colorful ? super.color(context) : SynchronousFuture(Colors.white); + Future color(BuildContext context) => colorful ? super.color(context) : SynchronousFuture(AvesFilterChip.defaultOutlineColor); @override String get typeKey => type; diff --git a/lib/model/metadata_db.dart b/lib/model/metadata_db.dart index 3630f5a27..389c640e0 100644 --- a/lib/model/metadata_db.dart +++ b/lib/model/metadata_db.dart @@ -116,11 +116,11 @@ class MetadataDb { debugPrint('$runtimeType clearEntries deleted $count entries'); } - Future> loadEntries() async { + Future> loadEntries() async { final stopwatch = Stopwatch()..start(); final db = await _database; final maps = await db.query(entryTable); - final entries = maps.map((map) => AvesEntry.fromMap(map)).toList(); + final entries = maps.map((map) => AvesEntry.fromMap(map)).toSet(); debugPrint('$runtimeType loadEntries complete in ${stopwatch.elapsed.inMilliseconds}ms for ${entries.length} entries'); return entries; } diff --git a/lib/model/source/album.dart b/lib/model/source/album.dart index 44b2789c1..004517b7b 100644 --- a/lib/model/source/album.dart +++ b/lib/model/source/album.dart @@ -7,9 +7,9 @@ import 'package:collection/collection.dart'; import 'package:path/path.dart'; mixin AlbumMixin on SourceBase { - final Set _folderPaths = {}; + final Set _directories = {}; - List sortedAlbums = List.unmodifiable([]); + List get rawAlbums => List.unmodifiable(_directories); int compareAlbumsByName(String a, String b) { final ua = getUniqueAlbumName(a); @@ -21,15 +21,10 @@ mixin AlbumMixin on SourceBase { return compareAsciiUpperCase(va, vb); } - void updateAlbums() { - final sorted = _folderPaths.toList()..sort(compareAlbumsByName); - sortedAlbums = List.unmodifiable(sorted); - invalidateFilterEntryCounts(); - eventBus.fire(AlbumsChangedEvent()); - } + void _notifyAlbumChange() => eventBus.fire(AlbumsChangedEvent()); String getUniqueAlbumName(String album) { - final otherAlbums = _folderPaths.where((item) => item != album); + final otherAlbums = _directories.where((item) => item != album); final parts = album.split(separator); var partCount = 0; String testName; @@ -51,9 +46,9 @@ mixin AlbumMixin on SourceBase { } Map getAlbumEntries() { - final entries = sortedEntriesForFilterList; + final entries = sortedEntriesByDate; final regularAlbums = [], appAlbums = [], specialAlbums = []; - for (final album in sortedAlbums) { + for (final album in rawAlbums) { switch (androidFileUtils.getAlbumType(album)) { case AlbumType.regular: regularAlbums.add(album); @@ -72,13 +67,17 @@ mixin AlbumMixin on SourceBase { ))); } - void addFolderPath(Iterable albums) => _folderPaths.addAll(albums); + void addDirectory(Iterable albums) { + _directories.addAll(albums); + _notifyAlbumChange(); + } void cleanEmptyAlbums([Set albums]) { - final emptyAlbums = (albums ?? _folderPaths).where(_isEmptyAlbum).toList(); + final emptyAlbums = (albums ?? _directories).where(_isEmptyAlbum).toSet(); if (emptyAlbums.isNotEmpty) { - _folderPaths.removeAll(emptyAlbums); - updateAlbums(); + _directories.removeAll(emptyAlbums); + _notifyAlbumChange(); + invalidateAlbumFilterSummary(directories: emptyAlbums); final pinnedFilters = settings.pinnedFilters; emptyAlbums.forEach((album) => pinnedFilters.remove(AlbumFilter(album, getUniqueAlbumName(album)))); @@ -87,6 +86,31 @@ mixin AlbumMixin on SourceBase { } bool _isEmptyAlbum(String album) => !rawEntries.any((entry) => entry.directory == album); + + // filter summary + + // by directory + final Map _filterEntryCountMap = {}; + final Map _filterRecentEntryMap = {}; + + void invalidateAlbumFilterSummary({Set entries, Set directories}) { + if (entries == null && directories == null) { + _filterEntryCountMap.clear(); + _filterRecentEntryMap.clear(); + } else { + directories ??= entries.map((entry) => entry.directory).toSet(); + directories.forEach(_filterEntryCountMap.remove); + directories.forEach(_filterRecentEntryMap.remove); + } + } + + int albumEntryCount(AlbumFilter filter) { + return _filterEntryCountMap.putIfAbsent(filter.album, () => rawEntries.where((entry) => filter.filter(entry)).length); + } + + AvesEntry albumRecentEntry(AlbumFilter filter) { + return _filterRecentEntryMap.putIfAbsent(filter.album, () => sortedEntriesByDate.firstWhere((entry) => filter.filter(entry))); + } } class AlbumsChangedEvent {} diff --git a/lib/model/source/collection_lens.dart b/lib/model/source/collection_lens.dart index ec7d3702a..455619fa0 100644 --- a/lib/model/source/collection_lens.dart +++ b/lib/model/source/collection_lens.dart @@ -42,7 +42,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel sortFactor = sortFactor ?? settings.collectionSortFactor { id ??= hashCode; if (listenToSource) { - _subscriptions.add(source.eventBus.on().listen((e) => _refresh())); + _subscriptions.add(source.eventBus.on().listen((e) => onEntryAdded(e.entries))); _subscriptions.add(source.eventBus.on().listen((e) => onEntryRemoved(e.entries))); _subscriptions.add(source.eventBus.on().listen((e) => _refresh())); _subscriptions.add(source.eventBus.on().listen((e) => _refresh())); @@ -167,7 +167,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel break; case EntrySortFactor.name: final byAlbum = groupBy(_filteredEntries, (entry) => EntryAlbumSectionKey(entry.directory)); - sections = SplayTreeMap>.of(byAlbum, (a, b) => source.compareAlbumsByName(a.folderPath, b.folderPath)); + sections = SplayTreeMap>.of(byAlbum, (a, b) => source.compareAlbumsByName(a.directory, b.directory)); break; } sections = Map.unmodifiable(sections); @@ -183,7 +183,11 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel _applyGroup(); } - void onEntryRemoved(Iterable entries) { + void onEntryAdded(Set entries) { + _refresh(); + } + + void onEntryRemoved(Set entries) { // we should remove obsolete entries and sections // but do not apply sort/group // as section order change would surprise the user while browsing diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart index ac57ff7e6..6743e0c6e 100644 --- a/lib/model/source/collection_source.dart +++ b/lib/model/source/collection_source.dart @@ -2,19 +2,19 @@ import 'dart:async'; import 'package:aves/model/entry.dart'; import 'package:aves/model/favourite_repo.dart'; +import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/filters/location.dart'; +import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/metadata.dart'; import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/source/album.dart'; -import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/location.dart'; import 'package:aves/model/source/tag.dart'; import 'package:aves/services/image_op_events.dart'; import 'package:event_bus/event_bus.dart'; import 'package:flutter/foundation.dart'; -import 'enums.dart'; - mixin SourceBase { final List _rawEntries = []; @@ -24,11 +24,7 @@ mixin SourceBase { EventBus get eventBus => _eventBus; - List get sortedEntriesForFilterList; - - final Map _filterEntryCountMap = {}; - - void invalidateFilterEntryCounts() => _filterEntryCountMap.clear(); + List get sortedEntriesByDate; final StreamController _progressStreamController = StreamController.broadcast(); @@ -38,12 +34,13 @@ mixin SourceBase { } abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin { + List _sortedEntriesByDate; + @override - List get sortedEntriesForFilterList => CollectionLens( - source: this, - groupFactor: EntryGroupFactor.none, - sortFactor: EntrySortFactor.date, - ).sortedEntries; + List get sortedEntriesByDate { + _sortedEntriesByDate ??= List.of(_rawEntries)..sort(AvesEntry.compareByDate); + return _sortedEntriesByDate; + } ValueNotifier stateNotifier = ValueNotifier(SourceState.ready); @@ -55,7 +52,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM debugPrint('$runtimeType loadDates complete in ${stopwatch.elapsed.inMilliseconds}ms for ${_savedDates.length} entries'); } - void addAll(Iterable entries) { + void addAll(Set entries) { if (entries.isEmpty) return; if (_rawEntries.isNotEmpty) { final newContentIds = entries.map((entry) => entry.contentId).toList(); @@ -66,9 +63,9 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM entry.catalogDateMillis = _savedDates.firstWhere((metadata) => metadata.contentId == contentId, orElse: () => null)?.dateMillis; }); _rawEntries.addAll(entries); - addFolderPath(_rawEntries.map((entry) => entry.directory)); - invalidateFilterEntryCounts(); - eventBus.fire(EntryAddedEvent()); + addDirectory(_rawEntries.map((entry) => entry.directory)); + _invalidateFilterSummaries(entries); + eventBus.fire(EntryAddedEvent(entries)); } void removeEntries(Set entries) { @@ -78,17 +75,16 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM cleanEmptyAlbums(entries.map((entry) => entry.directory).toSet()); updateLocations(); updateTags(); - invalidateFilterEntryCounts(); + _invalidateFilterSummaries(entries); eventBus.fire(EntryRemovedEvent(entries)); } void clearEntries() { _rawEntries.clear(); cleanEmptyAlbums(); - updateAlbums(); updateLocations(); updateTags(); - invalidateFilterEntryCounts(); + _invalidateFilterSummaries(); } Future _moveEntry(AvesEntry entry, Map newFields, bool isFavourite) async { @@ -122,7 +118,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM if (movedOps.isEmpty) return; final fromAlbums = {}; - final movedEntries = []; + final movedEntries = {}; if (copy) { movedOps.forEach((movedOp) { final sourceUri = movedOp.uri; @@ -161,17 +157,13 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM addAll(movedEntries); } else { cleanEmptyAlbums(fromAlbums); - addFolderPath({destinationAlbum}); + addDirectory({destinationAlbum}); } - updateAlbums(); - invalidateFilterEntryCounts(); + invalidateAlbumFilterSummary(directories: fromAlbums); + _invalidateFilterSummaries(movedEntries); eventBus.fire(EntryMovedEvent(movedEntries)); } - int count(CollectionFilter filter) { - return _filterEntryCountMap.putIfAbsent(filter, () => _rawEntries.where((entry) => filter.filter(entry)).length); - } - bool get initialized => false; Future init(); @@ -179,18 +171,41 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM Future refresh(); Future refreshMetadata(Set entries); + + // filter summary + + void _invalidateFilterSummaries([Set entries]) { + _sortedEntriesByDate = null; + invalidateAlbumFilterSummary(entries: entries); + invalidateCountryFilterSummary(entries); + invalidateTagFilterSummary(entries); + } + + int count(CollectionFilter filter) { + if (filter is AlbumFilter) return albumEntryCount(filter); + if (filter is LocationFilter) return countryEntryCount(filter); + if (filter is TagFilter) return tagEntryCount(filter); + return 0; + } + + AvesEntry recentEntry(CollectionFilter filter) { + if (filter is AlbumFilter) return albumRecentEntry(filter); + if (filter is LocationFilter) return countryRecentEntry(filter); + if (filter is TagFilter) return tagRecentEntry(filter); + return null; + } } enum SourceState { loading, cataloguing, locating, ready } class EntryAddedEvent { - final AvesEntry entry; + final Set entries; - const EntryAddedEvent([this.entry]); + const EntryAddedEvent([this.entries]); } class EntryRemovedEvent { - final Iterable entries; + final Set entries; const EntryRemovedEvent(this.entries); } diff --git a/lib/model/source/location.dart b/lib/model/source/location.dart index ef5d36f51..8c81cfca1 100644 --- a/lib/model/source/location.dart +++ b/lib/model/source/location.dart @@ -100,9 +100,33 @@ mixin LocationMixin on SourceBase { final countriesByCode = Map.fromEntries(locations.map((address) => MapEntry(address.countryCode, address.countryName)).where((kv) => kv.key != null && kv.key.isNotEmpty)); sortedCountries = List.unmodifiable(countriesByCode.entries.map((kv) => '${kv.value}${LocationFilter.locationSeparator}${kv.key}').toList()..sort(compareAsciiUpperCase)); - invalidateFilterEntryCounts(); + invalidateCountryFilterSummary(); eventBus.fire(LocationsChangedEvent()); } + + // filter summary + + // by country code + final Map _filterEntryCountMap = {}; + final Map _filterRecentEntryMap = {}; + + void invalidateCountryFilterSummary([Set entries]) { + if (entries == null) { + _filterEntryCountMap.clear(); + _filterRecentEntryMap.clear(); + } else { + final countryCodes = entries.where((entry) => entry.isLocated).map((entry) => entry.addressDetails.countryCode).toSet(); + countryCodes.forEach(_filterEntryCountMap.remove); + } + } + + int countryEntryCount(LocationFilter filter) { + return _filterEntryCountMap.putIfAbsent(filter.countryCode, () => rawEntries.where((entry) => filter.filter(entry)).length); + } + + AvesEntry countryRecentEntry(LocationFilter filter) { + return _filterRecentEntryMap.putIfAbsent(filter.countryCode, () => sortedEntriesByDate.firstWhere((entry) => filter.filter(entry))); + } } class AddressMetadataChangedEvent {} diff --git a/lib/model/source/media_store_source.dart b/lib/model/source/media_store_source.dart index 8c17c80c6..ff8dfe584 100644 --- a/lib/model/source/media_store_source.dart +++ b/lib/model/source/media_store_source.dart @@ -66,7 +66,7 @@ class MediaStoreSource extends CollectionSource { // refresh after the first 10 entries, then after 100 more, then every 1000 entries var refreshCount = 10; const refreshCountMax = 1000; - final allNewEntries = [], pendingNewEntries = []; + final allNewEntries = {}, pendingNewEntries = {}; void addPendingEntries() { allNewEntries.addAll(pendingNewEntries); addAll(pendingNewEntries); @@ -86,10 +86,11 @@ class MediaStoreSource extends CollectionSource { debugPrint('$runtimeType refresh loaded ${allNewEntries.length} new entries, elapsed=${stopwatch.elapsed}'); await metadataDb.saveEntries(allNewEntries); // 700ms for 5500 entries - updateAlbums(); + invalidateAlbumFilterSummary(entries: allNewEntries); + final analytics = FirebaseAnalytics(); unawaited(analytics.setUserProperty(name: 'local_item_count', value: (ceilBy(rawEntries.length, 3)).toString())); - unawaited(analytics.setUserProperty(name: 'album_count', value: (ceilBy(sortedAlbums.length, 1)).toString())); + unawaited(analytics.setUserProperty(name: 'album_count', value: (ceilBy(rawAlbums.length, 1)).toString())); stateNotifier.value = SourceState.cataloguing; await catalogEntries(); @@ -128,7 +129,7 @@ class MediaStoreSource extends CollectionSource { removeEntries(obsoleteEntries); // fetch new entries - final newEntries = []; + final newEntries = {}; for (final kv in uriByContentId.entries) { final contentId = kv.key; final uri = kv.value; @@ -150,7 +151,7 @@ class MediaStoreSource extends CollectionSource { if (newEntries.isNotEmpty) { addAll(newEntries); await metadataDb.saveEntries(newEntries); - updateAlbums(); + invalidateAlbumFilterSummary(entries: newEntries); stateNotifier.value = SourceState.cataloguing; await catalogEntries(); diff --git a/lib/model/source/section_keys.dart b/lib/model/source/section_keys.dart index 072ddcbb3..66988f27d 100644 --- a/lib/model/source/section_keys.dart +++ b/lib/model/source/section_keys.dart @@ -5,21 +5,21 @@ class SectionKey { } class EntryAlbumSectionKey extends SectionKey { - final String folderPath; + final String directory; - const EntryAlbumSectionKey(this.folderPath); + const EntryAlbumSectionKey(this.directory); @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) return false; - return other is EntryAlbumSectionKey && other.folderPath == folderPath; + return other is EntryAlbumSectionKey && other.directory == directory; } @override - int get hashCode => folderPath.hashCode; + int get hashCode => directory.hashCode; @override - String toString() => '$runtimeType#${shortHash(this)}{folderPath=$folderPath}'; + String toString() => '$runtimeType#${shortHash(this)}{directory=$directory}'; } class EntryDateSectionKey extends SectionKey { diff --git a/lib/model/source/tag.dart b/lib/model/source/tag.dart index d5b1ca3c0..9ce9d8cf0 100644 --- a/lib/model/source/tag.dart +++ b/lib/model/source/tag.dart @@ -1,4 +1,5 @@ import 'package:aves/model/entry.dart'; +import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/metadata.dart'; import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/source/collection_source.dart'; @@ -56,9 +57,34 @@ mixin TagMixin on SourceBase { void updateTags() { final tags = rawEntries.expand((entry) => entry.xmpSubjects).toSet().toList()..sort(compareAsciiUpperCase); sortedTags = List.unmodifiable(tags); - invalidateFilterEntryCounts(); + + invalidateTagFilterSummary(); eventBus.fire(TagsChangedEvent()); } + + // filter summary + + // by tag + final Map _filterEntryCountMap = {}; + final Map _filterRecentEntryMap = {}; + + void invalidateTagFilterSummary([Set entries]) { + if (entries == null) { + _filterEntryCountMap.clear(); + _filterRecentEntryMap.clear(); + } else { + final tags = entries.where((entry) => entry.isCatalogued).expand((entry) => entry.xmpSubjects).toSet(); + tags.forEach(_filterEntryCountMap.remove); + } + } + + int tagEntryCount(TagFilter filter) { + return _filterEntryCountMap.putIfAbsent(filter.tag, () => rawEntries.where((entry) => filter.filter(entry)).length); + } + + AvesEntry tagRecentEntry(TagFilter filter) { + return _filterRecentEntryMap.putIfAbsent(filter.tag, () => sortedEntriesByDate.firstWhere((entry) => filter.filter(entry))); + } } class CatalogMetadataChangedEvent {} diff --git a/lib/widgets/collection/collection_page.dart b/lib/widgets/collection/collection_page.dart index 34fc10f68..75e6ee911 100644 --- a/lib/widgets/collection/collection_page.dart +++ b/lib/widgets/collection/collection_page.dart @@ -8,13 +8,26 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -class CollectionPage extends StatelessWidget { +class CollectionPage extends StatefulWidget { static const routeName = '/collection'; final CollectionLens collection; const CollectionPage(this.collection); + @override + _CollectionPageState createState() => _CollectionPageState(); +} + +class _CollectionPageState extends State { + CollectionLens get collection => widget.collection; + + @override + void dispose() { + collection.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return MediaQueryDataProvider( diff --git a/lib/widgets/collection/grid/headers/album.dart b/lib/widgets/collection/grid/headers/album.dart index f6db50107..cf67b3b6d 100644 --- a/lib/widgets/collection/grid/headers/album.dart +++ b/lib/widgets/collection/grid/headers/album.dart @@ -7,18 +7,18 @@ import 'package:aves/widgets/common/identity/aves_icons.dart'; import 'package:flutter/material.dart'; class AlbumSectionHeader extends StatelessWidget { - final String folderPath, albumName; + final String directory, albumName; AlbumSectionHeader({ Key key, @required CollectionSource source, - @required this.folderPath, - }) : albumName = source.getUniqueAlbumName(folderPath), + @required this.directory, + }) : albumName = source.getUniqueAlbumName(directory), super(key: key); @override Widget build(BuildContext context) { - var albumIcon = IconUtils.getAlbumIcon(context: context, album: folderPath); + var albumIcon = IconUtils.getAlbumIcon(context: context, album: directory); if (albumIcon != null) { albumIcon = Material( type: MaterialType.circle, @@ -29,10 +29,10 @@ class AlbumSectionHeader extends StatelessWidget { ); } return SectionHeader( - sectionKey: EntryAlbumSectionKey(folderPath), + sectionKey: EntryAlbumSectionKey(directory), leading: albumIcon, title: albumName, - trailing: androidFileUtils.isOnRemovableStorage(folderPath) + trailing: androidFileUtils.isOnRemovableStorage(directory) ? Icon( AIcons.removableStorage, size: 16, @@ -43,13 +43,13 @@ class AlbumSectionHeader extends StatelessWidget { } static double getPreferredHeight(BuildContext context, double maxWidth, CollectionSource source, EntryAlbumSectionKey sectionKey) { - final folderPath = sectionKey.folderPath; + final directory = sectionKey.directory; return SectionHeader.getPreferredHeight( context: context, maxWidth: maxWidth, - title: source.getUniqueAlbumName(folderPath), - hasLeading: androidFileUtils.getAlbumType(folderPath) != AlbumType.regular, - hasTrailing: androidFileUtils.isOnRemovableStorage(folderPath), + title: source.getUniqueAlbumName(directory), + hasLeading: androidFileUtils.getAlbumType(directory) != AlbumType.regular, + hasTrailing: androidFileUtils.isOnRemovableStorage(directory), ); } } diff --git a/lib/widgets/collection/grid/headers/any.dart b/lib/widgets/collection/grid/headers/any.dart index 9a21b47ce..7ed5bf63c 100644 --- a/lib/widgets/collection/grid/headers/any.dart +++ b/lib/widgets/collection/grid/headers/any.dart @@ -36,7 +36,7 @@ class CollectionSectionHeader extends StatelessWidget { Widget _buildAlbumHeader() => AlbumSectionHeader( key: ValueKey(sectionKey), source: collection.source, - folderPath: (sectionKey as EntryAlbumSectionKey).folderPath, + directory: (sectionKey as EntryAlbumSectionKey).directory, ); switch (collection.sortFactor) { diff --git a/lib/widgets/common/identity/aves_filter_chip.dart b/lib/widgets/common/identity/aves_filter_chip.dart index d096bdcda..db71d1320 100644 --- a/lib/widgets/common/identity/aves_filter_chip.dart +++ b/lib/widgets/common/identity/aves_filter_chip.dart @@ -20,6 +20,7 @@ class AvesFilterChip extends StatefulWidget { final OffsetFilterCallback onLongPress; final BorderRadius borderRadius; + static const Color defaultOutlineColor = Colors.white; static const double defaultRadius = 32; static const double outlineWidth = 2; static const double minChipHeight = kMinInteractiveDimension; @@ -82,7 +83,7 @@ class _AvesFilterChipState extends State { // the existing widget FutureBuilder cycles again from the start, with a frame in `waiting` state and no data. // So we save the result of the Future to a local variable because of this specific case. _colorFuture = filter.color(context); - _outlineColor = Colors.transparent; + _outlineColor = AvesFilterChip.defaultOutlineColor; } @override diff --git a/lib/widgets/debug/database.dart b/lib/widgets/debug/database.dart index 1e261e43f..ea881a82e 100644 --- a/lib/widgets/debug/database.dart +++ b/lib/widgets/debug/database.dart @@ -13,7 +13,7 @@ class DebugAppDatabaseSection extends StatefulWidget { class _DebugAppDatabaseSectionState extends State with AutomaticKeepAliveClientMixin { Future _dbFileSizeLoader; - Future> _dbEntryLoader; + Future> _dbEntryLoader; Future> _dbDateLoader; Future> _dbMetadataLoader; Future> _dbAddressLoader; @@ -57,7 +57,7 @@ class _DebugAppDatabaseSectionState extends State with ); }, ), - FutureBuilder( + FutureBuilder>( future: _dbEntryLoader, builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); diff --git a/lib/widgets/drawer/app_drawer.dart b/lib/widgets/drawer/app_drawer.dart index 1604da167..3871da673 100644 --- a/lib/widgets/drawer/app_drawer.dart +++ b/lib/widgets/drawer/app_drawer.dart @@ -142,10 +142,11 @@ class _AppDrawerState extends State { return StreamBuilder( stream: source.eventBus.on(), builder: (context, snapshot) { - final specialAlbums = source.sortedAlbums.where((album) { + final specialAlbums = source.rawAlbums.where((album) { final type = androidFileUtils.getAlbumType(album); return [AlbumType.camera, AlbumType.screenshots].contains(type); - }); + }).toList() + ..sort(source.compareAlbumsByName); if (specialAlbums.isEmpty) return SizedBox.shrink(); return Column( @@ -185,7 +186,7 @@ class _AppDrawerState extends State { title: 'Albums', trailing: StreamBuilder( stream: source.eventBus.on(), - builder: (context, _) => Text('${source.sortedAlbums.length}'), + builder: (context, _) => Text('${source.rawAlbums.length}'), ), routeName: AlbumListPage.routeName, pageBuilder: (_) => AlbumListPage(source: source), diff --git a/lib/widgets/filter_grids/albums_page.dart b/lib/widgets/filter_grids/albums_page.dart index a50328f3f..90b8750c4 100644 --- a/lib/widgets/filter_grids/albums_page.dart +++ b/lib/widgets/filter_grids/albums_page.dart @@ -60,8 +60,7 @@ class AlbumListPage extends StatelessWidget { // common with album selection page to move/copy entries static Map>> getAlbumEntries(CollectionSource source) { - // albums are initially sorted by name at the source level - final filters = source.sortedAlbums.map((album) => AlbumFilter(album, source.getUniqueAlbumName(album))); + final filters = source.rawAlbums.map((album) => AlbumFilter(album, source.getUniqueAlbumName(album))).toSet(); final sorted = FilterNavigationPage.sort(settings.albumSortFactor, source, filters); return _group(sorted); diff --git a/lib/widgets/filter_grids/common/filter_nav_page.dart b/lib/widgets/filter_grids/common/filter_nav_page.dart index 326ec83af..d67f41a8b 100644 --- a/lib/widgets/filter_grids/common/filter_nav_page.dart +++ b/lib/widgets/filter_grids/common/filter_nav_page.dart @@ -160,19 +160,22 @@ class FilterNavigationPage extends StatelessWidget { return c != 0 ? c : a.key.compareTo(b.key); } - static Iterable> sort(ChipSortFactor sortFactor, CollectionSource source, Iterable filters) { + static int compareFiltersByName(FilterGridItem a, FilterGridItem b) { + return a.filter.compareTo(b.filter); + } + + static Iterable> sort(ChipSortFactor sortFactor, CollectionSource source, Set filters) { Iterable> toGridItem(CollectionSource source, Iterable filters) { - final entriesByDate = source.sortedEntriesForFilterList; return filters.map((filter) => FilterGridItem( filter, - entriesByDate.firstWhere(filter.filter, orElse: () => null), + source.recentEntry(filter), )); } Iterable> allMapEntries; switch (sortFactor) { case ChipSortFactor.name: - allMapEntries = toGridItem(source, filters); + allMapEntries = toGridItem(source, filters).toList()..sort(compareFiltersByName); break; case ChipSortFactor.date: allMapEntries = toGridItem(source, filters).toList()..sort(compareFiltersByDate); @@ -180,7 +183,7 @@ class FilterNavigationPage extends StatelessWidget { case ChipSortFactor.count: final filtersWithCount = List.of(filters.map((filter) => MapEntry(filter, source.count(filter)))); filtersWithCount.sort(compareFiltersByEntryCount); - filters = filtersWithCount.map((kv) => kv.key).toList(); + filters = filtersWithCount.map((kv) => kv.key).toSet(); allMapEntries = toGridItem(source, filters); break; } diff --git a/lib/widgets/filter_grids/countries_page.dart b/lib/widgets/filter_grids/countries_page.dart index 64da80407..f348480c4 100644 --- a/lib/widgets/filter_grids/countries_page.dart +++ b/lib/widgets/filter_grids/countries_page.dart @@ -50,8 +50,7 @@ class CountryListPage extends StatelessWidget { } Map>> _getCountryEntries() { - // countries are initially sorted by name at the source level - final filters = source.sortedCountries.map((location) => LocationFilter(LocationLevel.country, location)); + final filters = source.sortedCountries.map((location) => LocationFilter(LocationLevel.country, location)).toSet(); final sorted = FilterNavigationPage.sort(settings.countrySortFactor, source, filters); return _group(sorted); diff --git a/lib/widgets/filter_grids/tags_page.dart b/lib/widgets/filter_grids/tags_page.dart index 122a2c4b9..5743cec5d 100644 --- a/lib/widgets/filter_grids/tags_page.dart +++ b/lib/widgets/filter_grids/tags_page.dart @@ -50,8 +50,7 @@ class TagListPage extends StatelessWidget { } Map>> _getTagEntries() { - // tags are initially sorted by name at the source level - final filters = source.sortedTags.map((tag) => TagFilter(tag)); + final filters = source.sortedTags.map((tag) => TagFilter(tag)).toSet(); final sorted = FilterNavigationPage.sort(settings.tagSortFactor, source, filters); return _group(sorted); diff --git a/lib/widgets/search/search_delegate.dart b/lib/widgets/search/search_delegate.dart index 446672b87..b3627ec5f 100644 --- a/lib/widgets/search/search_delegate.dart +++ b/lib/widgets/search/search_delegate.dart @@ -86,7 +86,7 @@ class CollectionSearchDelegate { MimeFilter(MimeFilter.sphericalVideo), MimeFilter(MimeFilter.geotiff), MimeFilter(MimeTypes.svg), - ].where((f) => f != null && containQuery(f.label)), + ].where((f) => f != null && containQuery(f.label)).toList(), // usually perform hero animation only on tapped chips, // but we also need to animate the query chip when it is selected by submitting the search query heroTypeBuilder: (filter) => filter == queryFilter ? HeroType.always : HeroType.onTap, @@ -100,7 +100,8 @@ class CollectionSearchDelegate { StreamBuilder( stream: source.eventBus.on(), builder: (context, snapshot) { - final filters = source.sortedAlbums.where(containQuery).map((s) => AlbumFilter(s, source.getUniqueAlbumName(s))).where((f) => containQuery(f.uniqueName)); + // filter twice: full path, and then unique name + final filters = source.rawAlbums.where(containQuery).map((s) => AlbumFilter(s, source.getUniqueAlbumName(s))).where((f) => containQuery(f.uniqueName)).toList()..sort(); return _buildFilterRow( context: context, title: 'Albums', @@ -110,7 +111,7 @@ class CollectionSearchDelegate { StreamBuilder( stream: source.eventBus.on(), builder: (context, snapshot) { - final filters = source.sortedCountries.where(containQuery).map((s) => LocationFilter(LocationLevel.country, s)); + final filters = source.sortedCountries.where(containQuery).map((s) => LocationFilter(LocationLevel.country, s)).toList(); return _buildFilterRow( context: context, title: 'Countries', @@ -154,7 +155,7 @@ class CollectionSearchDelegate { Widget _buildFilterRow({ @required BuildContext context, String title, - @required Iterable filters, + @required List filters, HeroType Function(CollectionFilter filter) heroTypeBuilder, }) { return ExpandableFilterRow(