#13 hidden filters

This commit is contained in:
Thibault Deckers 2021-02-09 13:38:53 +09:00
parent c5ee55adb0
commit ea3d79afbe
25 changed files with 260 additions and 107 deletions

View file

@ -10,6 +10,7 @@ enum ChipSetAction {
enum ChipAction { enum ChipAction {
delete, delete,
hide,
pin, pin,
unpin, unpin,
rename, rename,
@ -20,6 +21,8 @@ extension ExtraChipAction on ChipAction {
switch (this) { switch (this) {
case ChipAction.delete: case ChipAction.delete:
return 'Delete'; return 'Delete';
case ChipAction.hide:
return 'Hide';
case ChipAction.pin: case ChipAction.pin:
return 'Pin to top'; return 'Pin to top';
case ChipAction.unpin: case ChipAction.unpin:
@ -34,6 +37,8 @@ extension ExtraChipAction on ChipAction {
switch (this) { switch (this) {
case ChipAction.delete: case ChipAction.delete:
return AIcons.delete; return AIcons.delete;
case ChipAction.hide:
return AIcons.hide;
case ChipAction.pin: case ChipAction.pin:
case ChipAction.unpin: case ChipAction.unpin:
return AIcons.pin; return AIcons.pin;

View file

@ -172,14 +172,8 @@ class AvesEntry {
addressChangeNotifier.dispose(); addressChangeNotifier.dispose();
} }
@override // do not implement [Object.==] and [Object.hashCode] using mutable attributes (e.g. `uri`)
bool operator ==(Object other) { // so that we can reliably use instances in a `Set`, which requires consistent hash codes over time
if (other.runtimeType != runtimeType) return false;
return other is AvesEntry && other.uri == uri && other.pageId == pageId && other._dateModifiedSecs == _dateModifiedSecs;
}
@override
int get hashCode => hashValues(uri, pageId, _dateModifiedSecs);
@override @override
String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, path=$path, pageId=$pageId}'; String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, path=$path, pageId=$pageId}';

View file

@ -43,6 +43,7 @@ class Settings extends ChangeNotifier {
static const countrySortFactorKey = 'country_sort_factor'; static const countrySortFactorKey = 'country_sort_factor';
static const tagSortFactorKey = 'tag_sort_factor'; static const tagSortFactorKey = 'tag_sort_factor';
static const pinnedFiltersKey = 'pinned_filters'; static const pinnedFiltersKey = 'pinned_filters';
static const hiddenFiltersKey = 'hidden_filters';
// viewer // viewer
static const showOverlayMinimapKey = 'show_overlay_minimap'; static const showOverlayMinimapKey = 'show_overlay_minimap';
@ -167,6 +168,10 @@ class Settings extends ChangeNotifier {
set pinnedFilters(Set<CollectionFilter> newValue) => setAndNotify(pinnedFiltersKey, newValue.map((filter) => filter.toJson()).toList()); set pinnedFilters(Set<CollectionFilter> newValue) => setAndNotify(pinnedFiltersKey, newValue.map((filter) => filter.toJson()).toList());
Set<CollectionFilter> get hiddenFilters => (_prefs.getStringList(hiddenFiltersKey) ?? []).map(CollectionFilter.fromJson).toSet();
set hiddenFilters(Set<CollectionFilter> newValue) => setAndNotify(hiddenFiltersKey, newValue.map((filter) => filter.toJson()).toList());
// viewer // viewer
bool get showOverlayMinimap => getBoolOrDefault(showOverlayMinimapKey, false); bool get showOverlayMinimap => getBoolOrDefault(showOverlayMinimapKey, false);

View file

@ -34,9 +34,18 @@ mixin AlbumMixin on SourceBase {
final uniqueName = parts.skip(parts.length - partCount).join(separator); final uniqueName = parts.skip(parts.length - partCount).join(separator);
final volume = androidFileUtils.getStorageVolume(album); final volume = androidFileUtils.getStorageVolume(album);
final volumeRoot = volume?.path ?? ''; if (volume == null) {
final albumRelativePath = album.substring(volumeRoot.length); return uniqueName;
if (uniqueName.length < albumRelativePath.length || volume == null) { }
final volumeRootLength = volume.path.length;
if (album.length < volumeRootLength) {
// `album` is at the root, without trailing '/'
return uniqueName;
}
final albumRelativePath = album.substring(volumeRootLength);
if (uniqueName.length < albumRelativePath.length) {
return uniqueName; return uniqueName;
} else if (volume.isPrimary) { } else if (volume.isPrimary) {
return albumRelativePath; return albumRelativePath;
@ -67,9 +76,17 @@ mixin AlbumMixin on SourceBase {
))); )));
} }
void addDirectory(Iterable<String> albums) { void updateDirectories() {
_directories.addAll(albums); final visibleDirectories = visibleEntries.map((entry) => entry.directory).toSet();
_notifyAlbumChange(); addDirectories(visibleDirectories);
cleanEmptyAlbums();
}
void addDirectories(Set<String> albums) {
if (!_directories.containsAll(albums)) {
_directories.addAll(albums);
_notifyAlbumChange();
}
} }
void cleanEmptyAlbums([Set<String> albums]) { void cleanEmptyAlbums([Set<String> albums]) {
@ -85,7 +102,7 @@ mixin AlbumMixin on SourceBase {
} }
} }
bool _isEmptyAlbum(String album) => !rawEntries.any((entry) => entry.directory == album); bool _isEmptyAlbum(String album) => !visibleEntries.any((entry) => entry.directory == album);
// filter summary // filter summary
@ -105,11 +122,11 @@ mixin AlbumMixin on SourceBase {
} }
int albumEntryCount(AlbumFilter filter) { int albumEntryCount(AlbumFilter filter) {
return _filterEntryCountMap.putIfAbsent(filter.album, () => rawEntries.where((entry) => filter.filter(entry)).length); return _filterEntryCountMap.putIfAbsent(filter.album, () => visibleEntries.where((entry) => filter.filter(entry)).length);
} }
AvesEntry albumRecentEntry(AlbumFilter filter) { AvesEntry albumRecentEntry(AlbumFilter filter) {
return _filterRecentEntryMap.putIfAbsent(filter.album, () => sortedEntriesByDate.firstWhere((entry) => filter.filter(entry))); return _filterRecentEntryMap.putIfAbsent(filter.album, () => sortedEntriesByDate.firstWhere((entry) => filter.filter(entry), orElse: () => null));
} }
} }

View file

@ -25,7 +25,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
int id; int id;
bool listenToSource; bool listenToSource;
List<AvesEntry> _filteredEntries; List<AvesEntry> _filteredSortedEntries;
List<StreamSubscription> _subscriptions = []; List<StreamSubscription> _subscriptions = [];
Map<SectionKey, List<AvesEntry>> sections = Map.unmodifiable({}); Map<SectionKey, List<AvesEntry>> sections = Map.unmodifiable({});
@ -64,9 +64,9 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
super.dispose(); super.dispose();
} }
bool get isEmpty => _filteredEntries.isEmpty; bool get isEmpty => _filteredSortedEntries.isEmpty;
int get entryCount => _filteredEntries.length; int get entryCount => _filteredSortedEntries.length;
// sorted as displayed to the user, i.e. sorted then grouped, not an absolute order on all entries // sorted as displayed to the user, i.e. sorted then grouped, not an absolute order on all entries
List<AvesEntry> _sortedEntries; List<AvesEntry> _sortedEntries;
@ -122,20 +122,20 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
} }
void _applyFilters() { void _applyFilters() {
final rawEntries = source.rawEntries; final entries = source.visibleEntries;
_filteredEntries = List.of(filters.isEmpty ? rawEntries : rawEntries.where((entry) => filters.fold(true, (prev, filter) => prev && filter.filter(entry)))); _filteredSortedEntries = List.of(filters.isEmpty ? entries : entries.where((entry) => filters.fold(true, (prev, filter) => prev && filter.filter(entry))));
} }
void _applySort() { void _applySort() {
switch (sortFactor) { switch (sortFactor) {
case EntrySortFactor.date: case EntrySortFactor.date:
_filteredEntries.sort(AvesEntry.compareByDate); _filteredSortedEntries.sort(AvesEntry.compareByDate);
break; break;
case EntrySortFactor.size: case EntrySortFactor.size:
_filteredEntries.sort(AvesEntry.compareBySize); _filteredSortedEntries.sort(AvesEntry.compareBySize);
break; break;
case EntrySortFactor.name: case EntrySortFactor.name:
_filteredEntries.sort(AvesEntry.compareByName); _filteredSortedEntries.sort(AvesEntry.compareByName);
break; break;
} }
} }
@ -145,28 +145,28 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
case EntrySortFactor.date: case EntrySortFactor.date:
switch (groupFactor) { switch (groupFactor) {
case EntryGroupFactor.album: case EntryGroupFactor.album:
sections = groupBy<AvesEntry, EntryAlbumSectionKey>(_filteredEntries, (entry) => EntryAlbumSectionKey(entry.directory)); sections = groupBy<AvesEntry, EntryAlbumSectionKey>(_filteredSortedEntries, (entry) => EntryAlbumSectionKey(entry.directory));
break; break;
case EntryGroupFactor.month: case EntryGroupFactor.month:
sections = groupBy<AvesEntry, EntryDateSectionKey>(_filteredEntries, (entry) => EntryDateSectionKey(entry.monthTaken)); sections = groupBy<AvesEntry, EntryDateSectionKey>(_filteredSortedEntries, (entry) => EntryDateSectionKey(entry.monthTaken));
break; break;
case EntryGroupFactor.day: case EntryGroupFactor.day:
sections = groupBy<AvesEntry, EntryDateSectionKey>(_filteredEntries, (entry) => EntryDateSectionKey(entry.dayTaken)); sections = groupBy<AvesEntry, EntryDateSectionKey>(_filteredSortedEntries, (entry) => EntryDateSectionKey(entry.dayTaken));
break; break;
case EntryGroupFactor.none: case EntryGroupFactor.none:
sections = Map.fromEntries([ sections = Map.fromEntries([
MapEntry(null, _filteredEntries), MapEntry(null, _filteredSortedEntries),
]); ]);
break; break;
} }
break; break;
case EntrySortFactor.size: case EntrySortFactor.size:
sections = Map.fromEntries([ sections = Map.fromEntries([
MapEntry(null, _filteredEntries), MapEntry(null, _filteredSortedEntries),
]); ]);
break; break;
case EntrySortFactor.name: case EntrySortFactor.name:
final byAlbum = groupBy<AvesEntry, EntryAlbumSectionKey>(_filteredEntries, (entry) => EntryAlbumSectionKey(entry.directory)); final byAlbum = groupBy<AvesEntry, EntryAlbumSectionKey>(_filteredSortedEntries, (entry) => EntryAlbumSectionKey(entry.directory));
sections = SplayTreeMap<EntryAlbumSectionKey, List<AvesEntry>>.of(byAlbum, (a, b) => source.compareAlbumsByName(a.directory, b.directory)); sections = SplayTreeMap<EntryAlbumSectionKey, List<AvesEntry>>.of(byAlbum, (a, b) => source.compareAlbumsByName(a.directory, b.directory));
break; break;
} }
@ -191,7 +191,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
// 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
_filteredEntries.removeWhere(entries.contains); _filteredSortedEntries.removeWhere(entries.contains);
_sortedEntries?.removeWhere(entries.contains); _sortedEntries?.removeWhere(entries.contains);
sections.forEach((key, sectionEntries) => sectionEntries.removeWhere(entries.contains)); sections.forEach((key, sectionEntries) => sectionEntries.removeWhere(entries.contains));
sections = Map.unmodifiable(Map.fromEntries(sections.entries.where((kv) => kv.value.isNotEmpty))); sections = Map.unmodifiable(Map.fromEntries(sections.entries.where((kv) => kv.value.isNotEmpty)));

View file

@ -8,6 +8,7 @@ import 'package:aves/model/filters/location.dart';
import 'package:aves/model/filters/tag.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/settings/settings.dart';
import 'package:aves/model/source/album.dart'; import 'package:aves/model/source/album.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';
@ -16,13 +17,9 @@ import 'package:event_bus/event_bus.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
mixin SourceBase { mixin SourceBase {
final List<AvesEntry> _rawEntries = []; EventBus get eventBus;
List<AvesEntry> get rawEntries => List.unmodifiable(_rawEntries); Set<AvesEntry> get visibleEntries;
final EventBus _eventBus = EventBus();
EventBus get eventBus => _eventBus;
List<AvesEntry> get sortedEntriesByDate; List<AvesEntry> get sortedEntriesByDate;
@ -34,11 +31,30 @@ mixin SourceBase {
} }
abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin { abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin {
final EventBus _eventBus = EventBus();
@override
EventBus get eventBus => _eventBus;
final Set<AvesEntry> _rawEntries = {};
// TODO TLAD use `Set.unmodifiable()` when possible
Set<AvesEntry> get allEntries => Set.of(_rawEntries);
Set<AvesEntry> _visibleEntries;
@override
Set<AvesEntry> get visibleEntries {
// TODO TLAD use `Set.unmodifiable()` when possible
_visibleEntries ??= Set.of(_applyHiddenFilters(_rawEntries));
return _visibleEntries;
}
List<AvesEntry> _sortedEntriesByDate; List<AvesEntry> _sortedEntriesByDate;
@override @override
List<AvesEntry> get sortedEntriesByDate { List<AvesEntry> get sortedEntriesByDate {
_sortedEntriesByDate ??= List.of(_rawEntries)..sort(AvesEntry.compareByDate); _sortedEntriesByDate ??= List.unmodifiable(visibleEntries.toList()..sort(AvesEntry.compareByDate));
return _sortedEntriesByDate; return _sortedEntriesByDate;
} }
@ -52,10 +68,23 @@ 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(Set<AvesEntry> entries) { Iterable<AvesEntry> _applyHiddenFilters(Iterable<AvesEntry> entries) {
final hiddenFilters = settings.hiddenFilters;
return hiddenFilters.isEmpty ? entries : entries.where((entry) => !hiddenFilters.any((filter) => filter.filter(entry)));
}
void _invalidate([Set<AvesEntry> entries]) {
_visibleEntries = null;
_sortedEntriesByDate = null;
invalidateAlbumFilterSummary(entries: entries);
invalidateCountryFilterSummary(entries);
invalidateTagFilterSummary(entries);
}
void addEntries(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).toSet();
_rawEntries.removeWhere((entry) => newContentIds.contains(entry.contentId)); _rawEntries.removeWhere((entry) => newContentIds.contains(entry.contentId));
} }
entries.forEach((entry) { entries.forEach((entry) {
@ -63,28 +92,32 @@ 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);
addDirectory(_rawEntries.map((entry) => entry.directory)); _invalidate(entries);
_invalidateFilterSummaries(entries);
addDirectories(_applyHiddenFilters(entries).map((entry) => entry.directory).toSet());
eventBus.fire(EntryAddedEvent(entries)); eventBus.fire(EntryAddedEvent(entries));
} }
void removeEntries(Set<AvesEntry> entries) { void removeEntries(Set<String> uris) {
if (entries.isEmpty) return; if (uris.isEmpty) return;
final entries = _rawEntries.where((entry) => uris.contains(entry.uri)).toSet();
entries.forEach((entry) => entry.removeFromFavourites()); entries.forEach((entry) => entry.removeFromFavourites());
_rawEntries.removeWhere(entries.contains); _rawEntries.removeAll(entries);
_invalidate(entries);
cleanEmptyAlbums(entries.map((entry) => entry.directory).toSet()); cleanEmptyAlbums(entries.map((entry) => entry.directory).toSet());
updateLocations(); updateLocations();
updateTags(); updateTags();
_invalidateFilterSummaries(entries);
eventBus.fire(EntryRemovedEvent(entries)); eventBus.fire(EntryRemovedEvent(entries));
} }
void clearEntries() { void clearEntries() {
_rawEntries.clear(); _rawEntries.clear();
cleanEmptyAlbums(); _invalidate();
updateDirectories();
updateLocations(); updateLocations();
updateTags(); updateTags();
_invalidateFilterSummaries();
} }
Future<void> _moveEntry(AvesEntry entry, Map newFields, bool isFavourite) async { Future<void> _moveEntry(AvesEntry entry, Map newFields, bool isFavourite) async {
@ -154,13 +187,13 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
} }
if (copy) { if (copy) {
addAll(movedEntries); addEntries(movedEntries);
} else { } else {
cleanEmptyAlbums(fromAlbums); cleanEmptyAlbums(fromAlbums);
addDirectory({destinationAlbum}); addDirectories({destinationAlbum});
} }
invalidateAlbumFilterSummary(directories: fromAlbums); invalidateAlbumFilterSummary(directories: fromAlbums);
_invalidateFilterSummaries(movedEntries); _invalidate(movedEntries);
eventBus.fire(EntryMovedEvent(movedEntries)); eventBus.fire(EntryMovedEvent(movedEntries));
} }
@ -174,13 +207,6 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
// filter summary // filter summary
void _invalidateFilterSummaries([Set<AvesEntry> entries]) {
_sortedEntriesByDate = null;
invalidateAlbumFilterSummary(entries: entries);
invalidateCountryFilterSummary(entries);
invalidateTagFilterSummary(entries);
}
int count(CollectionFilter filter) { int count(CollectionFilter filter) {
if (filter is AlbumFilter) return albumEntryCount(filter); if (filter is AlbumFilter) return albumEntryCount(filter);
if (filter is LocationFilter) return countryEntryCount(filter); if (filter is LocationFilter) return countryEntryCount(filter);
@ -194,6 +220,24 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
if (filter is TagFilter) return tagRecentEntry(filter); if (filter is TagFilter) return tagRecentEntry(filter);
return null; return null;
} }
void changeFilterVisibility(CollectionFilter filter, bool visible) {
final hiddenFilters = settings.hiddenFilters;
if (visible) {
hiddenFilters.remove(filter);
} else {
hiddenFilters.add(filter);
settings.searchHistory = settings.searchHistory..remove(filter);
}
settings.hiddenFilters = hiddenFilters;
_invalidate();
// it is possible for entries hidden by a filter type, to have an impact on other types
// e.g. given a sole entry for country C and tag T, hiding T should make C disappear too
updateDirectories();
updateLocations();
updateTags();
}
} }
enum SourceState { loading, cataloguing, locating, ready } enum SourceState { loading, cataloguing, locating, ready }

View file

@ -19,7 +19,7 @@ mixin LocationMixin on SourceBase {
Future<void> loadAddresses() async { Future<void> loadAddresses() async {
final stopwatch = Stopwatch()..start(); final stopwatch = Stopwatch()..start();
final saved = await metadataDb.loadAddresses(); final saved = await metadataDb.loadAddresses();
rawEntries.forEach((entry) { visibleEntries.forEach((entry) {
final contentId = entry.contentId; final contentId = entry.contentId;
entry.addressDetails = saved.firstWhere((address) => address.contentId == contentId, orElse: () => null); entry.addressDetails = saved.firstWhere((address) => address.contentId == contentId, orElse: () => null);
}); });
@ -31,7 +31,7 @@ mixin LocationMixin on SourceBase {
if (!(await availability.canGeolocate)) return; if (!(await availability.canGeolocate)) return;
// final stopwatch = Stopwatch()..start(); // final stopwatch = Stopwatch()..start();
final byLocated = groupBy<AvesEntry, bool>(rawEntries.where((entry) => entry.hasGps), (entry) => entry.isLocated); final byLocated = groupBy<AvesEntry, bool>(visibleEntries.where((entry) => entry.hasGps), (entry) => entry.isLocated);
final todo = byLocated[false] ?? []; final todo = byLocated[false] ?? [];
if (todo.isEmpty) return; if (todo.isEmpty) return;
@ -91,7 +91,7 @@ mixin LocationMixin on SourceBase {
} }
void updateLocations() { void updateLocations() {
final locations = rawEntries.where((entry) => entry.isLocated).map((entry) => entry.addressDetails).toList(); final locations = visibleEntries.where((entry) => entry.isLocated).map((entry) => entry.addressDetails).toList();
sortedPlaces = List<String>.unmodifiable(locations.map((address) => address.place).where((s) => s != null && s.isNotEmpty).toSet().toList()..sort(compareAsciiUpperCase)); sortedPlaces = List<String>.unmodifiable(locations.map((address) => address.place).where((s) => s != null && s.isNotEmpty).toSet().toList()..sort(compareAsciiUpperCase));
// the same country code could be found with different country names // the same country code could be found with different country names
@ -121,11 +121,11 @@ mixin LocationMixin on SourceBase {
} }
int countryEntryCount(LocationFilter filter) { int countryEntryCount(LocationFilter filter) {
return _filterEntryCountMap.putIfAbsent(filter.countryCode, () => rawEntries.where((entry) => filter.filter(entry)).length); return _filterEntryCountMap.putIfAbsent(filter.countryCode, () => visibleEntries.where((entry) => filter.filter(entry)).length);
} }
AvesEntry countryRecentEntry(LocationFilter filter) { AvesEntry countryRecentEntry(LocationFilter filter) {
return _filterRecentEntryMap.putIfAbsent(filter.countryCode, () => sortedEntriesByDate.firstWhere((entry) => filter.filter(entry))); return _filterRecentEntryMap.putIfAbsent(filter.countryCode, () => sortedEntriesByDate.firstWhere((entry) => filter.filter(entry), orElse: () => null));
} }
} }

View file

@ -54,7 +54,7 @@ class MediaStoreSource extends CollectionSource {
oldEntries.removeWhere((entry) => obsoleteContentIds.contains(entry.contentId)); oldEntries.removeWhere((entry) => obsoleteContentIds.contains(entry.contentId));
// show known entries // show known entries
addAll(oldEntries); addEntries(oldEntries);
await loadCatalogMetadata(); // 600ms for 5500 entries await loadCatalogMetadata(); // 600ms for 5500 entries
await loadAddresses(); // 200ms for 3000 entries await loadAddresses(); // 200ms for 3000 entries
debugPrint('$runtimeType refresh loaded ${oldEntries.length} known entries, elapsed=${stopwatch.elapsed}'); debugPrint('$runtimeType refresh loaded ${oldEntries.length} known entries, elapsed=${stopwatch.elapsed}');
@ -69,7 +69,7 @@ class MediaStoreSource extends CollectionSource {
final allNewEntries = <AvesEntry>{}, pendingNewEntries = <AvesEntry>{}; final allNewEntries = <AvesEntry>{}, pendingNewEntries = <AvesEntry>{};
void addPendingEntries() { void addPendingEntries() {
allNewEntries.addAll(pendingNewEntries); allNewEntries.addAll(pendingNewEntries);
addAll(pendingNewEntries); addEntries(pendingNewEntries);
pendingNewEntries.clear(); pendingNewEntries.clear();
} }
@ -89,7 +89,7 @@ class MediaStoreSource extends CollectionSource {
invalidateAlbumFilterSummary(entries: allNewEntries); 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(allEntries.length, 3)).toString()));
unawaited(analytics.setUserProperty(name: 'album_count', value: (ceilBy(rawAlbums.length, 1)).toString())); unawaited(analytics.setUserProperty(name: 'album_count', value: (ceilBy(rawAlbums.length, 1)).toString()));
stateNotifier.value = SourceState.cataloguing; stateNotifier.value = SourceState.cataloguing;
@ -124,9 +124,9 @@ class MediaStoreSource extends CollectionSource {
// clean up obsolete entries // clean up obsolete entries
final obsoleteContentIds = (await ImageFileService.getObsoleteEntries(uriByContentId.keys.toList())).toSet(); final obsoleteContentIds = (await ImageFileService.getObsoleteEntries(uriByContentId.keys.toList())).toSet();
final obsoleteUris = obsoleteContentIds.map((contentId) => uriByContentId[contentId]).toSet();
removeEntries(obsoleteUris);
obsoleteContentIds.forEach(uriByContentId.remove); obsoleteContentIds.forEach(uriByContentId.remove);
final obsoleteEntries = rawEntries.where((e) => obsoleteContentIds.contains(e.contentId)).toSet();
removeEntries(obsoleteEntries);
// fetch new entries // fetch new entries
final newEntries = <AvesEntry>{}; final newEntries = <AvesEntry>{};
@ -135,7 +135,7 @@ class MediaStoreSource extends CollectionSource {
final uri = kv.value; final uri = kv.value;
final sourceEntry = await ImageFileService.getEntry(uri, null); final sourceEntry = await ImageFileService.getEntry(uri, null);
if (sourceEntry != null) { if (sourceEntry != null) {
final existingEntry = rawEntries.firstWhere((entry) => entry.contentId == contentId, orElse: () => null); final existingEntry = allEntries.firstWhere((entry) => entry.contentId == contentId, orElse: () => null);
if (existingEntry == null || sourceEntry.dateModifiedSecs > existingEntry.dateModifiedSecs) { if (existingEntry == null || sourceEntry.dateModifiedSecs > existingEntry.dateModifiedSecs) {
final volume = androidFileUtils.getStorageVolume(sourceEntry.path); final volume = androidFileUtils.getStorageVolume(sourceEntry.path);
if (volume != null) { if (volume != null) {
@ -149,7 +149,7 @@ class MediaStoreSource extends CollectionSource {
} }
if (newEntries.isNotEmpty) { if (newEntries.isNotEmpty) {
addAll(newEntries); addEntries(newEntries);
await metadataDb.saveEntries(newEntries); await metadataDb.saveEntries(newEntries);
invalidateAlbumFilterSummary(entries: newEntries); invalidateAlbumFilterSummary(entries: newEntries);

View file

@ -14,7 +14,7 @@ mixin TagMixin on SourceBase {
Future<void> loadCatalogMetadata() async { Future<void> loadCatalogMetadata() async {
final stopwatch = Stopwatch()..start(); final stopwatch = Stopwatch()..start();
final saved = await metadataDb.loadMetadataEntries(); final saved = await metadataDb.loadMetadataEntries();
rawEntries.forEach((entry) { visibleEntries.forEach((entry) {
final contentId = entry.contentId; final contentId = entry.contentId;
entry.catalogMetadata = saved.firstWhere((metadata) => metadata.contentId == contentId, orElse: () => null); entry.catalogMetadata = saved.firstWhere((metadata) => metadata.contentId == contentId, orElse: () => null);
}); });
@ -24,7 +24,7 @@ mixin TagMixin on SourceBase {
Future<void> catalogEntries() async { Future<void> catalogEntries() async {
// final stopwatch = Stopwatch()..start(); // final stopwatch = Stopwatch()..start();
final todo = rawEntries.where((entry) => !entry.isCatalogued).toList(); final todo = visibleEntries.where((entry) => !entry.isCatalogued).toList();
if (todo.isEmpty) return; if (todo.isEmpty) return;
var progressDone = 0; var progressDone = 0;
@ -55,7 +55,7 @@ mixin TagMixin on SourceBase {
} }
void updateTags() { void updateTags() {
final tags = rawEntries.expand((entry) => entry.xmpSubjects).toSet().toList()..sort(compareAsciiUpperCase); final tags = visibleEntries.expand((entry) => entry.xmpSubjects).toSet().toList()..sort(compareAsciiUpperCase);
sortedTags = List.unmodifiable(tags); sortedTags = List.unmodifiable(tags);
invalidateTagFilterSummary(); invalidateTagFilterSummary();
@ -79,11 +79,11 @@ mixin TagMixin on SourceBase {
} }
int tagEntryCount(TagFilter filter) { int tagEntryCount(TagFilter filter) {
return _filterEntryCountMap.putIfAbsent(filter.tag, () => rawEntries.where((entry) => filter.filter(entry)).length); return _filterEntryCountMap.putIfAbsent(filter.tag, () => visibleEntries.where((entry) => filter.filter(entry)).length);
} }
AvesEntry tagRecentEntry(TagFilter filter) { AvesEntry tagRecentEntry(TagFilter filter) {
return _filterRecentEntryMap.putIfAbsent(filter.tag, () => sortedEntriesByDate.firstWhere((entry) => filter.filter(entry))); return _filterRecentEntryMap.putIfAbsent(filter.tag, () => sortedEntriesByDate.firstWhere((entry) => filter.filter(entry), orElse: () => null));
} }
} }

View file

@ -37,6 +37,7 @@ class AIcons {
static const IconData favouriteActive = Icons.favorite; static const IconData favouriteActive = Icons.favorite;
static const IconData goUp = Icons.arrow_upward_outlined; static const IconData goUp = Icons.arrow_upward_outlined;
static const IconData group = Icons.group_work_outlined; static const IconData group = Icons.group_work_outlined;
static const IconData hide = Icons.visibility_off_outlined;
static const IconData info = Icons.info_outlined; static const IconData info = Icons.info_outlined;
static const IconData layers = Icons.layers_outlined; static const IconData layers = Icons.layers_outlined;
static const IconData openOutside = Icons.open_in_new_outlined; static const IconData openOutside = Icons.open_in_new_outlined;

View file

@ -42,7 +42,12 @@ class AndroidFileUtils {
bool isDownloadPath(String path) => path == downloadPath; bool isDownloadPath(String path) => path == downloadPath;
StorageVolume getStorageVolume(String path) => storageVolumes.firstWhere((v) => path.startsWith(v.path), orElse: () => null); StorageVolume getStorageVolume(String path) {
final volume = storageVolumes.firstWhere((v) => path.startsWith(v.path), orElse: () => null);
// storage volume path includes trailing '/', but argument path may or may not,
// which is an issue when the path is at the root
return volume != null || path.endsWith('/') ? volume : getStorageVolume('$path/');
}
bool isOnRemovableStorage(String path) => getStorageVolume(path)?.isRemovable ?? false; bool isOnRemovableStorage(String path) => getStorageVolume(path)?.isRemovable ?? false;

View file

@ -194,6 +194,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
return PopupMenuButton<CollectionAction>( return PopupMenuButton<CollectionAction>(
key: Key('appbar-menu-button'), key: Key('appbar-menu-button'),
itemBuilder: (context) { itemBuilder: (context) {
final isNotEmpty = !collection.isEmpty;
final hasSelection = collection.selection.isNotEmpty; final hasSelection = collection.selection.isNotEmpty;
return [ return [
PopupMenuItem( PopupMenuItem(
@ -216,10 +217,12 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
if (AvesApp.mode == AppMode.main) if (AvesApp.mode == AppMode.main)
PopupMenuItem( PopupMenuItem(
value: CollectionAction.select, value: CollectionAction.select,
enabled: isNotEmpty,
child: MenuRow(text: 'Select', icon: AIcons.select), child: MenuRow(text: 'Select', icon: AIcons.select),
), ),
PopupMenuItem( PopupMenuItem(
value: CollectionAction.stats, value: CollectionAction.stats,
enabled: isNotEmpty,
child: MenuRow(text: 'Stats', icon: AIcons.stats), child: MenuRow(text: 'Stats', icon: AIcons.stats),
), ),
if (AvesApp.mode == AppMode.main && canAddShortcuts) if (AvesApp.mode == AppMode.main && canAddShortcuts)
@ -248,6 +251,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
PopupMenuDivider(), PopupMenuDivider(),
PopupMenuItem( PopupMenuItem(
value: CollectionAction.selectAll, value: CollectionAction.selectAll,
enabled: collection.selection.length < collection.entryCount,
child: MenuRow(text: 'Select all'), child: MenuRow(text: 'Select all'),
), ),
PopupMenuItem( PopupMenuItem(

View file

@ -146,15 +146,13 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
opStream: ImageFileService.delete(selection), opStream: ImageFileService.delete(selection),
itemCount: selectionCount, itemCount: selectionCount,
onDone: (processed) { onDone: (processed) {
final deletedUris = processed.where((e) => e.success).map((e) => e.uri).toList(); final deletedUris = processed.where((event) => event.success).map((event) => event.uri).toSet();
final deletedCount = deletedUris.length; final deletedCount = deletedUris.length;
if (deletedCount < selectionCount) { if (deletedCount < selectionCount) {
final count = selectionCount - deletedCount; final count = selectionCount - deletedCount;
showFeedback(context, 'Failed to delete ${Intl.plural(count, one: '$count item', other: '$count items')}'); showFeedback(context, 'Failed to delete ${Intl.plural(count, one: '$count item', other: '$count items')}');
} }
if (deletedCount > 0) { source.removeEntries(deletedUris);
source.removeEntries(selection.where((e) => deletedUris.contains(e.uri)).toSet());
}
collection.clearSelection(); collection.clearSelection();
collection.browse(); collection.browse();
}, },

View file

@ -27,7 +27,9 @@ class AppDebugPage extends StatefulWidget {
} }
class _AppDebugPageState extends State<AppDebugPage> { class _AppDebugPageState extends State<AppDebugPage> {
List<AvesEntry> get entries => widget.source.rawEntries; CollectionSource get source => widget.source;
Set<AvesEntry> get visibleEntries => source.visibleEntries;
static OverlayEntry _taskQueueOverlayEntry; static OverlayEntry _taskQueueOverlayEntry;
@ -59,7 +61,7 @@ class _AppDebugPageState extends State<AppDebugPage> {
} }
Widget _buildGeneralTabView() { Widget _buildGeneralTabView() {
final catalogued = entries.where((entry) => entry.isCatalogued); final catalogued = visibleEntries.where((entry) => entry.isCatalogued);
final withGps = catalogued.where((entry) => entry.hasGps); final withGps = catalogued.where((entry) => entry.hasGps);
final located = withGps.where((entry) => entry.isLocated); final located = withGps.where((entry) => entry.isLocated);
return AvesExpansionTile( return AvesExpansionTile(
@ -98,7 +100,8 @@ class _AppDebugPageState extends State<AppDebugPage> {
padding: EdgeInsets.only(left: 8, right: 8, bottom: 8), padding: EdgeInsets.only(left: 8, right: 8, bottom: 8),
child: InfoRowGroup( child: InfoRowGroup(
{ {
'Entries': '${entries.length}', 'All entries': '${source.allEntries.length}',
'Visible entries': '${visibleEntries.length}',
'Catalogued': '${catalogued.length}', 'Catalogued': '${catalogued.length}',
'With GPS': '${withGps.length}', 'With GPS': '${withGps.length}',
'With address': '${located.length}', 'With address': '${located.length}',

View file

@ -44,6 +44,7 @@ class AlbumListPage extends StatelessWidget {
settings.pinnedFilters.contains(filter) ? ChipAction.unpin : ChipAction.pin, settings.pinnedFilters.contains(filter) ? ChipAction.unpin : ChipAction.pin,
ChipAction.rename, ChipAction.rename,
ChipAction.delete, ChipAction.delete,
ChipAction.hide,
], ],
filterSections: getAlbumEntries(source), filterSections: getAlbumEntries(source),
emptyBuilder: () => EmptyContent( emptyBuilder: () => EmptyContent(

View file

@ -16,6 +16,12 @@ import 'package:intl/intl.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
class ChipActionDelegate { class ChipActionDelegate {
final CollectionSource source;
ChipActionDelegate({
@required this.source,
});
void onActionSelected(BuildContext context, CollectionFilter filter, ChipAction action) { void onActionSelected(BuildContext context, CollectionFilter filter, ChipAction action) {
switch (action) { switch (action) {
case ChipAction.pin: case ChipAction.pin:
@ -24,6 +30,9 @@ class ChipActionDelegate {
case ChipAction.unpin: case ChipAction.unpin:
settings.pinnedFilters = settings.pinnedFilters..remove(filter); settings.pinnedFilters = settings.pinnedFilters..remove(filter);
break; break;
case ChipAction.hide:
source.changeFilterVisibility(filter, false);
break;
default: default:
break; break;
} }
@ -31,11 +40,9 @@ class ChipActionDelegate {
} }
class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
final CollectionSource source;
AlbumChipActionDelegate({ AlbumChipActionDelegate({
@required this.source, @required CollectionSource source,
}); }) : super(source: source);
@override @override
void onActionSelected(BuildContext context, CollectionFilter filter, ChipAction action) { void onActionSelected(BuildContext context, CollectionFilter filter, ChipAction action) {
@ -53,7 +60,7 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per
} }
Future<void> _showDeleteDialog(BuildContext context, AlbumFilter filter) async { Future<void> _showDeleteDialog(BuildContext context, AlbumFilter filter) async {
final selection = source.rawEntries.where(filter.filter).toSet(); final selection = source.visibleEntries.where(filter.filter).toSet();
final count = selection.length; final count = selection.length;
final confirmed = await showDialog<bool>( final confirmed = await showDialog<bool>(
@ -85,15 +92,13 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per
opStream: ImageFileService.delete(selection), opStream: ImageFileService.delete(selection),
itemCount: selectionCount, itemCount: selectionCount,
onDone: (processed) { onDone: (processed) {
final deletedUris = processed.where((e) => e.success).map((e) => e.uri).toList(); final deletedUris = processed.where((event) => event.success).map((event) => event.uri).toSet();
final deletedCount = deletedUris.length; final deletedCount = deletedUris.length;
if (deletedCount < selectionCount) { if (deletedCount < selectionCount) {
final count = selectionCount - deletedCount; final count = selectionCount - deletedCount;
showFeedback(context, 'Failed to delete ${Intl.plural(count, one: '$count item', other: '$count items')}'); showFeedback(context, 'Failed to delete ${Intl.plural(count, one: '$count item', other: '$count items')}');
} }
if (deletedCount > 0) { source.removeEntries(deletedUris);
source.removeEntries(selection.where((e) => deletedUris.contains(e.uri)).toSet());
}
}, },
); );
} }
@ -108,7 +113,7 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per
if (!await checkStoragePermissionForAlbums(context, {album})) return; if (!await checkStoragePermissionForAlbums(context, {album})) return;
final todoEntries = source.rawEntries.where(filter.filter).toSet(); final todoEntries = source.visibleEntries.where(filter.filter).toSet();
final destinationAlbum = path.join(path.dirname(album), newName); final destinationAlbum = path.join(path.dirname(album), newName);
if (!await checkFreeSpaceForMove(context, todoEntries, destinationAlbum, MoveType.move)) return; if (!await checkFreeSpaceForMove(context, todoEntries, destinationAlbum, MoveType.move)) return;

View file

@ -151,7 +151,7 @@ class FilterNavigationPage<T extends CollectionFilter> extends StatelessWidget {
} }
static int compareFiltersByDate(FilterGridItem<CollectionFilter> a, FilterGridItem<CollectionFilter> b) { static int compareFiltersByDate(FilterGridItem<CollectionFilter> a, FilterGridItem<CollectionFilter> b) {
final c = b.entry.bestDate?.compareTo(a.entry.bestDate) ?? -1; final c = (b.entry?.bestDate ?? DateTime.fromMillisecondsSinceEpoch(0)).compareTo(a.entry?.bestDate ?? DateTime.fromMillisecondsSinceEpoch(0));
return c != 0 ? c : a.filter.compareTo(b.filter); return c != 0 ? c : a.filter.compareTo(b.filter);
} }

View file

@ -75,8 +75,8 @@ extension ExtraAlbumImportance on AlbumImportance {
class StorageVolumeSectionKey extends ChipSectionKey { class StorageVolumeSectionKey extends ChipSectionKey {
final StorageVolume volume; final StorageVolume volume;
StorageVolumeSectionKey(this.volume) : super(title: volume.description); StorageVolumeSectionKey(this.volume) : super(title: volume?.description ?? 'Unknown');
@override @override
Widget get leading => volume.isRemovable ? Icon(AIcons.removableStorage) : null; Widget get leading => (volume?.isRemovable ?? false) ? Icon(AIcons.removableStorage) : null;
} }

View file

@ -34,9 +34,10 @@ class CountryListPage extends StatelessWidget {
source: source, source: source,
title: 'Countries', title: 'Countries',
chipSetActionDelegate: CountryChipSetActionDelegate(source: source), chipSetActionDelegate: CountryChipSetActionDelegate(source: source),
chipActionDelegate: ChipActionDelegate(), chipActionDelegate: ChipActionDelegate(source: source),
chipActionsBuilder: (filter) => [ chipActionsBuilder: (filter) => [
settings.pinnedFilters.contains(filter) ? ChipAction.unpin : ChipAction.pin, settings.pinnedFilters.contains(filter) ? ChipAction.unpin : ChipAction.pin,
ChipAction.hide,
], ],
filterSections: _getCountryEntries(), filterSections: _getCountryEntries(),
emptyBuilder: () => EmptyContent( emptyBuilder: () => EmptyContent(

View file

@ -34,9 +34,10 @@ class TagListPage extends StatelessWidget {
source: source, source: source,
title: 'Tags', title: 'Tags',
chipSetActionDelegate: TagChipSetActionDelegate(source: source), chipSetActionDelegate: TagChipSetActionDelegate(source: source),
chipActionDelegate: ChipActionDelegate(), chipActionDelegate: ChipActionDelegate(source: source),
chipActionsBuilder: (filter) => [ chipActionsBuilder: (filter) => [
settings.pinnedFilters.contains(filter) ? ChipAction.unpin : ChipAction.pin, settings.pinnedFilters.contains(filter) ? ChipAction.unpin : ChipAction.pin,
ChipAction.hide,
], ],
filterSections: _getTagEntries(), filterSections: _getTagEntries(),
emptyBuilder: () => EmptyContent( emptyBuilder: () => EmptyContent(

View file

@ -15,7 +15,7 @@ class ExpandableFilterRow extends StatelessWidget {
const ExpandableFilterRow({ const ExpandableFilterRow({
this.title, this.title,
@required this.filters, @required this.filters,
this.expandedNotifier, @required this.expandedNotifier,
this.heroTypeBuilder, this.heroTypeBuilder,
@required this.onTap, @required this.onTap,
}); });
@ -29,7 +29,7 @@ class ExpandableFilterRow extends StatelessWidget {
final hasTitle = title != null && title.isNotEmpty; final hasTitle = title != null && title.isNotEmpty;
final isExpanded = hasTitle && expandedNotifier?.value == title; final isExpanded = hasTitle && expandedNotifier.value == title;
Widget titleRow; Widget titleRow;
if (hasTitle) { if (hasTitle) {
@ -52,7 +52,7 @@ class ExpandableFilterRow extends StatelessWidget {
); );
} }
final filtersList = filters.toList(); final filterList = filters.toList();
final wrap = Container( final wrap = Container(
key: ValueKey('wrap$title'), key: ValueKey('wrap$title'),
padding: EdgeInsets.symmetric(horizontal: horizontalPadding), padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
@ -62,7 +62,7 @@ class ExpandableFilterRow extends StatelessWidget {
child: Wrap( child: Wrap(
spacing: horizontalPadding, spacing: horizontalPadding,
runSpacing: verticalPadding, runSpacing: verticalPadding,
children: filtersList.map(_buildFilterChip).toList(), children: filterList.map(_buildFilterChip).toList(),
), ),
); );
final list = Container( final list = Container(
@ -76,10 +76,10 @@ class ExpandableFilterRow extends StatelessWidget {
physics: BouncingScrollPhysics(), physics: BouncingScrollPhysics(),
padding: EdgeInsets.symmetric(horizontal: horizontalPadding), padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
itemBuilder: (context, index) { itemBuilder: (context, index) {
return index < filtersList.length ? _buildFilterChip(filtersList[index]) : null; return index < filterList.length ? _buildFilterChip(filterList[index]) : null;
}, },
separatorBuilder: (context, index) => SizedBox(width: 8), separatorBuilder: (context, index) => SizedBox(width: 8),
itemCount: filtersList.length, itemCount: filterList.length,
), ),
); );
final filterChips = isExpanded ? wrap : list; final filterChips = isExpanded ? wrap : list;

View file

@ -0,0 +1,67 @@
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class HiddenFilters extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Selector<Settings, Set<CollectionFilter>>(
selector: (context, s) => s.hiddenFilters,
builder: (context, hiddenFilters, child) {
return ListTile(
title: hiddenFilters.isEmpty ? Text('There are no hidden filters') : Text('Hidden filters'),
trailing: hiddenFilters.isEmpty
? null
: OutlinedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
settings: RouteSettings(name: HiddenFilterPage.routeName),
builder: (context) => HiddenFilterPage(),
),
);
},
child: Text('Edit'.toUpperCase()),
),
);
});
}
}
class HiddenFilterPage extends StatelessWidget {
static const routeName = '/settings/hidden';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Hidden Filters'),
),
body: SafeArea(
child: Padding(
padding: EdgeInsets.all(8),
child: Consumer<Settings>(
builder: (context, settings, child) {
final filterList = settings.hiddenFilters.toList()..sort();
return Wrap(
spacing: 8,
runSpacing: 8,
children: filterList
.map((filter) => AvesFilterChip(
filter: filter,
removable: true,
onTap: (filter) => context.read<CollectionSource>().changeFilterVisibility(filter, true),
))
.toList(),
);
},
),
),
),
);
}
}

View file

@ -9,6 +9,7 @@ import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
import 'package:aves/widgets/settings/access_grants.dart'; import 'package:aves/widgets/settings/access_grants.dart';
import 'package:aves/widgets/settings/entry_background.dart'; import 'package:aves/widgets/settings/entry_background.dart';
import 'package:aves/widgets/settings/hidden_filters.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -241,6 +242,7 @@ class _SettingsPageState extends State<SettingsPage> {
onChanged: (v) => settings.isCrashlyticsEnabled = v, onChanged: (v) => settings.isCrashlyticsEnabled = v,
title: Text('Allow anonymous analytics and crash reporting'), title: Text('Allow anonymous analytics and crash reporting'),
), ),
HiddenFilters(),
Padding( Padding(
padding: EdgeInsets.only(top: 8, bottom: 16), padding: EdgeInsets.only(top: 8, bottom: 16),
child: GrantedDirectories(), child: GrantedDirectories(),

View file

@ -29,7 +29,7 @@ class StatsPage extends StatelessWidget {
final CollectionLens parentCollection; final CollectionLens parentCollection;
final Map<String, int> entryCountPerCountry = {}, entryCountPerPlace = {}, entryCountPerTag = {}; final Map<String, int> entryCountPerCountry = {}, entryCountPerPlace = {}, entryCountPerTag = {};
List<AvesEntry> get entries => parentCollection?.sortedEntries ?? source.rawEntries; Set<AvesEntry> get entries => parentCollection?.sortedEntries?.toSet() ?? source.visibleEntries;
static const mimeDonutMinWidth = 124.0; static const mimeDonutMinWidth = 124.0;

View file

@ -141,7 +141,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
showFeedback(context, 'Failed'); showFeedback(context, 'Failed');
} else { } else {
if (hasCollection) { if (hasCollection) {
collection.source.removeEntries({entry}); collection.source.removeEntries({entry.uri});
} }
EntryDeletedNotification(entry).dispatch(context); EntryDeletedNotification(entry).dispatch(context);
} }