import 'dart:convert'; import 'package:aves/model/filters/container/album_group.dart'; import 'package:aves/model/filters/container/group_base.dart'; import 'package:aves/model/filters/container/set_or.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/grouping/convert.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/utils/collection_utils.dart'; import 'package:collection/collection.dart'; import 'package:event_bus/event_bus.dart'; import 'package:flutter/foundation.dart'; final FilterGrouping albumGrouping = FilterGrouping._private(FilterGrouping.hostAlbums, AlbumGroupFilter.new); // album group URI: "aves://albums/group?path=/group12/subgroup34" // stored album URI: "aves://albums/stored?path=/volume/dir/path12" // dynamic album URI: "aves://albums/dynamic?name=dynalbum12" class FilterGrouping with ChangeNotifier { static const scheme = 'aves'; static const hostAlbums = 'albums'; static const _groupPath = '/group'; static const _groupPathParamKey = 'path'; final EventBus eventBus = EventBus(); final String _host; final T Function(Uri uri, SetOrFilter filter) _createGroupFilter; final Map> _groups = {}; FilterGrouping._private(this._host, this._createGroupFilter) { if (kFlutterMemoryAllocationsEnabled) ChangeNotifier.maybeDispatchObjectCreation(this); } static FilterGrouping? forUri(Uri uri) { switch (uri.host) { case hostAlbums: return albumGrouping; default: return null; } } void clear() => _groups.clear(); // TODO TLAD [nested] invalidate summary for derived filters (parent groups, dynamic albums) void addToGroup(Set childrenUris, Uri? destinationGroup) { _removeFromGroups(childrenUris); if (destinationGroup != null) { _ensureGroupFromRoot(destinationGroup); final children = _groups[destinationGroup] ?? {}; children.addAll(childrenUris); _groups[destinationGroup] = children; } _reparentGroupPaths(childrenUris, destinationGroup); _cleanEmptyGroups(); notifyListeners(); } void rename(Uri oldUri, Uri newUri) { final childrenUris = _groups[oldUri]; if (childrenUris != null) { // local copy to prevent concurrent modification addToGroup(Set.of(childrenUris), newUri); } eventBus.fire(GroupUriChangedEvent(oldUri, newUri)); } bool get isNotEmpty => _groups.isNotEmpty; bool exists(Uri? groupUri) => _groups.containsKey(groupUri); Set getGroups() => _groups.keys.toSet(); // returns number of filters within provided group, following subgroups without counting them // providing the null root will yield 0, rather than the total number of filters in the collection (which is out of scope) int countLeaves(Uri? groupUri) { int count = 0; if (groupUri != null) { final childrenUri = _groups[groupUri]; if (childrenUri != null) { childrenUri.map(uriToFilter).nonNulls.forEach((filter) { if (filter is GroupBaseFilter) { count += countLeaves(filter.uri); } else { count++; } }); } } return count; } // returns filters directly within the provided group, including subgroups as filters // providing the null root will yield its direct group filters Set getDirectChildren(Uri? currentGroupUri) { if (currentGroupUri == null) { return _groups.entries.where((kv) => getParentGroup(kv.key) == currentGroupUri).map((kv) { final groupUri = kv.key; final childrenUri = kv.value; final childrenFilters = childrenUri.map(uriToFilter).nonNulls.toSet(); return _createGroupFilter(groupUri, SetOrFilter(childrenFilters)); }).toSet(); } final childrenUri = _groups.entries.firstWhereOrNull((kv) => kv.key == currentGroupUri)?.value; if (childrenUri != null) { return childrenUri.map(uriToFilter).nonNulls.toSet(); } return {}; } Uri buildGroupUri(Uri? parentGroupUri, String name) { return _buildGroupUri(_host, parentGroupUri, name); } CollectionFilter? uriToFilter(Uri? uri) { if (uri == null || uri.host != _host) return null; if (FilterGrouping.isGroupUri(uri)) { return _createGroupFilter(uri, SetOrFilter(getDirectChildren(uri))); } return GroupingConversion.uriToFilter(uri); } void _removeFromGroups(Set uris) { _groups.forEach((_, childrenUris) { childrenUris.removeAll(uris); }); } void _cleanEmptyGroups() { final emptyGroupUris = _groups.entries.where((kv) => kv.value.isEmpty).map((v) => v.key).toSet(); if (emptyGroupUris.isNotEmpty) { _removeFromGroups(emptyGroupUris); _groups.removeWhere((groupUri, _) => emptyGroupUris.contains(groupUri)); _cleanEmptyGroups(); } } void _reparentGroupPaths(Set childrenUris, Uri? parentGroupUri) { final groupUris = childrenUris.where(FilterGrouping.isGroupUri).toSet(); groupUris.forEach((oldGroupUri) { final name = FilterGrouping.getGroupName(oldGroupUri); if (name != null) { final groupChildrenUris = _groups.remove(oldGroupUri); if (groupChildrenUris != null) { // create child group with updated URI final newGroupUri = buildGroupUri(parentGroupUri, name); _groups[newGroupUri] = groupChildrenUris; // update child group URI in parent group itself if (parentGroupUri != null) { final children = _groups[parentGroupUri]; if (children != null && children.remove(oldGroupUri)) { children.add(newGroupUri); } } eventBus.fire(GroupUriChangedEvent(oldGroupUri, newGroupUri)); _reparentGroupPaths(groupChildrenUris, newGroupUri); } } }); } void _ensureGroupFromRoot(Uri groupUri) { final parentGroupUri = FilterGrouping.getParentGroup(groupUri); if (parentGroupUri != null) { final children = _groups[parentGroupUri] ?? {}; children.addAll({groupUri}); _groups[parentGroupUri] = children; _ensureGroupFromRoot(parentGroupUri); } } // group uri / filter conversion static String? getGroupPath(Uri? uri) => uri?.queryParameters[_groupPathParamKey]; static String? getGroupName(Uri? uri) { final path = getGroupPath(uri); return path != null ? pContext.split(path).lastOrNull : null; } static bool isGroupUri(Uri uri) => uri.path == FilterGrouping._groupPath; // parent group URI is `null` for root static Uri _buildGroupUri(String host, Uri? parentGroupUri, String name) { if (parentGroupUri != null) { final path = getGroupPath(parentGroupUri); if (path != null) { return parentGroupUri.replace( queryParameters: { _groupPathParamKey: pContext.join(path, name), }, ); } } return Uri( scheme: scheme, host: host, path: _groupPath, queryParameters: { _groupPathParamKey: name, }, ); } // returns `null` for root Uri? getFilterParent(CollectionFilter filter) { final uri = GroupingConversion.filterToUri(filter); if (uri == null) return null; if (isGroupUri(uri)) { return getParentGroup(uri); } else { return _groups.entries.firstWhereOrNull((kv) { return kv.value.contains(uri); })?.key; } } // returns `null` for root static Uri? getParentGroup(Uri? groupUri) { if (groupUri == null) return null; final path = getGroupPath(groupUri); if (path != null) { final segments = pContext.split(path); final newLength = segments.length - 1; if (newLength > 0) { return groupUri.replace( queryParameters: { _groupPathParamKey: pContext.joinAll(segments.take(newLength)), }, ); } } return null; } // serialization static String toJson(Map> groups) => jsonEncode(groups.map((parentUri, childrenUris) { return MapEntry(parentUri.toString(), childrenUris.map((v) => v.toString())); })); static Map>? fromJson(String? jsonString) { if (jsonString == null || jsonString.isEmpty) return null; final jsonMap = jsonDecode(jsonString); if (jsonMap is! Map) return null; return jsonMap.map((parent, children) { final Uri? parentUri = parent is String ? Uri.tryParse(parent) : null; final Set childrenUris = children is Iterable ? children.whereType().map(Uri.tryParse).nonNulls.toSet() : {}; return MapEntry(parentUri, childrenUris); }).whereNotNullKey(); } } @immutable class GroupUriChangedEvent { final Uri oldGroupUri; final Uri newGroupUri; const GroupUriChangedEvent(this.oldGroupUri, this.newGroupUri); }