various collection model fixes

This commit is contained in:
Thibault Deckers 2021-02-06 10:25:16 +09:00
parent 319fd9584b
commit c5ee55adb0
22 changed files with 211 additions and 98 deletions

View file

@ -67,7 +67,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
val packages = HashMap<String, FieldMap>()
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) }

View file

@ -42,6 +42,8 @@ class LocationFilter extends CollectionFilter {
String get countryNameAndCode => '$_location$locationSeparator$_countryCode';
String get countryCode => _countryCode;
@override
EntryFilter get filter => _filter;

View file

@ -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> color(BuildContext context) => colorful ? super.color(context) : SynchronousFuture(Colors.white);
Future<Color> color(BuildContext context) => colorful ? super.color(context) : SynchronousFuture(AvesFilterChip.defaultOutlineColor);
@override
String get typeKey => type;

View file

@ -116,11 +116,11 @@ class MetadataDb {
debugPrint('$runtimeType clearEntries deleted $count entries');
}
Future<List<AvesEntry>> loadEntries() async {
Future<Set<AvesEntry>> 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;
}

View file

@ -7,9 +7,9 @@ import 'package:collection/collection.dart';
import 'package:path/path.dart';
mixin AlbumMixin on SourceBase {
final Set<String> _folderPaths = {};
final Set<String> _directories = {};
List<String> sortedAlbums = List.unmodifiable([]);
List<String> 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<String, AvesEntry> getAlbumEntries() {
final entries = sortedEntriesForFilterList;
final entries = sortedEntriesByDate;
final regularAlbums = <String>[], appAlbums = <String>[], specialAlbums = <String>[];
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<String> albums) => _folderPaths.addAll(albums);
void addDirectory(Iterable<String> albums) {
_directories.addAll(albums);
_notifyAlbumChange();
}
void cleanEmptyAlbums([Set<String> 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<String, int> _filterEntryCountMap = {};
final Map<String, AvesEntry> _filterRecentEntryMap = {};
void invalidateAlbumFilterSummary({Set<AvesEntry> entries, Set<String> 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 {}

View file

@ -42,7 +42,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
sortFactor = sortFactor ?? settings.collectionSortFactor {
id ??= hashCode;
if (listenToSource) {
_subscriptions.add(source.eventBus.on<EntryAddedEvent>().listen((e) => _refresh()));
_subscriptions.add(source.eventBus.on<EntryAddedEvent>().listen((e) => onEntryAdded(e.entries)));
_subscriptions.add(source.eventBus.on<EntryRemovedEvent>().listen((e) => onEntryRemoved(e.entries)));
_subscriptions.add(source.eventBus.on<EntryMovedEvent>().listen((e) => _refresh()));
_subscriptions.add(source.eventBus.on<CatalogMetadataChangedEvent>().listen((e) => _refresh()));
@ -167,7 +167,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
break;
case EntrySortFactor.name:
final byAlbum = groupBy<AvesEntry, EntryAlbumSectionKey>(_filteredEntries, (entry) => EntryAlbumSectionKey(entry.directory));
sections = SplayTreeMap<EntryAlbumSectionKey, List<AvesEntry>>.of(byAlbum, (a, b) => source.compareAlbumsByName(a.folderPath, b.folderPath));
sections = SplayTreeMap<EntryAlbumSectionKey, List<AvesEntry>>.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<AvesEntry> entries) {
void onEntryAdded(Set<AvesEntry> entries) {
_refresh();
}
void onEntryRemoved(Set<AvesEntry> entries) {
// we should remove obsolete entries and sections
// but do not apply sort/group
// as section order change would surprise the user while browsing

View file

@ -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<AvesEntry> _rawEntries = [];
@ -24,11 +24,7 @@ mixin SourceBase {
EventBus get eventBus => _eventBus;
List<AvesEntry> get sortedEntriesForFilterList;
final Map<CollectionFilter, int> _filterEntryCountMap = {};
void invalidateFilterEntryCounts() => _filterEntryCountMap.clear();
List<AvesEntry> get sortedEntriesByDate;
final StreamController<ProgressEvent> _progressStreamController = StreamController.broadcast();
@ -38,12 +34,13 @@ mixin SourceBase {
}
abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin {
List<AvesEntry> _sortedEntriesByDate;
@override
List<AvesEntry> get sortedEntriesForFilterList => CollectionLens(
source: this,
groupFactor: EntryGroupFactor.none,
sortFactor: EntrySortFactor.date,
).sortedEntries;
List<AvesEntry> get sortedEntriesByDate {
_sortedEntriesByDate ??= List.of(_rawEntries)..sort(AvesEntry.compareByDate);
return _sortedEntriesByDate;
}
ValueNotifier<SourceState> 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<AvesEntry> entries) {
void addAll(Set<AvesEntry> 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<AvesEntry> 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<void> _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 = <String>{};
final movedEntries = <AvesEntry>[];
final movedEntries = <AvesEntry>{};
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<void> init();
@ -179,18 +171,41 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
Future<void> refresh();
Future<void> refreshMetadata(Set<AvesEntry> entries);
// filter summary
void _invalidateFilterSummaries([Set<AvesEntry> 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<AvesEntry> entries;
const EntryAddedEvent([this.entry]);
const EntryAddedEvent([this.entries]);
}
class EntryRemovedEvent {
final Iterable<AvesEntry> entries;
final Set<AvesEntry> entries;
const EntryRemovedEvent(this.entries);
}

View file

@ -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<String>.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<String, int> _filterEntryCountMap = {};
final Map<String, AvesEntry> _filterRecentEntryMap = {};
void invalidateCountryFilterSummary([Set<AvesEntry> 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 {}

View file

@ -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 = <AvesEntry>[], pendingNewEntries = <AvesEntry>[];
final allNewEntries = <AvesEntry>{}, pendingNewEntries = <AvesEntry>{};
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 = <AvesEntry>[];
final newEntries = <AvesEntry>{};
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();

View file

@ -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 {

View file

@ -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<String, int> _filterEntryCountMap = {};
final Map<String, AvesEntry> _filterRecentEntryMap = {};
void invalidateTagFilterSummary([Set<AvesEntry> 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 {}

View file

@ -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<CollectionPage> {
CollectionLens get collection => widget.collection;
@override
void dispose() {
collection.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MediaQueryDataProvider(

View file

@ -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),
);
}
}

View file

@ -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) {

View file

@ -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<AvesFilterChip> {
// 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

View file

@ -13,7 +13,7 @@ class DebugAppDatabaseSection extends StatefulWidget {
class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with AutomaticKeepAliveClientMixin {
Future<int> _dbFileSizeLoader;
Future<List<AvesEntry>> _dbEntryLoader;
Future<Set<AvesEntry>> _dbEntryLoader;
Future<List<DateMetadata>> _dbDateLoader;
Future<List<CatalogMetadata>> _dbMetadataLoader;
Future<List<AddressDetails>> _dbAddressLoader;
@ -57,7 +57,7 @@ class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with
);
},
),
FutureBuilder<List>(
FutureBuilder<Set<AvesEntry>>(
future: _dbEntryLoader,
builder: (context, snapshot) {
if (snapshot.hasError) return Text(snapshot.error.toString());

View file

@ -142,10 +142,11 @@ class _AppDrawerState extends State<AppDrawer> {
return StreamBuilder(
stream: source.eventBus.on<AlbumsChangedEvent>(),
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<AppDrawer> {
title: 'Albums',
trailing: StreamBuilder(
stream: source.eventBus.on<AlbumsChangedEvent>(),
builder: (context, _) => Text('${source.sortedAlbums.length}'),
builder: (context, _) => Text('${source.rawAlbums.length}'),
),
routeName: AlbumListPage.routeName,
pageBuilder: (_) => AlbumListPage(source: source),

View file

@ -60,8 +60,7 @@ class AlbumListPage extends StatelessWidget {
// common with album selection page to move/copy entries
static Map<ChipSectionKey, List<FilterGridItem<AlbumFilter>>> 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);

View file

@ -160,19 +160,22 @@ class FilterNavigationPage<T extends CollectionFilter> extends StatelessWidget {
return c != 0 ? c : a.key.compareTo(b.key);
}
static Iterable<FilterGridItem<T>> sort<T extends CollectionFilter>(ChipSortFactor sortFactor, CollectionSource source, Iterable<T> filters) {
static int compareFiltersByName(FilterGridItem<CollectionFilter> a, FilterGridItem<CollectionFilter> b) {
return a.filter.compareTo(b.filter);
}
static Iterable<FilterGridItem<T>> sort<T extends CollectionFilter>(ChipSortFactor sortFactor, CollectionSource source, Set<T> filters) {
Iterable<FilterGridItem<T>> toGridItem(CollectionSource source, Iterable<T> filters) {
final entriesByDate = source.sortedEntriesForFilterList;
return filters.map((filter) => FilterGridItem(
filter,
entriesByDate.firstWhere(filter.filter, orElse: () => null),
source.recentEntry(filter),
));
}
Iterable<FilterGridItem<T>> 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<T extends CollectionFilter> 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;
}

View file

@ -50,8 +50,7 @@ class CountryListPage extends StatelessWidget {
}
Map<ChipSectionKey, List<FilterGridItem<LocationFilter>>> _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);

View file

@ -50,8 +50,7 @@ class TagListPage extends StatelessWidget {
}
Map<ChipSectionKey, List<FilterGridItem<TagFilter>>> _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);

View file

@ -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<AlbumsChangedEvent>(),
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<LocationsChangedEvent>(),
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<CollectionFilter> filters,
@required List<CollectionFilter> filters,
HeroType Function(CollectionFilter filter) heroTypeBuilder,
}) {
return ExpandableFilterRow(