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:
zarzet
2026-07-14 17:53:58 +07:00
parent 4ffd5cc701
commit e2a2e8a914
4 changed files with 0 additions and 973 deletions
-701
View File
@@ -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,
-219
View File
@@ -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};
}
-36
View File
@@ -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)