358 lines
12 KiB
Dart
358 lines
12 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
|
|
import 'package:aves/model/dynamic_albums.dart';
|
|
import 'package:aves/model/filters/container/album_group.dart';
|
|
import 'package:aves/model/filters/container/dynamic_album.dart';
|
|
import 'package:aves/model/filters/container/group_base.dart';
|
|
import 'package:aves/model/filters/container/set_or.dart';
|
|
import 'package:aves/model/filters/covered/stored_album.dart';
|
|
import 'package:aves/model/filters/filters.dart';
|
|
import 'package:aves/model/grouping/convert.dart';
|
|
import 'package:aves/model/source/album.dart';
|
|
import 'package:aves/model/source/collection_source.dart';
|
|
import 'package:aves/model/source/events.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<T extends GroupBaseFilter> 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<Uri, Set<Uri>> _groups = {};
|
|
final Set<StreamSubscription> _subscriptions = {};
|
|
final Map<CollectionSource, Set<StreamSubscription>> _sourceSubscriptions = {};
|
|
CollectionSource? _source;
|
|
|
|
Map<Uri, Set<Uri>> get allGroups => Map.unmodifiable(_groups);
|
|
|
|
// do not subscribe to events from other modules in constructor
|
|
// so that modules can subscribe to each other
|
|
FilterGrouping._private(this._host, this._createGroupFilter) {
|
|
if (kFlutterMemoryAllocationsEnabled) ChangeNotifier.maybeDispatchObjectCreation(this);
|
|
}
|
|
|
|
void init() {
|
|
_subscriptions.add(dynamicAlbums.eventBus.on<DynamicAlbumChangedEvent>().listen((e) => _clearObsoleteFilters()));
|
|
}
|
|
|
|
void setGroups(Map<Uri, Set<Uri>> groups) {
|
|
_groups.clear();
|
|
_groups.addAll(groups);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_subscriptions
|
|
..forEach((sub) => sub.cancel())
|
|
..clear();
|
|
_sourceSubscriptions.keys.toSet().forEach(unregisterSource);
|
|
super.dispose();
|
|
}
|
|
|
|
void registerSource(CollectionSource source) {
|
|
unregisterSource(_source);
|
|
final sourceEvents = source.eventBus;
|
|
_sourceSubscriptions[source] = {
|
|
sourceEvents.on<EntryMovedEvent>().listen((e) => _clearObsoleteFilters()),
|
|
sourceEvents.on<EntryRemovedEvent>().listen((e) => _clearObsoleteFilters()),
|
|
sourceEvents.on<AlbumsChangedEvent>().listen((e) => _clearObsoleteFilters()),
|
|
};
|
|
_source = source;
|
|
}
|
|
|
|
void unregisterSource(CollectionSource? source) {
|
|
_sourceSubscriptions.remove(source)
|
|
?..forEach((sub) => sub.cancel())
|
|
..clear();
|
|
}
|
|
|
|
void addToGroup(Set<Uri> 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) return;
|
|
|
|
// 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<Uri> 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 childrenUris = _groups[groupUri];
|
|
if (childrenUris != null) {
|
|
childrenUris.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<CollectionFilter> getDirectChildren(Uri? currentGroupUri) {
|
|
if (currentGroupUri == null) {
|
|
return _groups.entries.where((kv) => getParentGroup(kv.key) == currentGroupUri).map((kv) {
|
|
final groupUri = kv.key;
|
|
final childrenUris = kv.value;
|
|
final childrenFilters = childrenUris.map(uriToFilter).nonNulls.toSet();
|
|
return _createGroupFilter(groupUri, SetOrFilter(childrenFilters));
|
|
}).toSet();
|
|
}
|
|
|
|
final childrenUris = _groups.entries.firstWhereOrNull((kv) => kv.key == currentGroupUri)?.value;
|
|
if (childrenUris != null) {
|
|
return childrenUris.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<Uri> 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<Uri> 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);
|
|
}
|
|
}
|
|
|
|
void _clearObsoleteFilters() {
|
|
final source = _source;
|
|
if (source == null || source.targetScope != CollectionSource.fullScope || !source.isReady) return;
|
|
|
|
_groups.entries.forEach((kv) {
|
|
final groupUri = kv.key;
|
|
final childrenUris = kv.value;
|
|
|
|
final rawAlbums = source.rawAlbums;
|
|
final allEntries = source.allEntries;
|
|
|
|
childrenUris.toSet().forEach((childUri) {
|
|
final filter = uriToFilter(childUri);
|
|
var valid = false;
|
|
if (filter != null) {
|
|
switch (filter) {
|
|
case GroupBaseFilter _:
|
|
valid = true;
|
|
case StoredAlbumFilter _:
|
|
// check album itself
|
|
final isVisibleAlbum = rawAlbums.contains(filter.album);
|
|
if (isVisibleAlbum) {
|
|
valid = true;
|
|
} else {
|
|
// check non-visible content (hidden, trash, etc.)
|
|
valid = allEntries.any(filter.test);
|
|
}
|
|
case DynamicAlbumFilter _:
|
|
valid = dynamicAlbums.contains(filter.name);
|
|
}
|
|
}
|
|
if (!valid) {
|
|
childrenUris.remove(childUri);
|
|
debugPrint('Removed obsolete childUri=$childUri from group=$groupUri');
|
|
}
|
|
});
|
|
});
|
|
_cleanEmptyGroups();
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
static FilterGrouping? forUri(Uri uri) {
|
|
switch (uri.host) {
|
|
case hostAlbums:
|
|
return albumGrouping;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// serialization
|
|
|
|
static String toJson(Map<Uri, Set<Uri>> groups) => jsonEncode(groups.map((parentUri, childrenUris) {
|
|
return MapEntry(parentUri.toString(), childrenUris.map((v) => v.toString()).toList());
|
|
}));
|
|
|
|
static Map<Uri, Set<Uri>>? 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<Uri> childrenUris = children is Iterable ? children.whereType<String>().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);
|
|
}
|