#13 hidden filters
This commit is contained in:
parent
c5ee55adb0
commit
ea3d79afbe
25 changed files with 260 additions and 107 deletions
|
@ -10,6 +10,7 @@ enum ChipSetAction {
|
|||
|
||||
enum ChipAction {
|
||||
delete,
|
||||
hide,
|
||||
pin,
|
||||
unpin,
|
||||
rename,
|
||||
|
@ -20,6 +21,8 @@ extension ExtraChipAction on ChipAction {
|
|||
switch (this) {
|
||||
case ChipAction.delete:
|
||||
return 'Delete';
|
||||
case ChipAction.hide:
|
||||
return 'Hide';
|
||||
case ChipAction.pin:
|
||||
return 'Pin to top';
|
||||
case ChipAction.unpin:
|
||||
|
@ -34,6 +37,8 @@ extension ExtraChipAction on ChipAction {
|
|||
switch (this) {
|
||||
case ChipAction.delete:
|
||||
return AIcons.delete;
|
||||
case ChipAction.hide:
|
||||
return AIcons.hide;
|
||||
case ChipAction.pin:
|
||||
case ChipAction.unpin:
|
||||
return AIcons.pin;
|
||||
|
|
|
@ -172,14 +172,8 @@ class AvesEntry {
|
|||
addressChangeNotifier.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
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);
|
||||
// do not implement [Object.==] and [Object.hashCode] using mutable attributes (e.g. `uri`)
|
||||
// so that we can reliably use instances in a `Set`, which requires consistent hash codes over time
|
||||
|
||||
@override
|
||||
String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, path=$path, pageId=$pageId}';
|
||||
|
|
|
@ -43,6 +43,7 @@ class Settings extends ChangeNotifier {
|
|||
static const countrySortFactorKey = 'country_sort_factor';
|
||||
static const tagSortFactorKey = 'tag_sort_factor';
|
||||
static const pinnedFiltersKey = 'pinned_filters';
|
||||
static const hiddenFiltersKey = 'hidden_filters';
|
||||
|
||||
// viewer
|
||||
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<CollectionFilter> get hiddenFilters => (_prefs.getStringList(hiddenFiltersKey) ?? []).map(CollectionFilter.fromJson).toSet();
|
||||
|
||||
set hiddenFilters(Set<CollectionFilter> newValue) => setAndNotify(hiddenFiltersKey, newValue.map((filter) => filter.toJson()).toList());
|
||||
|
||||
// viewer
|
||||
|
||||
bool get showOverlayMinimap => getBoolOrDefault(showOverlayMinimapKey, false);
|
||||
|
|
|
@ -34,9 +34,18 @@ mixin AlbumMixin on SourceBase {
|
|||
final uniqueName = parts.skip(parts.length - partCount).join(separator);
|
||||
|
||||
final volume = androidFileUtils.getStorageVolume(album);
|
||||
final volumeRoot = volume?.path ?? '';
|
||||
final albumRelativePath = album.substring(volumeRoot.length);
|
||||
if (uniqueName.length < albumRelativePath.length || volume == null) {
|
||||
if (volume == null) {
|
||||
return uniqueName;
|
||||
}
|
||||
|
||||
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;
|
||||
} else if (volume.isPrimary) {
|
||||
return albumRelativePath;
|
||||
|
@ -67,9 +76,17 @@ mixin AlbumMixin on SourceBase {
|
|||
)));
|
||||
}
|
||||
|
||||
void addDirectory(Iterable<String> albums) {
|
||||
_directories.addAll(albums);
|
||||
_notifyAlbumChange();
|
||||
void updateDirectories() {
|
||||
final visibleDirectories = visibleEntries.map((entry) => entry.directory).toSet();
|
||||
addDirectories(visibleDirectories);
|
||||
cleanEmptyAlbums();
|
||||
}
|
||||
|
||||
void addDirectories(Set<String> albums) {
|
||||
if (!_directories.containsAll(albums)) {
|
||||
_directories.addAll(albums);
|
||||
_notifyAlbumChange();
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
|
@ -105,11 +122,11 @@ mixin AlbumMixin on SourceBase {
|
|||
}
|
||||
|
||||
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) {
|
||||
return _filterRecentEntryMap.putIfAbsent(filter.album, () => sortedEntriesByDate.firstWhere((entry) => filter.filter(entry)));
|
||||
return _filterRecentEntryMap.putIfAbsent(filter.album, () => sortedEntriesByDate.firstWhere((entry) => filter.filter(entry), orElse: () => null));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
|
|||
int id;
|
||||
bool listenToSource;
|
||||
|
||||
List<AvesEntry> _filteredEntries;
|
||||
List<AvesEntry> _filteredSortedEntries;
|
||||
List<StreamSubscription> _subscriptions = [];
|
||||
|
||||
Map<SectionKey, List<AvesEntry>> sections = Map.unmodifiable({});
|
||||
|
@ -64,9 +64,9 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
|
|||
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
|
||||
List<AvesEntry> _sortedEntries;
|
||||
|
@ -122,20 +122,20 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
|
|||
}
|
||||
|
||||
void _applyFilters() {
|
||||
final rawEntries = source.rawEntries;
|
||||
_filteredEntries = List.of(filters.isEmpty ? rawEntries : rawEntries.where((entry) => filters.fold(true, (prev, filter) => prev && filter.filter(entry))));
|
||||
final entries = source.visibleEntries;
|
||||
_filteredSortedEntries = List.of(filters.isEmpty ? entries : entries.where((entry) => filters.fold(true, (prev, filter) => prev && filter.filter(entry))));
|
||||
}
|
||||
|
||||
void _applySort() {
|
||||
switch (sortFactor) {
|
||||
case EntrySortFactor.date:
|
||||
_filteredEntries.sort(AvesEntry.compareByDate);
|
||||
_filteredSortedEntries.sort(AvesEntry.compareByDate);
|
||||
break;
|
||||
case EntrySortFactor.size:
|
||||
_filteredEntries.sort(AvesEntry.compareBySize);
|
||||
_filteredSortedEntries.sort(AvesEntry.compareBySize);
|
||||
break;
|
||||
case EntrySortFactor.name:
|
||||
_filteredEntries.sort(AvesEntry.compareByName);
|
||||
_filteredSortedEntries.sort(AvesEntry.compareByName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -145,28 +145,28 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
|
|||
case EntrySortFactor.date:
|
||||
switch (groupFactor) {
|
||||
case EntryGroupFactor.album:
|
||||
sections = groupBy<AvesEntry, EntryAlbumSectionKey>(_filteredEntries, (entry) => EntryAlbumSectionKey(entry.directory));
|
||||
sections = groupBy<AvesEntry, EntryAlbumSectionKey>(_filteredSortedEntries, (entry) => EntryAlbumSectionKey(entry.directory));
|
||||
break;
|
||||
case EntryGroupFactor.month:
|
||||
sections = groupBy<AvesEntry, EntryDateSectionKey>(_filteredEntries, (entry) => EntryDateSectionKey(entry.monthTaken));
|
||||
sections = groupBy<AvesEntry, EntryDateSectionKey>(_filteredSortedEntries, (entry) => EntryDateSectionKey(entry.monthTaken));
|
||||
break;
|
||||
case EntryGroupFactor.day:
|
||||
sections = groupBy<AvesEntry, EntryDateSectionKey>(_filteredEntries, (entry) => EntryDateSectionKey(entry.dayTaken));
|
||||
sections = groupBy<AvesEntry, EntryDateSectionKey>(_filteredSortedEntries, (entry) => EntryDateSectionKey(entry.dayTaken));
|
||||
break;
|
||||
case EntryGroupFactor.none:
|
||||
sections = Map.fromEntries([
|
||||
MapEntry(null, _filteredEntries),
|
||||
MapEntry(null, _filteredSortedEntries),
|
||||
]);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case EntrySortFactor.size:
|
||||
sections = Map.fromEntries([
|
||||
MapEntry(null, _filteredEntries),
|
||||
MapEntry(null, _filteredSortedEntries),
|
||||
]);
|
||||
break;
|
||||
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));
|
||||
break;
|
||||
}
|
||||
|
@ -191,7 +191,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
|
|||
// we should remove obsolete entries and sections
|
||||
// but do not apply sort/group
|
||||
// as section order change would surprise the user while browsing
|
||||
_filteredEntries.removeWhere(entries.contains);
|
||||
_filteredSortedEntries.removeWhere(entries.contains);
|
||||
_sortedEntries?.removeWhere(entries.contains);
|
||||
sections.forEach((key, sectionEntries) => sectionEntries.removeWhere(entries.contains));
|
||||
sections = Map.unmodifiable(Map.fromEntries(sections.entries.where((kv) => kv.value.isNotEmpty)));
|
||||
|
|
|
@ -8,6 +8,7 @@ import 'package:aves/model/filters/location.dart';
|
|||
import 'package:aves/model/filters/tag.dart';
|
||||
import 'package:aves/model/metadata.dart';
|
||||
import 'package:aves/model/metadata_db.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/album.dart';
|
||||
import 'package:aves/model/source/location.dart';
|
||||
import 'package:aves/model/source/tag.dart';
|
||||
|
@ -16,13 +17,9 @@ import 'package:event_bus/event_bus.dart';
|
|||
import 'package:flutter/foundation.dart';
|
||||
|
||||
mixin SourceBase {
|
||||
final List<AvesEntry> _rawEntries = [];
|
||||
EventBus get eventBus;
|
||||
|
||||
List<AvesEntry> get rawEntries => List.unmodifiable(_rawEntries);
|
||||
|
||||
final EventBus _eventBus = EventBus();
|
||||
|
||||
EventBus get eventBus => _eventBus;
|
||||
Set<AvesEntry> get visibleEntries;
|
||||
|
||||
List<AvesEntry> get sortedEntriesByDate;
|
||||
|
||||
|
@ -34,11 +31,30 @@ mixin SourceBase {
|
|||
}
|
||||
|
||||
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;
|
||||
|
||||
@override
|
||||
List<AvesEntry> get sortedEntriesByDate {
|
||||
_sortedEntriesByDate ??= List.of(_rawEntries)..sort(AvesEntry.compareByDate);
|
||||
_sortedEntriesByDate ??= List.unmodifiable(visibleEntries.toList()..sort(AvesEntry.compareByDate));
|
||||
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');
|
||||
}
|
||||
|
||||
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 (_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));
|
||||
}
|
||||
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;
|
||||
});
|
||||
_rawEntries.addAll(entries);
|
||||
addDirectory(_rawEntries.map((entry) => entry.directory));
|
||||
_invalidateFilterSummaries(entries);
|
||||
_invalidate(entries);
|
||||
|
||||
addDirectories(_applyHiddenFilters(entries).map((entry) => entry.directory).toSet());
|
||||
eventBus.fire(EntryAddedEvent(entries));
|
||||
}
|
||||
|
||||
void removeEntries(Set<AvesEntry> entries) {
|
||||
if (entries.isEmpty) return;
|
||||
void removeEntries(Set<String> uris) {
|
||||
if (uris.isEmpty) return;
|
||||
final entries = _rawEntries.where((entry) => uris.contains(entry.uri)).toSet();
|
||||
entries.forEach((entry) => entry.removeFromFavourites());
|
||||
_rawEntries.removeWhere(entries.contains);
|
||||
_rawEntries.removeAll(entries);
|
||||
_invalidate(entries);
|
||||
|
||||
cleanEmptyAlbums(entries.map((entry) => entry.directory).toSet());
|
||||
updateLocations();
|
||||
updateTags();
|
||||
_invalidateFilterSummaries(entries);
|
||||
eventBus.fire(EntryRemovedEvent(entries));
|
||||
}
|
||||
|
||||
void clearEntries() {
|
||||
_rawEntries.clear();
|
||||
cleanEmptyAlbums();
|
||||
_invalidate();
|
||||
|
||||
updateDirectories();
|
||||
updateLocations();
|
||||
updateTags();
|
||||
_invalidateFilterSummaries();
|
||||
}
|
||||
|
||||
Future<void> _moveEntry(AvesEntry entry, Map newFields, bool isFavourite) async {
|
||||
|
@ -154,13 +187,13 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
|||
}
|
||||
|
||||
if (copy) {
|
||||
addAll(movedEntries);
|
||||
addEntries(movedEntries);
|
||||
} else {
|
||||
cleanEmptyAlbums(fromAlbums);
|
||||
addDirectory({destinationAlbum});
|
||||
addDirectories({destinationAlbum});
|
||||
}
|
||||
invalidateAlbumFilterSummary(directories: fromAlbums);
|
||||
_invalidateFilterSummaries(movedEntries);
|
||||
_invalidate(movedEntries);
|
||||
eventBus.fire(EntryMovedEvent(movedEntries));
|
||||
}
|
||||
|
||||
|
@ -174,13 +207,6 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
|||
|
||||
// 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);
|
||||
|
@ -194,6 +220,24 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
|||
if (filter is TagFilter) return tagRecentEntry(filter);
|
||||
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 }
|
||||
|
|
|
@ -19,7 +19,7 @@ mixin LocationMixin on SourceBase {
|
|||
Future<void> loadAddresses() async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
final saved = await metadataDb.loadAddresses();
|
||||
rawEntries.forEach((entry) {
|
||||
visibleEntries.forEach((entry) {
|
||||
final contentId = entry.contentId;
|
||||
entry.addressDetails = saved.firstWhere((address) => address.contentId == contentId, orElse: () => null);
|
||||
});
|
||||
|
@ -31,7 +31,7 @@ mixin LocationMixin on SourceBase {
|
|||
if (!(await availability.canGeolocate)) return;
|
||||
|
||||
// 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] ?? [];
|
||||
if (todo.isEmpty) return;
|
||||
|
||||
|
@ -91,7 +91,7 @@ mixin LocationMixin on SourceBase {
|
|||
}
|
||||
|
||||
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));
|
||||
|
||||
// the same country code could be found with different country names
|
||||
|
@ -121,11 +121,11 @@ mixin LocationMixin on SourceBase {
|
|||
}
|
||||
|
||||
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) {
|
||||
return _filterRecentEntryMap.putIfAbsent(filter.countryCode, () => sortedEntriesByDate.firstWhere((entry) => filter.filter(entry)));
|
||||
return _filterRecentEntryMap.putIfAbsent(filter.countryCode, () => sortedEntriesByDate.firstWhere((entry) => filter.filter(entry), orElse: () => null));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -54,7 +54,7 @@ class MediaStoreSource extends CollectionSource {
|
|||
oldEntries.removeWhere((entry) => obsoleteContentIds.contains(entry.contentId));
|
||||
|
||||
// show known entries
|
||||
addAll(oldEntries);
|
||||
addEntries(oldEntries);
|
||||
await loadCatalogMetadata(); // 600ms for 5500 entries
|
||||
await loadAddresses(); // 200ms for 3000 entries
|
||||
debugPrint('$runtimeType refresh loaded ${oldEntries.length} known entries, elapsed=${stopwatch.elapsed}');
|
||||
|
@ -69,7 +69,7 @@ class MediaStoreSource extends CollectionSource {
|
|||
final allNewEntries = <AvesEntry>{}, pendingNewEntries = <AvesEntry>{};
|
||||
void addPendingEntries() {
|
||||
allNewEntries.addAll(pendingNewEntries);
|
||||
addAll(pendingNewEntries);
|
||||
addEntries(pendingNewEntries);
|
||||
pendingNewEntries.clear();
|
||||
}
|
||||
|
||||
|
@ -89,7 +89,7 @@ class MediaStoreSource extends CollectionSource {
|
|||
invalidateAlbumFilterSummary(entries: allNewEntries);
|
||||
|
||||
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()));
|
||||
|
||||
stateNotifier.value = SourceState.cataloguing;
|
||||
|
@ -124,9 +124,9 @@ class MediaStoreSource extends CollectionSource {
|
|||
|
||||
// clean up obsolete entries
|
||||
final obsoleteContentIds = (await ImageFileService.getObsoleteEntries(uriByContentId.keys.toList())).toSet();
|
||||
final obsoleteUris = obsoleteContentIds.map((contentId) => uriByContentId[contentId]).toSet();
|
||||
removeEntries(obsoleteUris);
|
||||
obsoleteContentIds.forEach(uriByContentId.remove);
|
||||
final obsoleteEntries = rawEntries.where((e) => obsoleteContentIds.contains(e.contentId)).toSet();
|
||||
removeEntries(obsoleteEntries);
|
||||
|
||||
// fetch new entries
|
||||
final newEntries = <AvesEntry>{};
|
||||
|
@ -135,7 +135,7 @@ class MediaStoreSource extends CollectionSource {
|
|||
final uri = kv.value;
|
||||
final sourceEntry = await ImageFileService.getEntry(uri, 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) {
|
||||
final volume = androidFileUtils.getStorageVolume(sourceEntry.path);
|
||||
if (volume != null) {
|
||||
|
@ -149,7 +149,7 @@ class MediaStoreSource extends CollectionSource {
|
|||
}
|
||||
|
||||
if (newEntries.isNotEmpty) {
|
||||
addAll(newEntries);
|
||||
addEntries(newEntries);
|
||||
await metadataDb.saveEntries(newEntries);
|
||||
invalidateAlbumFilterSummary(entries: newEntries);
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ mixin TagMixin on SourceBase {
|
|||
Future<void> loadCatalogMetadata() async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
final saved = await metadataDb.loadMetadataEntries();
|
||||
rawEntries.forEach((entry) {
|
||||
visibleEntries.forEach((entry) {
|
||||
final contentId = entry.contentId;
|
||||
entry.catalogMetadata = saved.firstWhere((metadata) => metadata.contentId == contentId, orElse: () => null);
|
||||
});
|
||||
|
@ -24,7 +24,7 @@ mixin TagMixin on SourceBase {
|
|||
|
||||
Future<void> catalogEntries() async {
|
||||
// final stopwatch = Stopwatch()..start();
|
||||
final todo = rawEntries.where((entry) => !entry.isCatalogued).toList();
|
||||
final todo = visibleEntries.where((entry) => !entry.isCatalogued).toList();
|
||||
if (todo.isEmpty) return;
|
||||
|
||||
var progressDone = 0;
|
||||
|
@ -55,7 +55,7 @@ mixin TagMixin on SourceBase {
|
|||
}
|
||||
|
||||
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);
|
||||
|
||||
invalidateTagFilterSummary();
|
||||
|
@ -79,11 +79,11 @@ mixin TagMixin on SourceBase {
|
|||
}
|
||||
|
||||
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) {
|
||||
return _filterRecentEntryMap.putIfAbsent(filter.tag, () => sortedEntriesByDate.firstWhere((entry) => filter.filter(entry)));
|
||||
return _filterRecentEntryMap.putIfAbsent(filter.tag, () => sortedEntriesByDate.firstWhere((entry) => filter.filter(entry), orElse: () => null));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ class AIcons {
|
|||
static const IconData favouriteActive = Icons.favorite;
|
||||
static const IconData goUp = Icons.arrow_upward_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 layers = Icons.layers_outlined;
|
||||
static const IconData openOutside = Icons.open_in_new_outlined;
|
||||
|
|
|
@ -42,7 +42,12 @@ class AndroidFileUtils {
|
|||
|
||||
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;
|
||||
|
||||
|
|
|
@ -194,6 +194,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
return PopupMenuButton<CollectionAction>(
|
||||
key: Key('appbar-menu-button'),
|
||||
itemBuilder: (context) {
|
||||
final isNotEmpty = !collection.isEmpty;
|
||||
final hasSelection = collection.selection.isNotEmpty;
|
||||
return [
|
||||
PopupMenuItem(
|
||||
|
@ -216,10 +217,12 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
if (AvesApp.mode == AppMode.main)
|
||||
PopupMenuItem(
|
||||
value: CollectionAction.select,
|
||||
enabled: isNotEmpty,
|
||||
child: MenuRow(text: 'Select', icon: AIcons.select),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: CollectionAction.stats,
|
||||
enabled: isNotEmpty,
|
||||
child: MenuRow(text: 'Stats', icon: AIcons.stats),
|
||||
),
|
||||
if (AvesApp.mode == AppMode.main && canAddShortcuts)
|
||||
|
@ -248,6 +251,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
PopupMenuDivider(),
|
||||
PopupMenuItem(
|
||||
value: CollectionAction.selectAll,
|
||||
enabled: collection.selection.length < collection.entryCount,
|
||||
child: MenuRow(text: 'Select all'),
|
||||
),
|
||||
PopupMenuItem(
|
||||
|
|
|
@ -146,15 +146,13 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
opStream: ImageFileService.delete(selection),
|
||||
itemCount: selectionCount,
|
||||
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;
|
||||
if (deletedCount < selectionCount) {
|
||||
final count = selectionCount - deletedCount;
|
||||
showFeedback(context, 'Failed to delete ${Intl.plural(count, one: '$count item', other: '$count items')}');
|
||||
}
|
||||
if (deletedCount > 0) {
|
||||
source.removeEntries(selection.where((e) => deletedUris.contains(e.uri)).toSet());
|
||||
}
|
||||
source.removeEntries(deletedUris);
|
||||
collection.clearSelection();
|
||||
collection.browse();
|
||||
},
|
||||
|
|
|
@ -27,7 +27,9 @@ class AppDebugPage extends StatefulWidget {
|
|||
}
|
||||
|
||||
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;
|
||||
|
||||
|
@ -59,7 +61,7 @@ class _AppDebugPageState extends State<AppDebugPage> {
|
|||
}
|
||||
|
||||
Widget _buildGeneralTabView() {
|
||||
final catalogued = entries.where((entry) => entry.isCatalogued);
|
||||
final catalogued = visibleEntries.where((entry) => entry.isCatalogued);
|
||||
final withGps = catalogued.where((entry) => entry.hasGps);
|
||||
final located = withGps.where((entry) => entry.isLocated);
|
||||
return AvesExpansionTile(
|
||||
|
@ -98,7 +100,8 @@ class _AppDebugPageState extends State<AppDebugPage> {
|
|||
padding: EdgeInsets.only(left: 8, right: 8, bottom: 8),
|
||||
child: InfoRowGroup(
|
||||
{
|
||||
'Entries': '${entries.length}',
|
||||
'All entries': '${source.allEntries.length}',
|
||||
'Visible entries': '${visibleEntries.length}',
|
||||
'Catalogued': '${catalogued.length}',
|
||||
'With GPS': '${withGps.length}',
|
||||
'With address': '${located.length}',
|
||||
|
|
|
@ -44,6 +44,7 @@ class AlbumListPage extends StatelessWidget {
|
|||
settings.pinnedFilters.contains(filter) ? ChipAction.unpin : ChipAction.pin,
|
||||
ChipAction.rename,
|
||||
ChipAction.delete,
|
||||
ChipAction.hide,
|
||||
],
|
||||
filterSections: getAlbumEntries(source),
|
||||
emptyBuilder: () => EmptyContent(
|
||||
|
|
|
@ -16,6 +16,12 @@ import 'package:intl/intl.dart';
|
|||
import 'package:path/path.dart' as path;
|
||||
|
||||
class ChipActionDelegate {
|
||||
final CollectionSource source;
|
||||
|
||||
ChipActionDelegate({
|
||||
@required this.source,
|
||||
});
|
||||
|
||||
void onActionSelected(BuildContext context, CollectionFilter filter, ChipAction action) {
|
||||
switch (action) {
|
||||
case ChipAction.pin:
|
||||
|
@ -24,6 +30,9 @@ class ChipActionDelegate {
|
|||
case ChipAction.unpin:
|
||||
settings.pinnedFilters = settings.pinnedFilters..remove(filter);
|
||||
break;
|
||||
case ChipAction.hide:
|
||||
source.changeFilterVisibility(filter, false);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -31,11 +40,9 @@ class ChipActionDelegate {
|
|||
}
|
||||
|
||||
class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
|
||||
final CollectionSource source;
|
||||
|
||||
AlbumChipActionDelegate({
|
||||
@required this.source,
|
||||
});
|
||||
@required CollectionSource source,
|
||||
}) : super(source: source);
|
||||
|
||||
@override
|
||||
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 {
|
||||
final selection = source.rawEntries.where(filter.filter).toSet();
|
||||
final selection = source.visibleEntries.where(filter.filter).toSet();
|
||||
final count = selection.length;
|
||||
|
||||
final confirmed = await showDialog<bool>(
|
||||
|
@ -85,15 +92,13 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per
|
|||
opStream: ImageFileService.delete(selection),
|
||||
itemCount: selectionCount,
|
||||
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;
|
||||
if (deletedCount < selectionCount) {
|
||||
final count = selectionCount - deletedCount;
|
||||
showFeedback(context, 'Failed to delete ${Intl.plural(count, one: '$count item', other: '$count items')}');
|
||||
}
|
||||
if (deletedCount > 0) {
|
||||
source.removeEntries(selection.where((e) => deletedUris.contains(e.uri)).toSet());
|
||||
}
|
||||
source.removeEntries(deletedUris);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -108,7 +113,7 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per
|
|||
|
||||
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);
|
||||
|
||||
if (!await checkFreeSpaceForMove(context, todoEntries, destinationAlbum, MoveType.move)) return;
|
||||
|
|
|
@ -151,7 +151,7 @@ class FilterNavigationPage<T extends CollectionFilter> extends StatelessWidget {
|
|||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -75,8 +75,8 @@ extension ExtraAlbumImportance on AlbumImportance {
|
|||
class StorageVolumeSectionKey extends ChipSectionKey {
|
||||
final StorageVolume volume;
|
||||
|
||||
StorageVolumeSectionKey(this.volume) : super(title: volume.description);
|
||||
StorageVolumeSectionKey(this.volume) : super(title: volume?.description ?? 'Unknown');
|
||||
|
||||
@override
|
||||
Widget get leading => volume.isRemovable ? Icon(AIcons.removableStorage) : null;
|
||||
Widget get leading => (volume?.isRemovable ?? false) ? Icon(AIcons.removableStorage) : null;
|
||||
}
|
||||
|
|
|
@ -34,9 +34,10 @@ class CountryListPage extends StatelessWidget {
|
|||
source: source,
|
||||
title: 'Countries',
|
||||
chipSetActionDelegate: CountryChipSetActionDelegate(source: source),
|
||||
chipActionDelegate: ChipActionDelegate(),
|
||||
chipActionDelegate: ChipActionDelegate(source: source),
|
||||
chipActionsBuilder: (filter) => [
|
||||
settings.pinnedFilters.contains(filter) ? ChipAction.unpin : ChipAction.pin,
|
||||
ChipAction.hide,
|
||||
],
|
||||
filterSections: _getCountryEntries(),
|
||||
emptyBuilder: () => EmptyContent(
|
||||
|
|
|
@ -34,9 +34,10 @@ class TagListPage extends StatelessWidget {
|
|||
source: source,
|
||||
title: 'Tags',
|
||||
chipSetActionDelegate: TagChipSetActionDelegate(source: source),
|
||||
chipActionDelegate: ChipActionDelegate(),
|
||||
chipActionDelegate: ChipActionDelegate(source: source),
|
||||
chipActionsBuilder: (filter) => [
|
||||
settings.pinnedFilters.contains(filter) ? ChipAction.unpin : ChipAction.pin,
|
||||
ChipAction.hide,
|
||||
],
|
||||
filterSections: _getTagEntries(),
|
||||
emptyBuilder: () => EmptyContent(
|
||||
|
|
|
@ -15,7 +15,7 @@ class ExpandableFilterRow extends StatelessWidget {
|
|||
const ExpandableFilterRow({
|
||||
this.title,
|
||||
@required this.filters,
|
||||
this.expandedNotifier,
|
||||
@required this.expandedNotifier,
|
||||
this.heroTypeBuilder,
|
||||
@required this.onTap,
|
||||
});
|
||||
|
@ -29,7 +29,7 @@ class ExpandableFilterRow extends StatelessWidget {
|
|||
|
||||
final hasTitle = title != null && title.isNotEmpty;
|
||||
|
||||
final isExpanded = hasTitle && expandedNotifier?.value == title;
|
||||
final isExpanded = hasTitle && expandedNotifier.value == title;
|
||||
|
||||
Widget titleRow;
|
||||
if (hasTitle) {
|
||||
|
@ -52,7 +52,7 @@ class ExpandableFilterRow extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
final filtersList = filters.toList();
|
||||
final filterList = filters.toList();
|
||||
final wrap = Container(
|
||||
key: ValueKey('wrap$title'),
|
||||
padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
|
||||
|
@ -62,7 +62,7 @@ class ExpandableFilterRow extends StatelessWidget {
|
|||
child: Wrap(
|
||||
spacing: horizontalPadding,
|
||||
runSpacing: verticalPadding,
|
||||
children: filtersList.map(_buildFilterChip).toList(),
|
||||
children: filterList.map(_buildFilterChip).toList(),
|
||||
),
|
||||
);
|
||||
final list = Container(
|
||||
|
@ -76,10 +76,10 @@ class ExpandableFilterRow extends StatelessWidget {
|
|||
physics: BouncingScrollPhysics(),
|
||||
padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
|
||||
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),
|
||||
itemCount: filtersList.length,
|
||||
itemCount: filterList.length,
|
||||
),
|
||||
);
|
||||
final filterChips = isExpanded ? wrap : list;
|
||||
|
|
67
lib/widgets/settings/hidden_filters.dart
Normal file
67
lib/widgets/settings/hidden_filters.dart
Normal 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(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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/settings/access_grants.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_staggered_animations/flutter_staggered_animations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
@ -241,6 +242,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||
onChanged: (v) => settings.isCrashlyticsEnabled = v,
|
||||
title: Text('Allow anonymous analytics and crash reporting'),
|
||||
),
|
||||
HiddenFilters(),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 8, bottom: 16),
|
||||
child: GrantedDirectories(),
|
||||
|
|
|
@ -29,7 +29,7 @@ class StatsPage extends StatelessWidget {
|
|||
final CollectionLens parentCollection;
|
||||
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;
|
||||
|
||||
|
|
|
@ -141,7 +141,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
showFeedback(context, 'Failed');
|
||||
} else {
|
||||
if (hasCollection) {
|
||||
collection.source.removeEntries({entry});
|
||||
collection.source.removeEntries({entry.uri});
|
||||
}
|
||||
EntryDeletedNotification(entry).dispatch(context);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue