mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-28 23:08:59 +02:00
refactor(library): drop dead pre-pagination filter pipeline in queue tab
The DB-backed pagination migration left the old in-widget filter/unification pipeline behind three unused_element ignores; every helper, cache field, and isolate entry point below them was reachable only from that dead root.
This commit is contained in:
@@ -41,7 +41,6 @@ import 'package:spotiflac_android/services/cover_cache_manager.dart';
|
||||
import 'package:spotiflac_android/screens/library_tracks_folder_screen.dart';
|
||||
import 'package:spotiflac_android/screens/local_album_screen.dart';
|
||||
import 'package:spotiflac_android/utils/clickable_metadata.dart';
|
||||
import 'package:spotiflac_android/utils/path_match_keys.dart';
|
||||
import 'package:spotiflac_android/utils/string_utils.dart';
|
||||
import 'package:spotiflac_android/widgets/download_service_picker.dart';
|
||||
import 'package:spotiflac_android/widgets/animation_utils.dart';
|
||||
@@ -139,7 +138,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
static const int _libraryPageSize = 300;
|
||||
final _FileExistsListenableCache _fileExistsCache =
|
||||
_FileExistsListenableCache();
|
||||
static const int _maxSearchIndexCacheSize = 4000;
|
||||
static const double _libraryGridMinExtent = 92;
|
||||
static const double _libraryGridDefaultExtent = 126;
|
||||
static const double _libraryGridMaxExtent = 190;
|
||||
@@ -186,46 +184,9 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
final FocusNode _searchFocusNode = FocusNode();
|
||||
String _searchQuery = '';
|
||||
Timer? _searchDebounce;
|
||||
List<DownloadHistoryItem>? _historyItemsCache;
|
||||
List<LocalLibraryItem>? _localLibraryItemsCache;
|
||||
_HistoryStats? _historyStatsCache;
|
||||
final Map<String, Track> _completionBridge = {};
|
||||
final Map<String, DateTime> _completionBridgeAt = {};
|
||||
final Set<String> _bridgePrecacheStarted = {};
|
||||
final Map<String, String> _searchIndexCache = {};
|
||||
final Map<String, String> _localSearchIndexCache = {};
|
||||
Map<String, List<DownloadHistoryItem>> _filteredHistoryCache = const {};
|
||||
List<DownloadHistoryItem>? _filterItemsCache;
|
||||
String _filterQueryCache = '';
|
||||
bool _filterRefreshScheduled = false;
|
||||
bool _isFilteringHistory = false;
|
||||
int _filterRequestId = 0;
|
||||
static const int _filterIsolateThreshold = 800;
|
||||
List<LocalLibraryItem>? _localFilterItemsCache;
|
||||
String _localFilterQueryCache = '';
|
||||
List<LocalLibraryItem> _filteredLocalItemsCache = const [];
|
||||
final Map<String, _UnifiedCacheEntry> _unifiedItemsCache = {};
|
||||
List<DownloadHistoryItem>? _cachedUnifiedDownloadedSource;
|
||||
List<UnifiedLibraryItem> _cachedUnifiedDownloaded = const [];
|
||||
List<LocalLibraryItem>? _cachedUnifiedLocalSource;
|
||||
List<UnifiedLibraryItem> _cachedUnifiedLocal = const [];
|
||||
List<DownloadHistoryItem>? _cachedDownloadedPathKeysSource;
|
||||
Set<String> _cachedDownloadedPathKeys = const <String>{};
|
||||
final Map<String, List<String>> _localPathMatchKeysCache = {};
|
||||
List<LocalLibraryItem>? _cachedLocalSinglesSource;
|
||||
Map<String, int>? _cachedLocalSinglesAlbumCountsSource;
|
||||
List<LocalLibraryItem> _cachedLocalSingles = const [];
|
||||
final Map<String, _FilterContentData> _filterContentDataCache = {};
|
||||
List<DownloadHistoryItem>? _filterCacheAllHistoryItems;
|
||||
_HistoryStats? _filterCacheHistoryStats;
|
||||
List<LocalLibraryItem>? _filterCacheLocalLibraryItems;
|
||||
LibraryCollectionsState? _filterCacheCollectionState;
|
||||
String _filterCacheSearchQuery = '';
|
||||
String? _filterCacheSource;
|
||||
String? _filterCacheQuality;
|
||||
String? _filterCacheFormat;
|
||||
String? _filterCacheMetadata;
|
||||
String _filterCacheSortMode = 'latest';
|
||||
String? _filterSource;
|
||||
String? _filterQuality;
|
||||
String? _filterFormat;
|
||||
@@ -318,7 +279,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
_searchQuery = normalized;
|
||||
_resetLibraryPaging();
|
||||
});
|
||||
_requestFilterRefresh();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -329,7 +289,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
_searchQuery = '';
|
||||
_resetLibraryPaging();
|
||||
});
|
||||
_requestFilterRefresh();
|
||||
}
|
||||
|
||||
int _libraryPageOffsetFor(String filterMode) =>
|
||||
@@ -419,8 +378,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
void _invalidateLibraryDataCaches() {
|
||||
_queueLibraryCountsCache.clear();
|
||||
_queueLibraryPageDataCache.clear();
|
||||
_unifiedItemsCache.clear();
|
||||
_invalidateFilterContentCache();
|
||||
}
|
||||
|
||||
void _scheduleBlankLibraryRepair({
|
||||
@@ -514,379 +471,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
return false;
|
||||
}
|
||||
|
||||
void _invalidateFilterContentCache() {
|
||||
_filterContentDataCache.clear();
|
||||
_filterCacheAllHistoryItems = null;
|
||||
_filterCacheHistoryStats = null;
|
||||
_filterCacheLocalLibraryItems = null;
|
||||
_filterCacheCollectionState = null;
|
||||
}
|
||||
|
||||
// ignore: unused_element
|
||||
void _prepareFilterContentCache({
|
||||
required List<DownloadHistoryItem> allHistoryItems,
|
||||
required _HistoryStats historyStats,
|
||||
required List<LocalLibraryItem> localLibraryItems,
|
||||
required LibraryCollectionsState collectionState,
|
||||
}) {
|
||||
final isCacheValid =
|
||||
identical(_filterCacheAllHistoryItems, allHistoryItems) &&
|
||||
identical(_filterCacheHistoryStats, historyStats) &&
|
||||
identical(_filterCacheLocalLibraryItems, localLibraryItems) &&
|
||||
identical(_filterCacheCollectionState, collectionState) &&
|
||||
_filterCacheSearchQuery == _searchQuery &&
|
||||
_filterCacheSource == _filterSource &&
|
||||
_filterCacheQuality == _filterQuality &&
|
||||
_filterCacheFormat == _filterFormat &&
|
||||
_filterCacheMetadata == _filterMetadata &&
|
||||
_filterCacheSortMode == _sortMode;
|
||||
|
||||
if (isCacheValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
_filterContentDataCache.clear();
|
||||
_filterCacheAllHistoryItems = allHistoryItems;
|
||||
_filterCacheHistoryStats = historyStats;
|
||||
_filterCacheLocalLibraryItems = localLibraryItems;
|
||||
_filterCacheCollectionState = collectionState;
|
||||
_filterCacheSearchQuery = _searchQuery;
|
||||
_filterCacheSource = _filterSource;
|
||||
_filterCacheQuality = _filterQuality;
|
||||
_filterCacheFormat = _filterFormat;
|
||||
_filterCacheMetadata = _filterMetadata;
|
||||
_filterCacheSortMode = _sortMode;
|
||||
}
|
||||
|
||||
// ignore: unused_element
|
||||
void _ensureHistoryCaches(
|
||||
List<DownloadHistoryItem> items,
|
||||
List<LocalLibraryItem> localItems,
|
||||
_HistoryStats historyStats,
|
||||
) {
|
||||
final historyChanged = !identical(items, _historyItemsCache);
|
||||
final localChanged = !identical(localItems, _localLibraryItemsCache);
|
||||
|
||||
if (!historyChanged && !localChanged) return;
|
||||
|
||||
_historyItemsCache = items;
|
||||
_localLibraryItemsCache = localItems;
|
||||
_historyStatsCache = historyStats;
|
||||
if (historyChanged) {
|
||||
_searchIndexCache.clear();
|
||||
_cachedUnifiedDownloadedSource = null;
|
||||
_cachedUnifiedDownloaded = const [];
|
||||
_cachedDownloadedPathKeysSource = null;
|
||||
_cachedDownloadedPathKeys = const <String>{};
|
||||
}
|
||||
if (localChanged) {
|
||||
_localSearchIndexCache.clear();
|
||||
_localPathMatchKeysCache.clear();
|
||||
_localFilterItemsCache = null;
|
||||
_localFilterQueryCache = '';
|
||||
_filteredLocalItemsCache = const [];
|
||||
_cachedLocalSinglesSource = null;
|
||||
_cachedLocalSinglesAlbumCountsSource = null;
|
||||
_cachedLocalSingles = const [];
|
||||
_cachedUnifiedLocalSource = null;
|
||||
_cachedUnifiedLocal = const [];
|
||||
}
|
||||
_unifiedItemsCache.clear();
|
||||
_invalidateFilterContentCache();
|
||||
|
||||
if (historyChanged) {
|
||||
final validPaths = items
|
||||
.map((item) => _cleanFilePath(item.filePath))
|
||||
.where((path) => path.isNotEmpty)
|
||||
.toSet();
|
||||
DownloadedEmbeddedCoverResolver.invalidatePathsNotIn(validPaths);
|
||||
}
|
||||
_requestFilterRefresh();
|
||||
}
|
||||
|
||||
String _buildSearchKey(DownloadHistoryItem item) {
|
||||
return '${item.trackName} ${item.artistName} ${item.albumName}'
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
String _buildLocalSearchKey(LocalLibraryItem item) {
|
||||
return '${item.trackName} ${item.artistName} ${item.albumName}'
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
String _historySearchKeyForItem(DownloadHistoryItem item) {
|
||||
final cached = _searchIndexCache[item.id];
|
||||
if (cached != null) return cached;
|
||||
|
||||
final searchKey = _buildSearchKey(item);
|
||||
_searchIndexCache[item.id] = searchKey;
|
||||
while (_searchIndexCache.length > _maxSearchIndexCacheSize) {
|
||||
_searchIndexCache.remove(_searchIndexCache.keys.first);
|
||||
}
|
||||
return searchKey;
|
||||
}
|
||||
|
||||
String _localSearchKeyForItem(LocalLibraryItem item) {
|
||||
final cached = _localSearchIndexCache[item.id];
|
||||
if (cached != null) return cached;
|
||||
|
||||
final searchKey = _buildLocalSearchKey(item);
|
||||
_localSearchIndexCache[item.id] = searchKey;
|
||||
while (_localSearchIndexCache.length > _maxSearchIndexCacheSize) {
|
||||
_localSearchIndexCache.remove(_localSearchIndexCache.keys.first);
|
||||
}
|
||||
return searchKey;
|
||||
}
|
||||
|
||||
List<UnifiedLibraryItem> _unifiedDownloadedItems(
|
||||
List<DownloadHistoryItem> items,
|
||||
) {
|
||||
if (identical(items, _cachedUnifiedDownloadedSource)) {
|
||||
return _cachedUnifiedDownloaded;
|
||||
}
|
||||
final unified = items
|
||||
.map(UnifiedLibraryItem.fromDownloadHistory)
|
||||
.toList(growable: false);
|
||||
_cachedUnifiedDownloadedSource = items;
|
||||
_cachedUnifiedDownloaded = unified;
|
||||
return unified;
|
||||
}
|
||||
|
||||
List<UnifiedLibraryItem> _unifiedLocalItems(List<LocalLibraryItem> items) {
|
||||
if (identical(items, _cachedUnifiedLocalSource)) {
|
||||
return _cachedUnifiedLocal;
|
||||
}
|
||||
final unified = items
|
||||
.map(UnifiedLibraryItem.fromLocalLibrary)
|
||||
.toList(growable: false);
|
||||
_cachedUnifiedLocalSource = items;
|
||||
_cachedUnifiedLocal = unified;
|
||||
return unified;
|
||||
}
|
||||
|
||||
Set<String> _downloadedPathKeys(List<DownloadHistoryItem> historyItems) {
|
||||
if (identical(historyItems, _cachedDownloadedPathKeysSource)) {
|
||||
return _cachedDownloadedPathKeys;
|
||||
}
|
||||
final keys = <String>{};
|
||||
for (final item in historyItems) {
|
||||
keys.addAll(buildPathMatchKeys(item.filePath));
|
||||
}
|
||||
_cachedDownloadedPathKeysSource = historyItems;
|
||||
_cachedDownloadedPathKeys = Set<String>.unmodifiable(keys);
|
||||
return _cachedDownloadedPathKeys;
|
||||
}
|
||||
|
||||
List<String> _localPathMatchKeys(LocalLibraryItem item) {
|
||||
final cached = _localPathMatchKeysCache[item.id];
|
||||
if (cached != null) return cached;
|
||||
final keys = buildPathMatchKeys(item.filePath).toList(growable: false);
|
||||
_localPathMatchKeysCache[item.id] = keys;
|
||||
return keys;
|
||||
}
|
||||
|
||||
List<LocalLibraryItem> _localSingleItems(
|
||||
List<LocalLibraryItem> items,
|
||||
Map<String, int> localAlbumCounts,
|
||||
) {
|
||||
if (identical(items, _cachedLocalSinglesSource) &&
|
||||
identical(localAlbumCounts, _cachedLocalSinglesAlbumCountsSource)) {
|
||||
return _cachedLocalSingles;
|
||||
}
|
||||
|
||||
final singles = items
|
||||
.where((item) => (localAlbumCounts[item.albumKey] ?? 0) == 1)
|
||||
.toList(growable: false);
|
||||
_cachedLocalSinglesSource = items;
|
||||
_cachedLocalSinglesAlbumCountsSource = localAlbumCounts;
|
||||
_cachedLocalSingles = singles;
|
||||
return singles;
|
||||
}
|
||||
|
||||
List<LocalLibraryItem> _filterLocalItems(
|
||||
List<LocalLibraryItem> items,
|
||||
String query,
|
||||
) {
|
||||
if (query.isEmpty) return items;
|
||||
if (identical(items, _localFilterItemsCache) &&
|
||||
query == _localFilterQueryCache) {
|
||||
return _filteredLocalItemsCache;
|
||||
}
|
||||
|
||||
final filtered = items
|
||||
.where((item) {
|
||||
final searchKey = _localSearchKeyForItem(item);
|
||||
return searchKey.contains(query);
|
||||
})
|
||||
.toList(growable: false);
|
||||
|
||||
_localFilterItemsCache = items;
|
||||
_localFilterQueryCache = query;
|
||||
_filteredLocalItemsCache = filtered;
|
||||
return filtered;
|
||||
}
|
||||
|
||||
bool _isFilterCacheValid(List<DownloadHistoryItem> items, String query) {
|
||||
return identical(items, _filterItemsCache) && query == _filterQueryCache;
|
||||
}
|
||||
|
||||
void _requestFilterRefresh() {
|
||||
if (_filterRefreshScheduled) return;
|
||||
_filterRefreshScheduled = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_filterRefreshScheduled = false;
|
||||
if (!mounted) return;
|
||||
_scheduleHistoryFilterUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
void _scheduleHistoryFilterUpdate() {
|
||||
final items = _historyItemsCache;
|
||||
if (items == null) return;
|
||||
final query = _searchQuery;
|
||||
if (_isFilterCacheValid(items, query)) return;
|
||||
|
||||
final albumCounts =
|
||||
_historyStatsCache?.albumCounts ?? const <String, int>{};
|
||||
if (items.isEmpty) {
|
||||
setState(() {
|
||||
_filteredHistoryCache = const {};
|
||||
_filterItemsCache = items;
|
||||
_filterQueryCache = query;
|
||||
_isFilteringHistory = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (items.length <= _filterIsolateThreshold) {
|
||||
final filteredAll = _applyHistorySearchFilter(items, query);
|
||||
final filteredAlbums = _filterHistoryByAlbumCount(
|
||||
filteredAll,
|
||||
albumCounts,
|
||||
2,
|
||||
);
|
||||
final filteredSingles = _filterHistoryByAlbumCount(
|
||||
filteredAll,
|
||||
albumCounts,
|
||||
1,
|
||||
);
|
||||
setState(() {
|
||||
_filteredHistoryCache = {
|
||||
'all': filteredAll,
|
||||
'albums': filteredAlbums,
|
||||
'singles': filteredSingles,
|
||||
};
|
||||
_filterItemsCache = items;
|
||||
_filterQueryCache = query;
|
||||
_isFilteringHistory = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_isFilteringHistory) {
|
||||
setState(() => _isFilteringHistory = true);
|
||||
}
|
||||
|
||||
final requestId = ++_filterRequestId;
|
||||
final includeSearchKey = query.isNotEmpty;
|
||||
final entries = List<List<String>>.generate(items.length, (index) {
|
||||
final item = items[index];
|
||||
final albumKey =
|
||||
'${item.albumName.toLowerCase()}|${(item.albumArtist ?? item.artistName).toLowerCase()}';
|
||||
if (!includeSearchKey) {
|
||||
return [item.id, albumKey];
|
||||
}
|
||||
final searchKey = _historySearchKeyForItem(item);
|
||||
return [item.id, albumKey, searchKey];
|
||||
}, growable: false);
|
||||
final payload = <String, Object>{
|
||||
'entries': entries,
|
||||
'albumCounts': albumCounts,
|
||||
'query': query,
|
||||
};
|
||||
|
||||
compute(_filterHistoryInIsolate, payload).then((result) {
|
||||
if (!mounted || requestId != _filterRequestId) return;
|
||||
final itemsById = {for (final item in items) item.id: item};
|
||||
final filtered = <String, List<DownloadHistoryItem>>{};
|
||||
for (final entry in result.entries) {
|
||||
filtered[entry.key] = entry.value
|
||||
.map((id) => itemsById[id])
|
||||
.whereType<DownloadHistoryItem>()
|
||||
.toList(growable: false);
|
||||
}
|
||||
setState(() {
|
||||
_filteredHistoryCache = filtered;
|
||||
_filterItemsCache = items;
|
||||
_filterQueryCache = query;
|
||||
_isFilteringHistory = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
List<DownloadHistoryItem> _resolveHistoryItems({
|
||||
required String filterMode,
|
||||
required List<DownloadHistoryItem> allHistoryItems,
|
||||
required Map<String, int> albumCounts,
|
||||
}) {
|
||||
final query = _searchQuery;
|
||||
if (_isFilterCacheValid(allHistoryItems, query)) {
|
||||
final cached = _filteredHistoryCache[filterMode];
|
||||
if (cached != null) return cached;
|
||||
}
|
||||
if (allHistoryItems.isEmpty) return const [];
|
||||
if (query.isEmpty && filterMode == 'all') return allHistoryItems;
|
||||
if (allHistoryItems.length <= _filterIsolateThreshold) {
|
||||
return _filterHistoryItems(
|
||||
allHistoryItems,
|
||||
filterMode,
|
||||
albumCounts,
|
||||
query,
|
||||
);
|
||||
}
|
||||
return const [];
|
||||
}
|
||||
|
||||
List<DownloadHistoryItem> _applyHistorySearchFilter(
|
||||
List<DownloadHistoryItem> items,
|
||||
String searchQuery,
|
||||
) {
|
||||
if (searchQuery.isEmpty) return items;
|
||||
final query = searchQuery;
|
||||
return items
|
||||
.where((item) {
|
||||
final searchKey = _historySearchKeyForItem(item);
|
||||
return searchKey.contains(query);
|
||||
})
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
List<DownloadHistoryItem> _filterHistoryByAlbumCount(
|
||||
List<DownloadHistoryItem> items,
|
||||
Map<String, int> albumCounts,
|
||||
int targetCount,
|
||||
) {
|
||||
return items
|
||||
.where((item) {
|
||||
final key =
|
||||
'${item.albumName.toLowerCase()}|${(item.albumArtist ?? item.artistName).toLowerCase()}';
|
||||
final count = albumCounts[key] ?? 0;
|
||||
return targetCount == 1 ? count == 1 : count >= targetCount;
|
||||
})
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
bool _shouldShowFilteringIndicator({
|
||||
required List<DownloadHistoryItem> allHistoryItems,
|
||||
required String filterMode,
|
||||
}) {
|
||||
if (allHistoryItems.isEmpty) return false;
|
||||
if (_searchQuery.isEmpty && filterMode == 'all') return false;
|
||||
if (allHistoryItems.length <= _filterIsolateThreshold) return false;
|
||||
return !_isFilterCacheValid(allHistoryItems, _searchQuery) ||
|
||||
_isFilteringHistory;
|
||||
}
|
||||
|
||||
void _onFilterPageChanged(int index) {
|
||||
HapticFeedback.selectionClick();
|
||||
final filterMode = _filterModes[index];
|
||||
@@ -959,8 +543,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
_filterMetadata = null;
|
||||
_sortMode = 'latest';
|
||||
_resetLibraryPaging();
|
||||
_unifiedItemsCache.clear();
|
||||
_invalidateFilterContentCache();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -984,173 +566,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
return _fileExtLower(item.filePath);
|
||||
}
|
||||
|
||||
List<UnifiedLibraryItem> _applyAdvancedFilters(
|
||||
List<UnifiedLibraryItem> items,
|
||||
) {
|
||||
List<UnifiedLibraryItem> filtered;
|
||||
if (_activeFilterCount == 0) {
|
||||
filtered = items;
|
||||
} else {
|
||||
filtered = items
|
||||
.where((item) {
|
||||
if (_filterSource != null) {
|
||||
if (_filterSource == 'downloaded' &&
|
||||
item.source != LibraryItemSource.downloaded) {
|
||||
return false;
|
||||
}
|
||||
if (_filterSource == 'local' &&
|
||||
item.source != LibraryItemSource.local) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (_filterQuality != null && item.quality != null) {
|
||||
final quality = item.quality!.toLowerCase();
|
||||
switch (_filterQuality) {
|
||||
case 'hires':
|
||||
if (!quality.startsWith('24')) return false;
|
||||
case 'cd':
|
||||
if (!quality.startsWith('16')) return false;
|
||||
case 'lossy':
|
||||
if (quality.startsWith('24') || quality.startsWith('16')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else if (_filterQuality != null && item.quality == null) {
|
||||
if (_filterQuality != 'lossy') return false;
|
||||
}
|
||||
|
||||
if (_filterFormat != null) {
|
||||
final ext = _itemFormatLower(item);
|
||||
if (ext != _filterFormat) return false;
|
||||
}
|
||||
|
||||
if (!_queueUnifiedItemMatchesMetadataFilter(
|
||||
item,
|
||||
_filterMetadata,
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
return _applySorting(filtered);
|
||||
}
|
||||
|
||||
List<UnifiedLibraryItem> _applySorting(List<UnifiedLibraryItem> items) {
|
||||
if (_sortMode == 'latest') {
|
||||
return items;
|
||||
}
|
||||
final sorted = List<UnifiedLibraryItem>.of(items);
|
||||
switch (_sortMode) {
|
||||
case 'oldest':
|
||||
sorted.sort((a, b) => a.addedAt.compareTo(b.addedAt));
|
||||
case 'a-z':
|
||||
sorted.sort(
|
||||
(a, b) =>
|
||||
a.trackName.toLowerCase().compareTo(b.trackName.toLowerCase()),
|
||||
);
|
||||
case 'z-a':
|
||||
sorted.sort(
|
||||
(a, b) =>
|
||||
b.trackName.toLowerCase().compareTo(a.trackName.toLowerCase()),
|
||||
);
|
||||
case 'artist-asc':
|
||||
sorted.sort((a, b) {
|
||||
final comparison = _queueCompareOptionalText(
|
||||
a.artistName,
|
||||
b.artistName,
|
||||
);
|
||||
if (comparison != 0) {
|
||||
return comparison;
|
||||
}
|
||||
return _queueCompareOptionalText(a.trackName, b.trackName);
|
||||
});
|
||||
case 'artist-desc':
|
||||
sorted.sort((a, b) {
|
||||
final comparison = _queueCompareOptionalText(
|
||||
a.artistName,
|
||||
b.artistName,
|
||||
descending: true,
|
||||
);
|
||||
if (comparison != 0) {
|
||||
return comparison;
|
||||
}
|
||||
return _queueCompareOptionalText(a.trackName, b.trackName);
|
||||
});
|
||||
case 'album-asc':
|
||||
sorted.sort((a, b) {
|
||||
final comparison = _queueCompareOptionalText(
|
||||
a.albumName,
|
||||
b.albumName,
|
||||
);
|
||||
if (comparison != 0) {
|
||||
return comparison;
|
||||
}
|
||||
return _queueCompareOptionalText(a.trackName, b.trackName);
|
||||
});
|
||||
case 'album-desc':
|
||||
sorted.sort((a, b) {
|
||||
final comparison = _queueCompareOptionalText(
|
||||
a.albumName,
|
||||
b.albumName,
|
||||
descending: true,
|
||||
);
|
||||
if (comparison != 0) {
|
||||
return comparison;
|
||||
}
|
||||
return _queueCompareOptionalText(a.trackName, b.trackName);
|
||||
});
|
||||
case 'release-oldest':
|
||||
sorted.sort((a, b) {
|
||||
final comparison = _queueCompareOptionalDate(
|
||||
_queueParseReleaseDate(a.releaseDate),
|
||||
_queueParseReleaseDate(b.releaseDate),
|
||||
);
|
||||
if (comparison != 0) {
|
||||
return comparison;
|
||||
}
|
||||
return _queueCompareOptionalText(a.trackName, b.trackName);
|
||||
});
|
||||
case 'release-newest':
|
||||
sorted.sort((a, b) {
|
||||
final comparison = _queueCompareOptionalDate(
|
||||
_queueParseReleaseDate(a.releaseDate),
|
||||
_queueParseReleaseDate(b.releaseDate),
|
||||
descending: true,
|
||||
);
|
||||
if (comparison != 0) {
|
||||
return comparison;
|
||||
}
|
||||
return _queueCompareOptionalText(a.trackName, b.trackName);
|
||||
});
|
||||
case 'genre-asc':
|
||||
sorted.sort((a, b) {
|
||||
final comparison = _queueCompareOptionalText(a.genre, b.genre);
|
||||
if (comparison != 0) {
|
||||
return comparison;
|
||||
}
|
||||
return _queueCompareOptionalText(a.trackName, b.trackName);
|
||||
});
|
||||
case 'genre-desc':
|
||||
sorted.sort((a, b) {
|
||||
final comparison = _queueCompareOptionalText(
|
||||
a.genre,
|
||||
b.genre,
|
||||
descending: true,
|
||||
);
|
||||
if (comparison != 0) {
|
||||
return comparison;
|
||||
}
|
||||
return _queueCompareOptionalText(a.trackName, b.trackName);
|
||||
});
|
||||
}
|
||||
return sorted;
|
||||
}
|
||||
|
||||
Set<String> _getAvailableFormats(List<UnifiedLibraryItem> items) {
|
||||
final formats = <String>{};
|
||||
for (final item in items) {
|
||||
@@ -1597,8 +1012,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
_filterMetadata = tempMetadata;
|
||||
_sortMode = tempSortMode;
|
||||
_resetLibraryPaging();
|
||||
_unifiedItemsCache.clear();
|
||||
_invalidateFilterContentCache();
|
||||
});
|
||||
Navigator.pop(context);
|
||||
},
|
||||
@@ -1998,120 +1411,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
);
|
||||
}
|
||||
|
||||
List<UnifiedLibraryItem> _getUnifiedItems({
|
||||
required String filterMode,
|
||||
required List<DownloadHistoryItem> historyItems,
|
||||
required List<LocalLibraryItem> localLibraryItems,
|
||||
required Map<String, int> localAlbumCounts,
|
||||
}) {
|
||||
if (filterMode == 'albums') return const [];
|
||||
|
||||
final query = _searchQuery;
|
||||
final cached = _unifiedItemsCache[filterMode];
|
||||
if (cached != null &&
|
||||
identical(cached.historyItems, historyItems) &&
|
||||
identical(cached.localItems, localLibraryItems) &&
|
||||
identical(cached.localAlbumCounts, localAlbumCounts) &&
|
||||
cached.query == query) {
|
||||
return cached.items;
|
||||
}
|
||||
|
||||
final unifiedDownloaded = _unifiedDownloadedItems(historyItems);
|
||||
|
||||
List<LocalLibraryItem> localItemsForMerge;
|
||||
if (filterMode == 'all') {
|
||||
localItemsForMerge = _filterLocalItems(localLibraryItems, query);
|
||||
} else {
|
||||
final localSingles = _localSingleItems(
|
||||
localLibraryItems,
|
||||
localAlbumCounts,
|
||||
);
|
||||
localItemsForMerge = _filterLocalItems(localSingles, query);
|
||||
}
|
||||
|
||||
final unifiedLocal = _unifiedLocalItems(localItemsForMerge);
|
||||
final downloadedPathKeys = _downloadedPathKeys(historyItems);
|
||||
|
||||
final dedupedUnifiedLocal = <UnifiedLibraryItem>[];
|
||||
for (final item in unifiedLocal) {
|
||||
final localSource = item.localItem;
|
||||
final localPathKeys = localSource != null
|
||||
? _localPathMatchKeys(localSource)
|
||||
: buildPathMatchKeys(item.filePath);
|
||||
final overlapsDownloaded = localPathKeys.any(downloadedPathKeys.contains);
|
||||
if (!overlapsDownloaded) {
|
||||
dedupedUnifiedLocal.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
final merged = <UnifiedLibraryItem>[
|
||||
...unifiedDownloaded,
|
||||
...dedupedUnifiedLocal,
|
||||
]..sort((a, b) => b.addedAt.compareTo(a.addedAt));
|
||||
|
||||
_unifiedItemsCache[filterMode] = _UnifiedCacheEntry(
|
||||
historyItems: historyItems,
|
||||
localItems: localLibraryItems,
|
||||
localAlbumCounts: localAlbumCounts,
|
||||
query: query,
|
||||
items: merged,
|
||||
);
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
// ignore: unused_element
|
||||
_FilterContentData _computeFilterContentData({
|
||||
required String filterMode,
|
||||
required List<DownloadHistoryItem> allHistoryItems,
|
||||
required List<_GroupedAlbum> filteredGroupedAlbums,
|
||||
required List<_GroupedLocalAlbum> filteredGroupedLocalAlbums,
|
||||
required Map<String, int> albumCounts,
|
||||
required Map<String, int> localAlbumCounts,
|
||||
required List<LocalLibraryItem> localLibraryItems,
|
||||
required LibraryCollectionsState collectionState,
|
||||
}) {
|
||||
final historyItems = _resolveHistoryItems(
|
||||
filterMode: filterMode,
|
||||
allHistoryItems: allHistoryItems,
|
||||
albumCounts: albumCounts,
|
||||
);
|
||||
final showFilteringIndicator = _shouldShowFilteringIndicator(
|
||||
allHistoryItems: allHistoryItems,
|
||||
filterMode: filterMode,
|
||||
);
|
||||
|
||||
final unifiedItems = _getUnifiedItems(
|
||||
filterMode: filterMode,
|
||||
historyItems: historyItems,
|
||||
localLibraryItems: localLibraryItems,
|
||||
localAlbumCounts: localAlbumCounts,
|
||||
);
|
||||
final filtered = _applyAdvancedFilters(unifiedItems);
|
||||
|
||||
// Remove tracks that are already in any playlist so they don't appear
|
||||
// in the main tracks list. When a track is removed from a playlist (or
|
||||
// the playlist is deleted) it will automatically reappear here because it
|
||||
// will no longer be in the set.
|
||||
final filteredUnifiedItems = !collectionState.hasPlaylistTracks
|
||||
? filtered
|
||||
: filtered
|
||||
.where(
|
||||
(item) =>
|
||||
!collectionState.isTrackInAnyPlaylist(item.collectionKey),
|
||||
)
|
||||
.toList(growable: false);
|
||||
|
||||
return _FilterContentData(
|
||||
historyItems: historyItems,
|
||||
unifiedItems: unifiedItems,
|
||||
filteredUnifiedItems: filteredUnifiedItems,
|
||||
filteredGroupedAlbums: filteredGroupedAlbums,
|
||||
filteredGroupedLocalAlbums: filteredGroupedLocalAlbums,
|
||||
showFilteringIndicator: showFilteringIndicator,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQueueHeaderSliver(
|
||||
BuildContext context,
|
||||
ColorScheme colorScheme,
|
||||
|
||||
@@ -50,20 +50,6 @@ class _GroupedLocalAlbum {
|
||||
int get displayTrackCount => trackCount ?? tracks.length;
|
||||
}
|
||||
|
||||
class _HistoryStats {
|
||||
final Map<String, int> albumCounts;
|
||||
final List<_GroupedAlbum> groupedAlbums;
|
||||
final int albumCount;
|
||||
final int singleTracks;
|
||||
|
||||
const _HistoryStats({
|
||||
required this.albumCounts,
|
||||
required this.groupedAlbums,
|
||||
required this.albumCount,
|
||||
required this.singleTracks,
|
||||
});
|
||||
}
|
||||
|
||||
class _FilterContentData {
|
||||
final List<DownloadHistoryItem> historyItems;
|
||||
final List<UnifiedLibraryItem> unifiedItems;
|
||||
@@ -366,22 +352,6 @@ final _queueLibraryCountsProvider =
|
||||
return LibraryDatabase.instance.getQueueCounts(request.toDbQuery());
|
||||
});
|
||||
|
||||
class _UnifiedCacheEntry {
|
||||
final List<DownloadHistoryItem> historyItems;
|
||||
final List<LocalLibraryItem> localItems;
|
||||
final Map<String, int> localAlbumCounts;
|
||||
final String query;
|
||||
final List<UnifiedLibraryItem> items;
|
||||
|
||||
const _UnifiedCacheEntry({
|
||||
required this.historyItems,
|
||||
required this.localItems,
|
||||
required this.localAlbumCounts,
|
||||
required this.query,
|
||||
required this.items,
|
||||
});
|
||||
}
|
||||
|
||||
class _QueueItemIdsSnapshot {
|
||||
final List<String> ids;
|
||||
|
||||
@@ -489,192 +459,3 @@ class _FileExistsListenableCache {
|
||||
_alwaysMissingNotifier.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
bool _queueHasMetadataValue(String? value) {
|
||||
return value != null && value.trim().isNotEmpty;
|
||||
}
|
||||
|
||||
String _queueNormalizedMetadataValue(String? value) {
|
||||
return value?.trim().toLowerCase() ?? '';
|
||||
}
|
||||
|
||||
DateTime? _queueParseReleaseDate(String? value) {
|
||||
final trimmed = value?.trim() ?? '';
|
||||
if (trimmed.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final parsed = DateTime.tryParse(trimmed);
|
||||
if (parsed != null) {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
final yearMatch = RegExp(r'(\d{4})').firstMatch(trimmed);
|
||||
if (yearMatch == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final year = int.tryParse(yearMatch.group(1)!);
|
||||
if (year == null || year <= 0) {
|
||||
return null;
|
||||
}
|
||||
return DateTime(year);
|
||||
}
|
||||
|
||||
bool _queueMatchesMetadataFilter({
|
||||
required String? filterMetadata,
|
||||
required String? artistName,
|
||||
required String? albumArtist,
|
||||
required String? releaseDate,
|
||||
required String? genre,
|
||||
required int? trackNumber,
|
||||
required int? discNumber,
|
||||
required String? isrc,
|
||||
required String? label,
|
||||
}) {
|
||||
if (filterMetadata == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final hasArtist = _queueHasMetadataValue(artistName);
|
||||
final hasAlbumArtist = _queueHasMetadataValue(albumArtist);
|
||||
final hasReleaseDate = _queueParseReleaseDate(releaseDate) != null;
|
||||
final hasGenre = _queueHasMetadataValue(genre);
|
||||
final hasTrackNumber = trackNumber != null && trackNumber > 0;
|
||||
final hasDiscNumber = discNumber != null && discNumber > 0;
|
||||
final hasLabel = _queueHasMetadataValue(label);
|
||||
final hasIncorrectIsrc = _queueHasIncorrectIsrcFormat(isrc);
|
||||
final isComplete =
|
||||
hasArtist &&
|
||||
hasAlbumArtist &&
|
||||
hasReleaseDate &&
|
||||
hasGenre &&
|
||||
hasTrackNumber &&
|
||||
hasDiscNumber &&
|
||||
hasLabel &&
|
||||
!hasIncorrectIsrc;
|
||||
|
||||
switch (filterMetadata) {
|
||||
case 'complete':
|
||||
return isComplete;
|
||||
case 'missing-any':
|
||||
return !isComplete;
|
||||
case 'missing-year':
|
||||
return !hasReleaseDate;
|
||||
case 'missing-genre':
|
||||
return !hasGenre;
|
||||
case 'missing-album-artist':
|
||||
return !hasAlbumArtist;
|
||||
case 'missing-track-number':
|
||||
return !hasTrackNumber;
|
||||
case 'missing-disc-number':
|
||||
return !hasDiscNumber;
|
||||
case 'missing-artist':
|
||||
return !hasArtist;
|
||||
case 'incorrect-isrc-format':
|
||||
return hasIncorrectIsrc;
|
||||
case 'missing-label':
|
||||
return !hasLabel;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool _queueHasIncorrectIsrcFormat(String? isrc) {
|
||||
final raw = isrc?.trim() ?? '';
|
||||
if (raw.isEmpty) return false;
|
||||
final normalized = raw.toUpperCase().replaceAll(RegExp(r'[-\s]'), '');
|
||||
return !RegExp(r'^[A-Z]{2}[A-Z0-9]{3}\d{7}$').hasMatch(normalized);
|
||||
}
|
||||
|
||||
bool _queueUnifiedItemMatchesMetadataFilter(
|
||||
UnifiedLibraryItem item,
|
||||
String? filterMetadata,
|
||||
) {
|
||||
return _queueMatchesMetadataFilter(
|
||||
filterMetadata: filterMetadata,
|
||||
artistName: item.artistName,
|
||||
albumArtist: item.albumArtist,
|
||||
releaseDate: item.releaseDate,
|
||||
genre: item.genre,
|
||||
trackNumber: item.trackNumber,
|
||||
discNumber: item.discNumber,
|
||||
isrc: item.isrc,
|
||||
label: item.label,
|
||||
);
|
||||
}
|
||||
|
||||
int _queueCompareOptionalText(
|
||||
String? left,
|
||||
String? right, {
|
||||
bool descending = false,
|
||||
}) {
|
||||
final normalizedLeft = _queueNormalizedMetadataValue(left);
|
||||
final normalizedRight = _queueNormalizedMetadataValue(right);
|
||||
final leftEmpty = normalizedLeft.isEmpty;
|
||||
final rightEmpty = normalizedRight.isEmpty;
|
||||
|
||||
if (leftEmpty && rightEmpty) {
|
||||
return 0;
|
||||
}
|
||||
if (leftEmpty) {
|
||||
return 1;
|
||||
}
|
||||
if (rightEmpty) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
final comparison = normalizedLeft.compareTo(normalizedRight);
|
||||
return descending ? -comparison : comparison;
|
||||
}
|
||||
|
||||
int _queueCompareOptionalDate(
|
||||
DateTime? left,
|
||||
DateTime? right, {
|
||||
bool descending = false,
|
||||
}) {
|
||||
if (left == null && right == null) {
|
||||
return 0;
|
||||
}
|
||||
if (left == null) {
|
||||
return 1;
|
||||
}
|
||||
if (right == null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
final comparison = left.compareTo(right);
|
||||
return descending ? -comparison : comparison;
|
||||
}
|
||||
|
||||
Map<String, List<String>> _filterHistoryInIsolate(Map<String, Object> payload) {
|
||||
final entries = (payload['entries'] as List).cast<List<Object?>>();
|
||||
final albumCounts = Map<String, int>.from(payload['albumCounts'] as Map);
|
||||
final query = (payload['query'] as String?) ?? '';
|
||||
final hasQuery = query.isNotEmpty;
|
||||
|
||||
final allIds = <String>[];
|
||||
final albumIds = <String>[];
|
||||
final singleIds = <String>[];
|
||||
|
||||
for (final entry in entries) {
|
||||
final id = entry[0] as String;
|
||||
final albumKey = entry[1] as String;
|
||||
if (hasQuery) {
|
||||
final searchKey = entry[2] as String;
|
||||
if (!searchKey.contains(query)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
allIds.add(id);
|
||||
final count = albumCounts[albumKey] ?? 0;
|
||||
if (count > 1) {
|
||||
albumIds.add(id);
|
||||
} else if (count == 1) {
|
||||
singleIds.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
return {'all': allIds, 'albums': albumIds, 'singles': singleIds};
|
||||
}
|
||||
|
||||
@@ -207,41 +207,6 @@ extension _QueueTabNavigation on _QueueTabState {
|
||||
).then((_) => _searchFocusNode.unfocus());
|
||||
}
|
||||
|
||||
List<DownloadHistoryItem> _filterHistoryItems(
|
||||
List<DownloadHistoryItem> items,
|
||||
String filterMode,
|
||||
Map<String, int> albumCounts, [
|
||||
String searchQuery = '',
|
||||
]) {
|
||||
var filteredItems = items;
|
||||
if (searchQuery.isNotEmpty) {
|
||||
final query = searchQuery;
|
||||
filteredItems = items.where((item) {
|
||||
final searchKey = _historySearchKeyForItem(item);
|
||||
return searchKey.contains(query);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
if (filterMode == 'all') return filteredItems;
|
||||
|
||||
switch (filterMode) {
|
||||
case 'albums':
|
||||
return filteredItems.where((item) {
|
||||
final key =
|
||||
'${item.albumName.toLowerCase()}|${(item.albumArtist ?? item.artistName).toLowerCase()}';
|
||||
return (albumCounts[key] ?? 0) > 1;
|
||||
}).toList();
|
||||
case 'singles':
|
||||
return filteredItems.where((item) {
|
||||
final key =
|
||||
'${item.albumName.toLowerCase()}|${(item.albumArtist ?? item.artistName).toLowerCase()}';
|
||||
return (albumCounts[key] ?? 0) == 1;
|
||||
}).toList();
|
||||
default:
|
||||
return filteredItems;
|
||||
}
|
||||
}
|
||||
|
||||
void _navigateWithUnfocus(Route<dynamic> route) {
|
||||
_searchFocusNode.unfocus();
|
||||
Navigator.of(context).push(route).then((_) => _searchFocusNode.unfocus());
|
||||
@@ -560,5 +525,4 @@ extension _QueueTabNavigation on _QueueTabState {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -112,23 +112,6 @@ class DownloadedEmbeddedCoverResolver {
|
||||
}
|
||||
}
|
||||
|
||||
static void invalidatePathsNotIn(Set<String> validCleanPaths) {
|
||||
if (validCleanPaths.isEmpty) {
|
||||
final keys = _cache.keys.toList(growable: false);
|
||||
for (final key in keys) {
|
||||
invalidate(key);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final staleKeys = _cache.keys
|
||||
.where((path) => !validCleanPaths.contains(path))
|
||||
.toList(growable: false);
|
||||
for (final key in staleKeys) {
|
||||
invalidate(key);
|
||||
}
|
||||
}
|
||||
|
||||
static void _touch(String cleanPath, _EmbeddedCoverCacheEntry entry) {
|
||||
_cache
|
||||
..remove(cleanPath)
|
||||
|
||||
Reference in New Issue
Block a user