various collection model fixes
This commit is contained in:
parent
319fd9584b
commit
c5ee55adb0
22 changed files with 211 additions and 98 deletions
|
@ -67,7 +67,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
||||||
val packages = HashMap<String, FieldMap>()
|
val packages = HashMap<String, FieldMap>()
|
||||||
|
|
||||||
fun addPackageDetails(intent: Intent) {
|
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
|
// so we get their names in English as well as the current locale
|
||||||
val englishConfig = Configuration().apply { setLocale(Locale.ENGLISH) }
|
val englishConfig = Configuration().apply { setLocale(Locale.ENGLISH) }
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,8 @@ class LocationFilter extends CollectionFilter {
|
||||||
|
|
||||||
String get countryNameAndCode => '$_location$locationSeparator$_countryCode';
|
String get countryNameAndCode => '$_location$locationSeparator$_countryCode';
|
||||||
|
|
||||||
|
String get countryCode => _countryCode;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
EntryFilter get filter => _filter;
|
EntryFilter get filter => _filter;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/theme/icons.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/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.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);
|
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => Icon(AIcons.text, size: size);
|
||||||
|
|
||||||
@override
|
@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
|
@override
|
||||||
String get typeKey => type;
|
String get typeKey => type;
|
||||||
|
|
|
@ -116,11 +116,11 @@ class MetadataDb {
|
||||||
debugPrint('$runtimeType clearEntries deleted $count entries');
|
debugPrint('$runtimeType clearEntries deleted $count entries');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<AvesEntry>> loadEntries() async {
|
Future<Set<AvesEntry>> loadEntries() async {
|
||||||
final stopwatch = Stopwatch()..start();
|
final stopwatch = Stopwatch()..start();
|
||||||
final db = await _database;
|
final db = await _database;
|
||||||
final maps = await db.query(entryTable);
|
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');
|
debugPrint('$runtimeType loadEntries complete in ${stopwatch.elapsed.inMilliseconds}ms for ${entries.length} entries');
|
||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,9 +7,9 @@ import 'package:collection/collection.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
|
|
||||||
mixin AlbumMixin on SourceBase {
|
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) {
|
int compareAlbumsByName(String a, String b) {
|
||||||
final ua = getUniqueAlbumName(a);
|
final ua = getUniqueAlbumName(a);
|
||||||
|
@ -21,15 +21,10 @@ mixin AlbumMixin on SourceBase {
|
||||||
return compareAsciiUpperCase(va, vb);
|
return compareAsciiUpperCase(va, vb);
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateAlbums() {
|
void _notifyAlbumChange() => eventBus.fire(AlbumsChangedEvent());
|
||||||
final sorted = _folderPaths.toList()..sort(compareAlbumsByName);
|
|
||||||
sortedAlbums = List.unmodifiable(sorted);
|
|
||||||
invalidateFilterEntryCounts();
|
|
||||||
eventBus.fire(AlbumsChangedEvent());
|
|
||||||
}
|
|
||||||
|
|
||||||
String getUniqueAlbumName(String album) {
|
String getUniqueAlbumName(String album) {
|
||||||
final otherAlbums = _folderPaths.where((item) => item != album);
|
final otherAlbums = _directories.where((item) => item != album);
|
||||||
final parts = album.split(separator);
|
final parts = album.split(separator);
|
||||||
var partCount = 0;
|
var partCount = 0;
|
||||||
String testName;
|
String testName;
|
||||||
|
@ -51,9 +46,9 @@ mixin AlbumMixin on SourceBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, AvesEntry> getAlbumEntries() {
|
Map<String, AvesEntry> getAlbumEntries() {
|
||||||
final entries = sortedEntriesForFilterList;
|
final entries = sortedEntriesByDate;
|
||||||
final regularAlbums = <String>[], appAlbums = <String>[], specialAlbums = <String>[];
|
final regularAlbums = <String>[], appAlbums = <String>[], specialAlbums = <String>[];
|
||||||
for (final album in sortedAlbums) {
|
for (final album in rawAlbums) {
|
||||||
switch (androidFileUtils.getAlbumType(album)) {
|
switch (androidFileUtils.getAlbumType(album)) {
|
||||||
case AlbumType.regular:
|
case AlbumType.regular:
|
||||||
regularAlbums.add(album);
|
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]) {
|
void cleanEmptyAlbums([Set<String> albums]) {
|
||||||
final emptyAlbums = (albums ?? _folderPaths).where(_isEmptyAlbum).toList();
|
final emptyAlbums = (albums ?? _directories).where(_isEmptyAlbum).toSet();
|
||||||
if (emptyAlbums.isNotEmpty) {
|
if (emptyAlbums.isNotEmpty) {
|
||||||
_folderPaths.removeAll(emptyAlbums);
|
_directories.removeAll(emptyAlbums);
|
||||||
updateAlbums();
|
_notifyAlbumChange();
|
||||||
|
invalidateAlbumFilterSummary(directories: emptyAlbums);
|
||||||
|
|
||||||
final pinnedFilters = settings.pinnedFilters;
|
final pinnedFilters = settings.pinnedFilters;
|
||||||
emptyAlbums.forEach((album) => pinnedFilters.remove(AlbumFilter(album, getUniqueAlbumName(album))));
|
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);
|
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 {}
|
class AlbumsChangedEvent {}
|
||||||
|
|
|
@ -42,7 +42,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
|
||||||
sortFactor = sortFactor ?? settings.collectionSortFactor {
|
sortFactor = sortFactor ?? settings.collectionSortFactor {
|
||||||
id ??= hashCode;
|
id ??= hashCode;
|
||||||
if (listenToSource) {
|
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<EntryRemovedEvent>().listen((e) => onEntryRemoved(e.entries)));
|
||||||
_subscriptions.add(source.eventBus.on<EntryMovedEvent>().listen((e) => _refresh()));
|
_subscriptions.add(source.eventBus.on<EntryMovedEvent>().listen((e) => _refresh()));
|
||||||
_subscriptions.add(source.eventBus.on<CatalogMetadataChangedEvent>().listen((e) => _refresh()));
|
_subscriptions.add(source.eventBus.on<CatalogMetadataChangedEvent>().listen((e) => _refresh()));
|
||||||
|
@ -167,7 +167,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
|
||||||
break;
|
break;
|
||||||
case EntrySortFactor.name:
|
case EntrySortFactor.name:
|
||||||
final byAlbum = groupBy<AvesEntry, EntryAlbumSectionKey>(_filteredEntries, (entry) => EntryAlbumSectionKey(entry.directory));
|
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;
|
break;
|
||||||
}
|
}
|
||||||
sections = Map.unmodifiable(sections);
|
sections = Map.unmodifiable(sections);
|
||||||
|
@ -183,7 +183,11 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
|
||||||
_applyGroup();
|
_applyGroup();
|
||||||
}
|
}
|
||||||
|
|
||||||
void onEntryRemoved(Iterable<AvesEntry> entries) {
|
void onEntryAdded(Set<AvesEntry> entries) {
|
||||||
|
_refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
void onEntryRemoved(Set<AvesEntry> entries) {
|
||||||
// we should remove obsolete entries and sections
|
// we should remove obsolete entries and sections
|
||||||
// but do not apply sort/group
|
// but do not apply sort/group
|
||||||
// as section order change would surprise the user while browsing
|
// as section order change would surprise the user while browsing
|
||||||
|
|
|
@ -2,19 +2,19 @@ import 'dart:async';
|
||||||
|
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/favourite_repo.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/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.dart';
|
||||||
import 'package:aves/model/metadata_db.dart';
|
import 'package:aves/model/metadata_db.dart';
|
||||||
import 'package:aves/model/source/album.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/location.dart';
|
||||||
import 'package:aves/model/source/tag.dart';
|
import 'package:aves/model/source/tag.dart';
|
||||||
import 'package:aves/services/image_op_events.dart';
|
import 'package:aves/services/image_op_events.dart';
|
||||||
import 'package:event_bus/event_bus.dart';
|
import 'package:event_bus/event_bus.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
import 'enums.dart';
|
|
||||||
|
|
||||||
mixin SourceBase {
|
mixin SourceBase {
|
||||||
final List<AvesEntry> _rawEntries = [];
|
final List<AvesEntry> _rawEntries = [];
|
||||||
|
|
||||||
|
@ -24,11 +24,7 @@ mixin SourceBase {
|
||||||
|
|
||||||
EventBus get eventBus => _eventBus;
|
EventBus get eventBus => _eventBus;
|
||||||
|
|
||||||
List<AvesEntry> get sortedEntriesForFilterList;
|
List<AvesEntry> get sortedEntriesByDate;
|
||||||
|
|
||||||
final Map<CollectionFilter, int> _filterEntryCountMap = {};
|
|
||||||
|
|
||||||
void invalidateFilterEntryCounts() => _filterEntryCountMap.clear();
|
|
||||||
|
|
||||||
final StreamController<ProgressEvent> _progressStreamController = StreamController.broadcast();
|
final StreamController<ProgressEvent> _progressStreamController = StreamController.broadcast();
|
||||||
|
|
||||||
|
@ -38,12 +34,13 @@ mixin SourceBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin {
|
abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin {
|
||||||
|
List<AvesEntry> _sortedEntriesByDate;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<AvesEntry> get sortedEntriesForFilterList => CollectionLens(
|
List<AvesEntry> get sortedEntriesByDate {
|
||||||
source: this,
|
_sortedEntriesByDate ??= List.of(_rawEntries)..sort(AvesEntry.compareByDate);
|
||||||
groupFactor: EntryGroupFactor.none,
|
return _sortedEntriesByDate;
|
||||||
sortFactor: EntrySortFactor.date,
|
}
|
||||||
).sortedEntries;
|
|
||||||
|
|
||||||
ValueNotifier<SourceState> stateNotifier = ValueNotifier(SourceState.ready);
|
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');
|
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 (entries.isEmpty) return;
|
||||||
if (_rawEntries.isNotEmpty) {
|
if (_rawEntries.isNotEmpty) {
|
||||||
final newContentIds = entries.map((entry) => entry.contentId).toList();
|
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;
|
entry.catalogDateMillis = _savedDates.firstWhere((metadata) => metadata.contentId == contentId, orElse: () => null)?.dateMillis;
|
||||||
});
|
});
|
||||||
_rawEntries.addAll(entries);
|
_rawEntries.addAll(entries);
|
||||||
addFolderPath(_rawEntries.map((entry) => entry.directory));
|
addDirectory(_rawEntries.map((entry) => entry.directory));
|
||||||
invalidateFilterEntryCounts();
|
_invalidateFilterSummaries(entries);
|
||||||
eventBus.fire(EntryAddedEvent());
|
eventBus.fire(EntryAddedEvent(entries));
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeEntries(Set<AvesEntry> 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());
|
cleanEmptyAlbums(entries.map((entry) => entry.directory).toSet());
|
||||||
updateLocations();
|
updateLocations();
|
||||||
updateTags();
|
updateTags();
|
||||||
invalidateFilterEntryCounts();
|
_invalidateFilterSummaries(entries);
|
||||||
eventBus.fire(EntryRemovedEvent(entries));
|
eventBus.fire(EntryRemovedEvent(entries));
|
||||||
}
|
}
|
||||||
|
|
||||||
void clearEntries() {
|
void clearEntries() {
|
||||||
_rawEntries.clear();
|
_rawEntries.clear();
|
||||||
cleanEmptyAlbums();
|
cleanEmptyAlbums();
|
||||||
updateAlbums();
|
|
||||||
updateLocations();
|
updateLocations();
|
||||||
updateTags();
|
updateTags();
|
||||||
invalidateFilterEntryCounts();
|
_invalidateFilterSummaries();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _moveEntry(AvesEntry entry, Map newFields, bool isFavourite) async {
|
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;
|
if (movedOps.isEmpty) return;
|
||||||
|
|
||||||
final fromAlbums = <String>{};
|
final fromAlbums = <String>{};
|
||||||
final movedEntries = <AvesEntry>[];
|
final movedEntries = <AvesEntry>{};
|
||||||
if (copy) {
|
if (copy) {
|
||||||
movedOps.forEach((movedOp) {
|
movedOps.forEach((movedOp) {
|
||||||
final sourceUri = movedOp.uri;
|
final sourceUri = movedOp.uri;
|
||||||
|
@ -161,17 +157,13 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
||||||
addAll(movedEntries);
|
addAll(movedEntries);
|
||||||
} else {
|
} else {
|
||||||
cleanEmptyAlbums(fromAlbums);
|
cleanEmptyAlbums(fromAlbums);
|
||||||
addFolderPath({destinationAlbum});
|
addDirectory({destinationAlbum});
|
||||||
}
|
}
|
||||||
updateAlbums();
|
invalidateAlbumFilterSummary(directories: fromAlbums);
|
||||||
invalidateFilterEntryCounts();
|
_invalidateFilterSummaries(movedEntries);
|
||||||
eventBus.fire(EntryMovedEvent(movedEntries));
|
eventBus.fire(EntryMovedEvent(movedEntries));
|
||||||
}
|
}
|
||||||
|
|
||||||
int count(CollectionFilter filter) {
|
|
||||||
return _filterEntryCountMap.putIfAbsent(filter, () => _rawEntries.where((entry) => filter.filter(entry)).length);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get initialized => false;
|
bool get initialized => false;
|
||||||
|
|
||||||
Future<void> init();
|
Future<void> init();
|
||||||
|
@ -179,18 +171,41 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
||||||
Future<void> refresh();
|
Future<void> refresh();
|
||||||
|
|
||||||
Future<void> refreshMetadata(Set<AvesEntry> entries);
|
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 }
|
enum SourceState { loading, cataloguing, locating, ready }
|
||||||
|
|
||||||
class EntryAddedEvent {
|
class EntryAddedEvent {
|
||||||
final AvesEntry entry;
|
final Set<AvesEntry> entries;
|
||||||
|
|
||||||
const EntryAddedEvent([this.entry]);
|
const EntryAddedEvent([this.entries]);
|
||||||
}
|
}
|
||||||
|
|
||||||
class EntryRemovedEvent {
|
class EntryRemovedEvent {
|
||||||
final Iterable<AvesEntry> entries;
|
final Set<AvesEntry> entries;
|
||||||
|
|
||||||
const EntryRemovedEvent(this.entries);
|
const EntryRemovedEvent(this.entries);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
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));
|
sortedCountries = List<String>.unmodifiable(countriesByCode.entries.map((kv) => '${kv.value}${LocationFilter.locationSeparator}${kv.key}').toList()..sort(compareAsciiUpperCase));
|
||||||
|
|
||||||
invalidateFilterEntryCounts();
|
invalidateCountryFilterSummary();
|
||||||
eventBus.fire(LocationsChangedEvent());
|
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 {}
|
class AddressMetadataChangedEvent {}
|
||||||
|
|
|
@ -66,7 +66,7 @@ class MediaStoreSource extends CollectionSource {
|
||||||
// refresh after the first 10 entries, then after 100 more, then every 1000 entries
|
// refresh after the first 10 entries, then after 100 more, then every 1000 entries
|
||||||
var refreshCount = 10;
|
var refreshCount = 10;
|
||||||
const refreshCountMax = 1000;
|
const refreshCountMax = 1000;
|
||||||
final allNewEntries = <AvesEntry>[], pendingNewEntries = <AvesEntry>[];
|
final allNewEntries = <AvesEntry>{}, pendingNewEntries = <AvesEntry>{};
|
||||||
void addPendingEntries() {
|
void addPendingEntries() {
|
||||||
allNewEntries.addAll(pendingNewEntries);
|
allNewEntries.addAll(pendingNewEntries);
|
||||||
addAll(pendingNewEntries);
|
addAll(pendingNewEntries);
|
||||||
|
@ -86,10 +86,11 @@ class MediaStoreSource extends CollectionSource {
|
||||||
debugPrint('$runtimeType refresh loaded ${allNewEntries.length} new entries, elapsed=${stopwatch.elapsed}');
|
debugPrint('$runtimeType refresh loaded ${allNewEntries.length} new entries, elapsed=${stopwatch.elapsed}');
|
||||||
|
|
||||||
await metadataDb.saveEntries(allNewEntries); // 700ms for 5500 entries
|
await metadataDb.saveEntries(allNewEntries); // 700ms for 5500 entries
|
||||||
updateAlbums();
|
invalidateAlbumFilterSummary(entries: allNewEntries);
|
||||||
|
|
||||||
final analytics = FirebaseAnalytics();
|
final analytics = FirebaseAnalytics();
|
||||||
unawaited(analytics.setUserProperty(name: 'local_item_count', value: (ceilBy(rawEntries.length, 3)).toString()));
|
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;
|
stateNotifier.value = SourceState.cataloguing;
|
||||||
await catalogEntries();
|
await catalogEntries();
|
||||||
|
@ -128,7 +129,7 @@ class MediaStoreSource extends CollectionSource {
|
||||||
removeEntries(obsoleteEntries);
|
removeEntries(obsoleteEntries);
|
||||||
|
|
||||||
// fetch new entries
|
// fetch new entries
|
||||||
final newEntries = <AvesEntry>[];
|
final newEntries = <AvesEntry>{};
|
||||||
for (final kv in uriByContentId.entries) {
|
for (final kv in uriByContentId.entries) {
|
||||||
final contentId = kv.key;
|
final contentId = kv.key;
|
||||||
final uri = kv.value;
|
final uri = kv.value;
|
||||||
|
@ -150,7 +151,7 @@ class MediaStoreSource extends CollectionSource {
|
||||||
if (newEntries.isNotEmpty) {
|
if (newEntries.isNotEmpty) {
|
||||||
addAll(newEntries);
|
addAll(newEntries);
|
||||||
await metadataDb.saveEntries(newEntries);
|
await metadataDb.saveEntries(newEntries);
|
||||||
updateAlbums();
|
invalidateAlbumFilterSummary(entries: newEntries);
|
||||||
|
|
||||||
stateNotifier.value = SourceState.cataloguing;
|
stateNotifier.value = SourceState.cataloguing;
|
||||||
await catalogEntries();
|
await catalogEntries();
|
||||||
|
|
|
@ -5,21 +5,21 @@ class SectionKey {
|
||||||
}
|
}
|
||||||
|
|
||||||
class EntryAlbumSectionKey extends SectionKey {
|
class EntryAlbumSectionKey extends SectionKey {
|
||||||
final String folderPath;
|
final String directory;
|
||||||
|
|
||||||
const EntryAlbumSectionKey(this.folderPath);
|
const EntryAlbumSectionKey(this.directory);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (other.runtimeType != runtimeType) return false;
|
if (other.runtimeType != runtimeType) return false;
|
||||||
return other is EntryAlbumSectionKey && other.folderPath == folderPath;
|
return other is EntryAlbumSectionKey && other.directory == directory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => folderPath.hashCode;
|
int get hashCode => directory.hashCode;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => '$runtimeType#${shortHash(this)}{folderPath=$folderPath}';
|
String toString() => '$runtimeType#${shortHash(this)}{directory=$directory}';
|
||||||
}
|
}
|
||||||
|
|
||||||
class EntryDateSectionKey extends SectionKey {
|
class EntryDateSectionKey extends SectionKey {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
|
import 'package:aves/model/filters/tag.dart';
|
||||||
import 'package:aves/model/metadata.dart';
|
import 'package:aves/model/metadata.dart';
|
||||||
import 'package:aves/model/metadata_db.dart';
|
import 'package:aves/model/metadata_db.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
|
@ -56,9 +57,34 @@ mixin TagMixin on SourceBase {
|
||||||
void updateTags() {
|
void updateTags() {
|
||||||
final tags = rawEntries.expand((entry) => entry.xmpSubjects).toSet().toList()..sort(compareAsciiUpperCase);
|
final tags = rawEntries.expand((entry) => entry.xmpSubjects).toSet().toList()..sort(compareAsciiUpperCase);
|
||||||
sortedTags = List.unmodifiable(tags);
|
sortedTags = List.unmodifiable(tags);
|
||||||
invalidateFilterEntryCounts();
|
|
||||||
|
invalidateTagFilterSummary();
|
||||||
eventBus.fire(TagsChangedEvent());
|
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 {}
|
class CatalogMetadataChangedEvent {}
|
||||||
|
|
|
@ -8,13 +8,26 @@ import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class CollectionPage extends StatelessWidget {
|
class CollectionPage extends StatefulWidget {
|
||||||
static const routeName = '/collection';
|
static const routeName = '/collection';
|
||||||
|
|
||||||
final CollectionLens collection;
|
final CollectionLens collection;
|
||||||
|
|
||||||
const CollectionPage(this.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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MediaQueryDataProvider(
|
return MediaQueryDataProvider(
|
||||||
|
|
|
@ -7,18 +7,18 @@ import 'package:aves/widgets/common/identity/aves_icons.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class AlbumSectionHeader extends StatelessWidget {
|
class AlbumSectionHeader extends StatelessWidget {
|
||||||
final String folderPath, albumName;
|
final String directory, albumName;
|
||||||
|
|
||||||
AlbumSectionHeader({
|
AlbumSectionHeader({
|
||||||
Key key,
|
Key key,
|
||||||
@required CollectionSource source,
|
@required CollectionSource source,
|
||||||
@required this.folderPath,
|
@required this.directory,
|
||||||
}) : albumName = source.getUniqueAlbumName(folderPath),
|
}) : albumName = source.getUniqueAlbumName(directory),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var albumIcon = IconUtils.getAlbumIcon(context: context, album: folderPath);
|
var albumIcon = IconUtils.getAlbumIcon(context: context, album: directory);
|
||||||
if (albumIcon != null) {
|
if (albumIcon != null) {
|
||||||
albumIcon = Material(
|
albumIcon = Material(
|
||||||
type: MaterialType.circle,
|
type: MaterialType.circle,
|
||||||
|
@ -29,10 +29,10 @@ class AlbumSectionHeader extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return SectionHeader(
|
return SectionHeader(
|
||||||
sectionKey: EntryAlbumSectionKey(folderPath),
|
sectionKey: EntryAlbumSectionKey(directory),
|
||||||
leading: albumIcon,
|
leading: albumIcon,
|
||||||
title: albumName,
|
title: albumName,
|
||||||
trailing: androidFileUtils.isOnRemovableStorage(folderPath)
|
trailing: androidFileUtils.isOnRemovableStorage(directory)
|
||||||
? Icon(
|
? Icon(
|
||||||
AIcons.removableStorage,
|
AIcons.removableStorage,
|
||||||
size: 16,
|
size: 16,
|
||||||
|
@ -43,13 +43,13 @@ class AlbumSectionHeader extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
static double getPreferredHeight(BuildContext context, double maxWidth, CollectionSource source, EntryAlbumSectionKey sectionKey) {
|
static double getPreferredHeight(BuildContext context, double maxWidth, CollectionSource source, EntryAlbumSectionKey sectionKey) {
|
||||||
final folderPath = sectionKey.folderPath;
|
final directory = sectionKey.directory;
|
||||||
return SectionHeader.getPreferredHeight(
|
return SectionHeader.getPreferredHeight(
|
||||||
context: context,
|
context: context,
|
||||||
maxWidth: maxWidth,
|
maxWidth: maxWidth,
|
||||||
title: source.getUniqueAlbumName(folderPath),
|
title: source.getUniqueAlbumName(directory),
|
||||||
hasLeading: androidFileUtils.getAlbumType(folderPath) != AlbumType.regular,
|
hasLeading: androidFileUtils.getAlbumType(directory) != AlbumType.regular,
|
||||||
hasTrailing: androidFileUtils.isOnRemovableStorage(folderPath),
|
hasTrailing: androidFileUtils.isOnRemovableStorage(directory),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ class CollectionSectionHeader extends StatelessWidget {
|
||||||
Widget _buildAlbumHeader() => AlbumSectionHeader(
|
Widget _buildAlbumHeader() => AlbumSectionHeader(
|
||||||
key: ValueKey(sectionKey),
|
key: ValueKey(sectionKey),
|
||||||
source: collection.source,
|
source: collection.source,
|
||||||
folderPath: (sectionKey as EntryAlbumSectionKey).folderPath,
|
directory: (sectionKey as EntryAlbumSectionKey).directory,
|
||||||
);
|
);
|
||||||
|
|
||||||
switch (collection.sortFactor) {
|
switch (collection.sortFactor) {
|
||||||
|
|
|
@ -20,6 +20,7 @@ class AvesFilterChip extends StatefulWidget {
|
||||||
final OffsetFilterCallback onLongPress;
|
final OffsetFilterCallback onLongPress;
|
||||||
final BorderRadius borderRadius;
|
final BorderRadius borderRadius;
|
||||||
|
|
||||||
|
static const Color defaultOutlineColor = Colors.white;
|
||||||
static const double defaultRadius = 32;
|
static const double defaultRadius = 32;
|
||||||
static const double outlineWidth = 2;
|
static const double outlineWidth = 2;
|
||||||
static const double minChipHeight = kMinInteractiveDimension;
|
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.
|
// 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.
|
// So we save the result of the Future to a local variable because of this specific case.
|
||||||
_colorFuture = filter.color(context);
|
_colorFuture = filter.color(context);
|
||||||
_outlineColor = Colors.transparent;
|
_outlineColor = AvesFilterChip.defaultOutlineColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -13,7 +13,7 @@ class DebugAppDatabaseSection extends StatefulWidget {
|
||||||
|
|
||||||
class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with AutomaticKeepAliveClientMixin {
|
class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with AutomaticKeepAliveClientMixin {
|
||||||
Future<int> _dbFileSizeLoader;
|
Future<int> _dbFileSizeLoader;
|
||||||
Future<List<AvesEntry>> _dbEntryLoader;
|
Future<Set<AvesEntry>> _dbEntryLoader;
|
||||||
Future<List<DateMetadata>> _dbDateLoader;
|
Future<List<DateMetadata>> _dbDateLoader;
|
||||||
Future<List<CatalogMetadata>> _dbMetadataLoader;
|
Future<List<CatalogMetadata>> _dbMetadataLoader;
|
||||||
Future<List<AddressDetails>> _dbAddressLoader;
|
Future<List<AddressDetails>> _dbAddressLoader;
|
||||||
|
@ -57,7 +57,7 @@ class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
FutureBuilder<List>(
|
FutureBuilder<Set<AvesEntry>>(
|
||||||
future: _dbEntryLoader,
|
future: _dbEntryLoader,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.hasError) return Text(snapshot.error.toString());
|
if (snapshot.hasError) return Text(snapshot.error.toString());
|
||||||
|
|
|
@ -142,10 +142,11 @@ class _AppDrawerState extends State<AppDrawer> {
|
||||||
return StreamBuilder(
|
return StreamBuilder(
|
||||||
stream: source.eventBus.on<AlbumsChangedEvent>(),
|
stream: source.eventBus.on<AlbumsChangedEvent>(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final specialAlbums = source.sortedAlbums.where((album) {
|
final specialAlbums = source.rawAlbums.where((album) {
|
||||||
final type = androidFileUtils.getAlbumType(album);
|
final type = androidFileUtils.getAlbumType(album);
|
||||||
return [AlbumType.camera, AlbumType.screenshots].contains(type);
|
return [AlbumType.camera, AlbumType.screenshots].contains(type);
|
||||||
});
|
}).toList()
|
||||||
|
..sort(source.compareAlbumsByName);
|
||||||
|
|
||||||
if (specialAlbums.isEmpty) return SizedBox.shrink();
|
if (specialAlbums.isEmpty) return SizedBox.shrink();
|
||||||
return Column(
|
return Column(
|
||||||
|
@ -185,7 +186,7 @@ class _AppDrawerState extends State<AppDrawer> {
|
||||||
title: 'Albums',
|
title: 'Albums',
|
||||||
trailing: StreamBuilder(
|
trailing: StreamBuilder(
|
||||||
stream: source.eventBus.on<AlbumsChangedEvent>(),
|
stream: source.eventBus.on<AlbumsChangedEvent>(),
|
||||||
builder: (context, _) => Text('${source.sortedAlbums.length}'),
|
builder: (context, _) => Text('${source.rawAlbums.length}'),
|
||||||
),
|
),
|
||||||
routeName: AlbumListPage.routeName,
|
routeName: AlbumListPage.routeName,
|
||||||
pageBuilder: (_) => AlbumListPage(source: source),
|
pageBuilder: (_) => AlbumListPage(source: source),
|
||||||
|
|
|
@ -60,8 +60,7 @@ class AlbumListPage extends StatelessWidget {
|
||||||
// common with album selection page to move/copy entries
|
// common with album selection page to move/copy entries
|
||||||
|
|
||||||
static Map<ChipSectionKey, List<FilterGridItem<AlbumFilter>>> getAlbumEntries(CollectionSource source) {
|
static Map<ChipSectionKey, List<FilterGridItem<AlbumFilter>>> getAlbumEntries(CollectionSource source) {
|
||||||
// albums are initially sorted by name at the source level
|
final filters = source.rawAlbums.map((album) => AlbumFilter(album, source.getUniqueAlbumName(album))).toSet();
|
||||||
final filters = source.sortedAlbums.map((album) => AlbumFilter(album, source.getUniqueAlbumName(album)));
|
|
||||||
|
|
||||||
final sorted = FilterNavigationPage.sort(settings.albumSortFactor, source, filters);
|
final sorted = FilterNavigationPage.sort(settings.albumSortFactor, source, filters);
|
||||||
return _group(sorted);
|
return _group(sorted);
|
||||||
|
|
|
@ -160,19 +160,22 @@ class FilterNavigationPage<T extends CollectionFilter> extends StatelessWidget {
|
||||||
return c != 0 ? c : a.key.compareTo(b.key);
|
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) {
|
Iterable<FilterGridItem<T>> toGridItem(CollectionSource source, Iterable<T> filters) {
|
||||||
final entriesByDate = source.sortedEntriesForFilterList;
|
|
||||||
return filters.map((filter) => FilterGridItem(
|
return filters.map((filter) => FilterGridItem(
|
||||||
filter,
|
filter,
|
||||||
entriesByDate.firstWhere(filter.filter, orElse: () => null),
|
source.recentEntry(filter),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Iterable<FilterGridItem<T>> allMapEntries;
|
Iterable<FilterGridItem<T>> allMapEntries;
|
||||||
switch (sortFactor) {
|
switch (sortFactor) {
|
||||||
case ChipSortFactor.name:
|
case ChipSortFactor.name:
|
||||||
allMapEntries = toGridItem(source, filters);
|
allMapEntries = toGridItem(source, filters).toList()..sort(compareFiltersByName);
|
||||||
break;
|
break;
|
||||||
case ChipSortFactor.date:
|
case ChipSortFactor.date:
|
||||||
allMapEntries = toGridItem(source, filters).toList()..sort(compareFiltersByDate);
|
allMapEntries = toGridItem(source, filters).toList()..sort(compareFiltersByDate);
|
||||||
|
@ -180,7 +183,7 @@ class FilterNavigationPage<T extends CollectionFilter> extends StatelessWidget {
|
||||||
case ChipSortFactor.count:
|
case ChipSortFactor.count:
|
||||||
final filtersWithCount = List.of(filters.map((filter) => MapEntry(filter, source.count(filter))));
|
final filtersWithCount = List.of(filters.map((filter) => MapEntry(filter, source.count(filter))));
|
||||||
filtersWithCount.sort(compareFiltersByEntryCount);
|
filtersWithCount.sort(compareFiltersByEntryCount);
|
||||||
filters = filtersWithCount.map((kv) => kv.key).toList();
|
filters = filtersWithCount.map((kv) => kv.key).toSet();
|
||||||
allMapEntries = toGridItem(source, filters);
|
allMapEntries = toGridItem(source, filters);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,8 +50,7 @@ class CountryListPage extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<ChipSectionKey, List<FilterGridItem<LocationFilter>>> _getCountryEntries() {
|
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)).toSet();
|
||||||
final filters = source.sortedCountries.map((location) => LocationFilter(LocationLevel.country, location));
|
|
||||||
|
|
||||||
final sorted = FilterNavigationPage.sort(settings.countrySortFactor, source, filters);
|
final sorted = FilterNavigationPage.sort(settings.countrySortFactor, source, filters);
|
||||||
return _group(sorted);
|
return _group(sorted);
|
||||||
|
|
|
@ -50,8 +50,7 @@ class TagListPage extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<ChipSectionKey, List<FilterGridItem<TagFilter>>> _getTagEntries() {
|
Map<ChipSectionKey, List<FilterGridItem<TagFilter>>> _getTagEntries() {
|
||||||
// tags are initially sorted by name at the source level
|
final filters = source.sortedTags.map((tag) => TagFilter(tag)).toSet();
|
||||||
final filters = source.sortedTags.map((tag) => TagFilter(tag));
|
|
||||||
|
|
||||||
final sorted = FilterNavigationPage.sort(settings.tagSortFactor, source, filters);
|
final sorted = FilterNavigationPage.sort(settings.tagSortFactor, source, filters);
|
||||||
return _group(sorted);
|
return _group(sorted);
|
||||||
|
|
|
@ -86,7 +86,7 @@ class CollectionSearchDelegate {
|
||||||
MimeFilter(MimeFilter.sphericalVideo),
|
MimeFilter(MimeFilter.sphericalVideo),
|
||||||
MimeFilter(MimeFilter.geotiff),
|
MimeFilter(MimeFilter.geotiff),
|
||||||
MimeFilter(MimeTypes.svg),
|
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,
|
// 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
|
// 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,
|
heroTypeBuilder: (filter) => filter == queryFilter ? HeroType.always : HeroType.onTap,
|
||||||
|
@ -100,7 +100,8 @@ class CollectionSearchDelegate {
|
||||||
StreamBuilder(
|
StreamBuilder(
|
||||||
stream: source.eventBus.on<AlbumsChangedEvent>(),
|
stream: source.eventBus.on<AlbumsChangedEvent>(),
|
||||||
builder: (context, snapshot) {
|
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(
|
return _buildFilterRow(
|
||||||
context: context,
|
context: context,
|
||||||
title: 'Albums',
|
title: 'Albums',
|
||||||
|
@ -110,7 +111,7 @@ class CollectionSearchDelegate {
|
||||||
StreamBuilder(
|
StreamBuilder(
|
||||||
stream: source.eventBus.on<LocationsChangedEvent>(),
|
stream: source.eventBus.on<LocationsChangedEvent>(),
|
||||||
builder: (context, snapshot) {
|
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(
|
return _buildFilterRow(
|
||||||
context: context,
|
context: context,
|
||||||
title: 'Countries',
|
title: 'Countries',
|
||||||
|
@ -154,7 +155,7 @@ class CollectionSearchDelegate {
|
||||||
Widget _buildFilterRow({
|
Widget _buildFilterRow({
|
||||||
@required BuildContext context,
|
@required BuildContext context,
|
||||||
String title,
|
String title,
|
||||||
@required Iterable<CollectionFilter> filters,
|
@required List<CollectionFilter> filters,
|
||||||
HeroType Function(CollectionFilter filter) heroTypeBuilder,
|
HeroType Function(CollectionFilter filter) heroTypeBuilder,
|
||||||
}) {
|
}) {
|
||||||
return ExpandableFilterRow(
|
return ExpandableFilterRow(
|
||||||
|
|
Loading…
Reference in a new issue