part of 'queue_tab.dart'; class _GroupedAlbum { final String albumName; final String artistName; final String? coverUrl; final String sampleFilePath; final List tracks; final int? trackCount; final DateTime latestDownload; final String searchKey; _GroupedAlbum({ required this.albumName, required this.artistName, this.coverUrl, required this.sampleFilePath, required this.tracks, this.trackCount, required this.latestDownload, }) : searchKey = '${albumName.toLowerCase()}|${artistName.toLowerCase()}'; String get key => '$albumName|$artistName'; int get displayTrackCount => trackCount ?? tracks.length; } class _GroupedLocalAlbum { final String albumKey; final String albumName; final String artistName; final String? coverPath; final List tracks; final int? trackCount; final DateTime latestScanned; final String searchKey; _GroupedLocalAlbum({ required this.albumKey, required this.albumName, required this.artistName, this.coverPath, required this.tracks, this.trackCount, required this.latestScanned, }) : searchKey = '${albumName.toLowerCase()}|${artistName.toLowerCase()}'; String get key => albumKey; int get displayTrackCount => trackCount ?? tracks.length; } class _FilterContentData { final List historyItems; final List unifiedItems; final List filteredUnifiedItems; final List<_GroupedAlbum> filteredGroupedAlbums; final List<_GroupedLocalAlbum> filteredGroupedLocalAlbums; final bool showFilteringIndicator; final int? totalTrackCountOverride; final int? totalAlbumCountOverride; const _FilterContentData({ required this.historyItems, required this.unifiedItems, required this.filteredUnifiedItems, required this.filteredGroupedAlbums, required this.filteredGroupedLocalAlbums, required this.showFilteringIndicator, this.totalTrackCountOverride, this.totalAlbumCountOverride, }); int get totalTrackCount => totalTrackCountOverride ?? filteredUnifiedItems.length; int get totalAlbumCount => totalAlbumCountOverride ?? filteredGroupedAlbums.length + filteredGroupedLocalAlbums.length; } class _QueueLibraryPageRequest { final String filterMode; final int limit; final int offset; final String searchQuery; final String? filterSource; final String? filterQuality; final String? filterFormat; final String? filterMetadata; final String sortMode; final bool localLibraryEnabled; const _QueueLibraryPageRequest({ required this.filterMode, required this.limit, required this.offset, required this.searchQuery, required this.filterSource, required this.filterQuality, required this.filterFormat, required this.filterMetadata, required this.sortMode, required this.localLibraryEnabled, }); QueueLibraryDbQuery toDbQuery() => QueueLibraryDbQuery( limit: limit, offset: offset, filterMode: filterMode, searchQuery: searchQuery, source: filterSource, quality: filterQuality, format: filterFormat, metadata: filterMetadata, sortMode: sortMode, includeLocal: localLibraryEnabled, ); @override bool operator ==(Object other) => identical(this, other) || other is _QueueLibraryPageRequest && filterMode == other.filterMode && limit == other.limit && offset == other.offset && searchQuery == other.searchQuery && filterSource == other.filterSource && filterQuality == other.filterQuality && filterFormat == other.filterFormat && filterMetadata == other.filterMetadata && sortMode == other.sortMode && localLibraryEnabled == other.localLibraryEnabled; @override int get hashCode => Object.hash( filterMode, limit, offset, searchQuery, filterSource, filterQuality, filterFormat, filterMetadata, sortMode, localLibraryEnabled, ); } class _QueueLibraryCountsRequest { final String searchQuery; final String? filterSource; final String? filterQuality; final String? filterFormat; final String? filterMetadata; final bool localLibraryEnabled; const _QueueLibraryCountsRequest({ required this.searchQuery, required this.filterSource, required this.filterQuality, required this.filterFormat, required this.filterMetadata, required this.localLibraryEnabled, }); QueueLibraryDbQuery toDbQuery() => QueueLibraryDbQuery( searchQuery: searchQuery, source: filterSource, quality: filterQuality, format: filterFormat, metadata: filterMetadata, includeLocal: localLibraryEnabled, ); @override bool operator ==(Object other) => identical(this, other) || other is _QueueLibraryCountsRequest && searchQuery == other.searchQuery && filterSource == other.filterSource && filterQuality == other.filterQuality && filterFormat == other.filterFormat && filterMetadata == other.filterMetadata && localLibraryEnabled == other.localLibraryEnabled; @override int get hashCode => Object.hash( searchQuery, filterSource, filterQuality, filterFormat, filterMetadata, localLibraryEnabled, ); } class _QueueLibraryPageData { final List items; final List historyItems; final List localItems; final List<_GroupedAlbum> groupedAlbums; final List<_GroupedLocalAlbum> groupedLocalAlbums; const _QueueLibraryPageData({ this.items = const [], this.historyItems = const [], this.localItems = const [], this.groupedAlbums = const [], this.groupedLocalAlbums = const [], }); factory _QueueLibraryPageData.combine(List<_QueueLibraryPageData> pages) { if (pages.isEmpty) return const _QueueLibraryPageData(); if (pages.length == 1) return pages.first; final items = []; final historyItems = []; final localItems = []; final groupedAlbums = <_GroupedAlbum>[]; final groupedLocalAlbums = <_GroupedLocalAlbum>[]; for (final page in pages) { items.addAll(page.items); historyItems.addAll(page.historyItems); localItems.addAll(page.localItems); groupedAlbums.addAll(page.groupedAlbums); groupedLocalAlbums.addAll(page.groupedLocalAlbums); } return _QueueLibraryPageData( items: items, historyItems: historyItems, localItems: localItems, groupedAlbums: groupedAlbums, groupedLocalAlbums: groupedLocalAlbums, ); } _FilterContentData toFilterContentData( LibraryCollectionsState collectionState, { int? totalTrackCount, int? totalAlbumCount, }) { final filteredItems = !collectionState.hasPlaylistTracks ? items : items .where( (item) => !collectionState.isTrackInAnyPlaylist(item.collectionKey), ) .toList(growable: false); return _FilterContentData( historyItems: historyItems, unifiedItems: items, filteredUnifiedItems: filteredItems, filteredGroupedAlbums: groupedAlbums, filteredGroupedLocalAlbums: groupedLocalAlbums, showFilteringIndicator: false, totalTrackCountOverride: totalTrackCount, totalAlbumCountOverride: totalAlbumCount, ); } } final _queueLibraryPageProvider = FutureProvider.autoDispose .family<_QueueLibraryPageData, _QueueLibraryPageRequest>(( ref, request, ) async { ref.watch( downloadHistoryProvider.select((state) => state.loadedIndexVersion), ); ref.watch( localLibraryProvider.select((state) => state.loadedIndexVersion), ); final dbQuery = request.toDbQuery(); if (request.filterMode == 'albums') { final rows = await LibraryDatabase.instance.getQueueAlbumPage(dbQuery); final groupedAlbums = <_GroupedAlbum>[]; final groupedLocalAlbums = <_GroupedLocalAlbum>[]; for (final row in rows) { final source = row['queue_source'] as String? ?? ''; final latestMillis = (row['sort_added'] as num?)?.toInt() ?? 0; final latest = DateTime.fromMillisecondsSinceEpoch(latestMillis); if (source == 'local') { groupedLocalAlbums.add( _GroupedLocalAlbum( albumKey: row['album_key'] as String? ?? '', albumName: row['album_name'] as String? ?? '', artistName: row['artist_name'] as String? ?? '', coverPath: row['cover_path'] as String?, tracks: const [], trackCount: (row['track_count'] as num?)?.toInt() ?? 0, latestScanned: latest, ), ); } else if (source == 'downloaded') { groupedAlbums.add( _GroupedAlbum( albumName: row['album_name'] as String? ?? '', artistName: row['artist_name'] as String? ?? '', coverUrl: row['cover_url'] as String?, sampleFilePath: row['sample_file_path'] as String? ?? '', tracks: const [], trackCount: (row['track_count'] as num?)?.toInt() ?? 0, latestDownload: latest, ), ); } } return _QueueLibraryPageData( groupedAlbums: groupedAlbums, groupedLocalAlbums: groupedLocalAlbums, ); } final rows = await LibraryDatabase.instance.getQueueTrackPage(dbQuery); final items = []; final historyItems = []; final localItems = []; for (final row in rows) { final source = row['source'] as String? ?? ''; final itemJson = Map.from(row['item'] as Map); if (source == 'local') { final item = LocalLibraryItem.fromJson(itemJson); localItems.add(item); items.add(UnifiedLibraryItem.fromLocalLibrary(item)); } else if (source == 'downloaded') { final item = DownloadHistoryItem.fromJson(itemJson); historyItems.add(item); items.add(UnifiedLibraryItem.fromDownloadHistory(item)); } } return _QueueLibraryPageData( items: items, historyItems: historyItems, localItems: localItems, ); }); final _queueLibraryCountsProvider = FutureProvider.autoDispose .family(( ref, request, ) async { ref.watch( downloadHistoryProvider.select((state) => state.loadedIndexVersion), ); ref.watch( localLibraryProvider.select((state) => state.loadedIndexVersion), ); return LibraryDatabase.instance.getQueueCounts(request.toDbQuery()); }); class _QueueItemIdsSnapshot { final List ids; const _QueueItemIdsSnapshot(this.ids); @override bool operator ==(Object other) => identical(this, other) || other is _QueueItemIdsSnapshot && listEquals(ids, other.ids); @override int get hashCode => Object.hashAll(ids); } class _FileExistsListenableCache { static const int _maxCacheSize = 500; final Map _cache = {}; final Map _missCounts = {}; final Map> _notifiers = {}; final ValueNotifier _alwaysMissingNotifier = ValueNotifier(false); final Set _pendingChecks = {}; ValueListenable listenable(String? filePath) { final cleanPath = DownloadedEmbeddedCoverResolver.cleanFilePath(filePath); if (cleanPath.isEmpty) return _alwaysMissingNotifier; final existingNotifier = _notifiers[cleanPath]; if (existingNotifier != null) { final cached = _cache[cleanPath]; if (cached != null && existingNotifier.value != cached) { existingNotifier.value = cached; } else if (cached == null) { _startCheck(cleanPath); } return existingNotifier; } if (_notifiers.length >= _maxCacheSize) { final oldestKey = _notifiers.keys.first; _notifiers.remove(oldestKey)?.dispose(); _cache.remove(oldestKey); } final notifier = ValueNotifier(_cache[cleanPath] ?? true); _notifiers[cleanPath] = notifier; _startCheck(cleanPath); return notifier; } void _startCheck(String cleanPath) { if (_pendingChecks.contains(cleanPath)) { return; } final cached = _cache[cleanPath]; if (cached != null) { final notifier = _notifiers[cleanPath]; if (notifier != null && notifier.value != cached) { notifier.value = cached; } return; } _pendingChecks.add(cleanPath); Future.microtask(() async { bool exists; try { exists = await fileExists(cleanPath); } catch (_) { _pendingChecks.remove(cleanPath); Timer(const Duration(milliseconds: 700), () => _startCheck(cleanPath)); return; } _pendingChecks.remove(cleanPath); if (exists) { _missCounts.remove(cleanPath); _cache[cleanPath] = true; } else { final misses = (_missCounts[cleanPath] ?? 0) + 1; _missCounts[cleanPath] = misses; if (misses < 2) { Timer( const Duration(milliseconds: 700), () => _startCheck(cleanPath), ); return; } _cache[cleanPath] = false; } final notifier = _notifiers[cleanPath]; final value = _cache[cleanPath] ?? true; if (notifier != null && notifier.value != value) { notifier.value = value; } }); } void dispose() { for (final notifier in _notifiers.values) { notifier.dispose(); } _notifiers.clear(); _missCounts.clear(); _alwaysMissingNotifier.dispose(); } }