import 'dart:async'; import 'package:aves/model/filters/container/container.dart'; import 'package:aves/model/filters/container/dynamic_album.dart'; import 'package:aves/model/filters/container/group_base.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/grouping/common.dart'; import 'package:aves/services/common/services.dart'; import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; import 'package:event_bus/event_bus.dart'; import 'package:flutter/foundation.dart'; import 'package:synchronized/synchronized.dart'; final DynamicAlbums dynamicAlbums = DynamicAlbums._private(); class DynamicAlbums with ChangeNotifier { final Set _subscriptions = {}; final _lock = Lock(); Set _rows = {}; final EventBus eventBus = EventBus(); // do not subscribe to events from other modules in constructor // so that modules can subscribe to each other DynamicAlbums._private() { if (kFlutterMemoryAllocationsEnabled) ChangeNotifier.maybeDispatchObjectCreation(this); } Future init() async { _rows = (await localMediaDb.loadAllDynamicAlbums()).map((v) => DynamicAlbumFilter(v.name, v.filter)).toSet(); _subscriptions.add(albumGrouping.eventBus.on().listen((e) => _onGroupUriChanged(e.oldGroupUri, e.newGroupUri))); } int get count => _rows.length; Set get all => Set.unmodifiable(_rows); Future _doAdd(Set? filters) async { if (filters == null || filters.isEmpty) return; await localMediaDb.addDynamicAlbums(filters.map((v) => DynamicAlbumRow(name: v.name, filter: v.filter)).toSet()); _rows.addAll(filters); } Future _doRemove(Set? names) async { if (names == null || names.isEmpty) return; await localMediaDb.removeDynamicAlbums(names); _rows.removeWhere((v) => names.contains(v.name)); } Future add(DynamicAlbumFilter filter) async { await _lock.synchronized(() async { await _doAdd({filter}); notifyListeners(); }); } Future remove(Set filters) async { await _lock.synchronized(() async { await _doRemove(filters.map((filter) => filter.name).toSet()); notifyListeners(); eventBus.fire(DynamicAlbumChangedEvent(Map.fromEntries(filters.map((v) => MapEntry(v, null))))); }); } Future rename(DynamicAlbumFilter oldFilter, String newName) async { await _lock.synchronized(() async { final newFilter = DynamicAlbumFilter(newName, oldFilter.filter); await _doRemove({oldFilter.name}); await _doAdd({newFilter}); notifyListeners(); eventBus.fire(DynamicAlbumChangedEvent({oldFilter: newFilter})); }); } Future update(Map changes) async { await _lock.synchronized(() async { final oldFilterNames = changes.keys.map((v) => v.name).toSet(); final newFilters = changes.values.nonNulls.toSet(); await _doRemove(oldFilterNames); await _doAdd(newFilters); notifyListeners(); eventBus.fire(DynamicAlbumChangedEvent(changes)); }); } Future clear() => remove(all); DynamicAlbumFilter? get(String name) => _rows.firstWhereOrNull((row) => row.name == name); bool contains(String? name) => name != null && get(name) != null; Future _onGroupUriChanged(Uri oldGroupUri, Uri newGroupUri) async { bool isOldGroupFilter(CollectionFilter filter) => filter is GroupBaseFilter && filter.uri == oldGroupUri; final oldDynamicAlbumFilters = _rows.where((v) => v.containsFilter(isOldGroupFilter)).toSet(); if (oldDynamicAlbumFilters.isEmpty) return; final grouping = FilterGrouping.forUri(oldGroupUri); final newGroupFilter = grouping?.uriToFilter(newGroupUri); CollectionFilter? transformer(v) { if (isOldGroupFilter(v)) return v.reversed ? newGroupFilter?.reverse() : newGroupFilter; if (v is ContainerFilter) return v.replaceFilters(transformer); return v; } final newFilters = {}; oldDynamicAlbumFilters.forEach((oldFilter) { final newFilter = oldFilter.replaceFilters(transformer); newFilters[oldFilter] = newFilter; }); await update(newFilters); } // import/export List? export() { final jsonList = all.map((row) => row.toJson()).toList(); return jsonList.isNotEmpty ? jsonList : null; } void import(dynamic jsonList) { if (jsonList is! List) { debugPrint('failed to import dynamic albums for jsonMap=$jsonList'); return; } jsonList.forEach((row) { final filter = CollectionFilter.fromJson(row); if (filter == null || filter is! DynamicAlbumFilter) { debugPrint('failed to import dynamic album for row=$row'); return; } add(filter); }); } } @immutable class DynamicAlbumRow extends Equatable { final String name; final CollectionFilter filter; @override List get props => [name, filter]; const DynamicAlbumRow({ required this.name, required this.filter, }); static DynamicAlbumRow? fromMap(Map map) { final filter = CollectionFilter.fromJson(map['filter']); if (filter == null) return null; return DynamicAlbumRow( name: map['name'] as String, filter: filter, ); } Map toMap() => { 'name': name, 'filter': filter.toJson(), }; } @immutable class DynamicAlbumChangedEvent { final Map changes; const DynamicAlbumChangedEvent(this.changes); }