import 'dart:async'; import 'dart:collection'; import 'package:aves/model/entry.dart'; import 'package:aves/model/favourites.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/location.dart'; import 'package:aves/model/filters/query.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/location.dart'; import 'package:aves/model/source/section_keys.dart'; import 'package:aves/model/source/tag.dart'; import 'package:aves/utils/change_notifier.dart'; import 'package:aves/utils/collection_utils.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'enums.dart'; class CollectionLens with ChangeNotifier { final CollectionSource source; final Set filters; EntryGroupFactor sectionFactor; EntrySortFactor sortFactor; final AChangeNotifier filterChangeNotifier = AChangeNotifier(), sortSectionChangeNotifier = AChangeNotifier(); final List _subscriptions = []; int? id; bool listenToSource; List? fixedSelection; List _filteredSortedEntries = []; Map> sections = Map.unmodifiable({}); CollectionLens({ required this.source, Iterable? filters, this.id, this.listenToSource = true, this.fixedSelection, }) : filters = (filters ?? {}).whereNotNull().toSet(), sectionFactor = settings.collectionSectionFactor, sortFactor = settings.collectionSortFactor { id ??= hashCode; if (listenToSource) { final sourceEvents = source.eventBus; _subscriptions.add(sourceEvents.on().listen((e) => _onEntryAdded(e.entries))); _subscriptions.add(sourceEvents.on().listen((e) => _onEntryRemoved(e.entries))); _subscriptions.add(sourceEvents.on().listen((e) => _refresh())); _subscriptions.add(sourceEvents.on().listen((e) => _refresh())); _subscriptions.add(sourceEvents.on().listen((e) => _refresh())); _subscriptions.add(sourceEvents.on().listen((e) => _refresh())); _subscriptions.add(sourceEvents.on().listen((e) { if (this.filters.any((filter) => filter is LocationFilter)) { _refresh(); } })); favourites.addListener(_onFavouritesChanged); } settings.addListener(_onSettingsChanged); _refresh(); } @override void dispose() { _subscriptions ..forEach((sub) => sub.cancel()) ..clear(); favourites.removeListener(_onFavouritesChanged); settings.removeListener(_onSettingsChanged); super.dispose(); } CollectionLens copyWith({ CollectionSource? source, Set? filters, bool? listenToSource, List? fixedSelection, }) => CollectionLens( source: source ?? this.source, filters: filters ?? this.filters, id: id, listenToSource: listenToSource ?? this.listenToSource, fixedSelection: fixedSelection ?? this.fixedSelection, ); bool get isEmpty => _filteredSortedEntries.isEmpty; int get entryCount => _filteredSortedEntries.length; // sorted as displayed to the user, i.e. sorted then sectioned, not an absolute order on all entries List? _sortedEntries; List get sortedEntries { _sortedEntries ??= List.of(sections.entries.expand((kv) => kv.value)); return _sortedEntries!; } bool get showHeaders { if (sortFactor == EntrySortFactor.size) return false; if (sortFactor == EntrySortFactor.date && sectionFactor == EntryGroupFactor.none) return false; final albumSections = sortFactor == EntrySortFactor.name || (sortFactor == EntrySortFactor.date && sectionFactor == EntryGroupFactor.album); final filterByAlbum = filters.any((f) => f is AlbumFilter); if (albumSections && filterByAlbum) return false; return true; } void addFilter(CollectionFilter filter) { if (filters.contains(filter)) return; if (filter.isUnique) { filters.removeWhere((old) => old.category == filter.category); } filters.add(filter); _onFilterChanged(); } void removeFilter(CollectionFilter filter) { if (!filters.contains(filter)) return; filters.remove(filter); _onFilterChanged(); } void setLiveQuery(String query) { filters.removeWhere((v) => v is QueryFilter && v.live); if (query.isNotEmpty) { filters.add(QueryFilter(query, live: true)); } _onFilterChanged(); } void _onFilterChanged() { _refresh(); filterChangeNotifier.notifyListeners(); } final bool groupBursts = true; void _applyFilters() { final entries = fixedSelection ?? source.visibleEntries; _filteredSortedEntries = List.of(filters.isEmpty ? entries : entries.where((entry) => filters.every((filter) => filter.test(entry)))); if (groupBursts) { _groupBursts(); } } void _groupBursts() { final byBurstKey = groupBy(_filteredSortedEntries, (entry) => entry.burstKey).whereNotNullKey(); byBurstKey.forEach((burstKey, entries) { if (entries.length > 1) { entries.sort(AvesEntry.compareByName); final mainEntry = entries.first; final burstEntry = mainEntry.copyWith(burstEntries: entries); entries.skip(1).toList().forEach((subEntry) { _filteredSortedEntries.remove(subEntry); }); final index = _filteredSortedEntries.indexOf(mainEntry); _filteredSortedEntries.removeAt(index); _filteredSortedEntries.insert(index, burstEntry); } }); } void _applySort() { switch (sortFactor) { case EntrySortFactor.date: _filteredSortedEntries.sort(AvesEntry.compareByDate); break; case EntrySortFactor.size: _filteredSortedEntries.sort(AvesEntry.compareBySize); break; case EntrySortFactor.name: _filteredSortedEntries.sort(AvesEntry.compareByName); break; } } void _applySection() { switch (sortFactor) { case EntrySortFactor.date: switch (sectionFactor) { case EntryGroupFactor.album: sections = groupBy(_filteredSortedEntries, (entry) => EntryAlbumSectionKey(entry.directory)); break; case EntryGroupFactor.month: sections = groupBy(_filteredSortedEntries, (entry) => EntryDateSectionKey(entry.monthTaken)); break; case EntryGroupFactor.day: sections = groupBy(_filteredSortedEntries, (entry) => EntryDateSectionKey(entry.dayTaken)); break; case EntryGroupFactor.none: sections = Map.fromEntries([ MapEntry(const SectionKey(), _filteredSortedEntries), ]); break; } break; case EntrySortFactor.size: sections = Map.fromEntries([ MapEntry(const SectionKey(), _filteredSortedEntries), ]); break; case EntrySortFactor.name: final byAlbum = groupBy(_filteredSortedEntries, (entry) => EntryAlbumSectionKey(entry.directory)); sections = SplayTreeMap>.of(byAlbum, (a, b) => source.compareAlbumsByName(a.directory!, b.directory!)); break; } sections = Map.unmodifiable(sections); _sortedEntries = null; notifyListeners(); } // metadata change should also trigger a full refresh // as dates impact sorting and sectioning void _refresh() { _applyFilters(); _applySort(); _applySection(); } void _onFavouritesChanged() { if (filters.any((filter) => filter is FavouriteFilter)) { _refresh(); } } void _onSettingsChanged() { final newSortFactor = settings.collectionSortFactor; final newSectionFactor = settings.collectionSectionFactor; final needSort = sortFactor != newSortFactor; final needSection = needSort || sectionFactor != newSectionFactor; if (needSort) { sortFactor = newSortFactor; _applySort(); } if (needSection) { sectionFactor = newSectionFactor; _applySection(); } if (needSort || needSection) { sortSectionChangeNotifier.notifyListeners(); } } void _onEntryAdded(Set? entries) { _refresh(); } void _onEntryRemoved(Set entries) { if (groupBursts) { // find impacted burst groups final obsoleteBurstEntries = {}; final burstKeys = entries.map((entry) => entry.burstKey).whereNotNull().toSet(); if (burstKeys.isNotEmpty) { _filteredSortedEntries.where((entry) => entry.isBurst && burstKeys.contains(entry.burstKey)).forEach((mainEntry) { final subEntries = mainEntry.burstEntries!; // remove the deleted sub-entries subEntries.removeWhere(entries.contains); if (subEntries.isEmpty) { // remove the burst entry itself obsoleteBurstEntries.add(mainEntry); } // TODO TLAD [burst] recreate the burst main entry if the first sub-entry got deleted }); entries.addAll(obsoleteBurstEntries); } } // we should remove obsolete entries and sections // but do not apply sort/section // as section order change would surprise the user while browsing fixedSelection?.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))); notifyListeners(); } }