Files
SpotiFLAC-Mobile/lib/screens/queue_tab_helpers.dart
T
zarzet e2a2e8a914 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.
2026-07-14 17:53:58 +07:00

462 lines
14 KiB
Dart

part of 'queue_tab.dart';
class _GroupedAlbum {
final String albumName;
final String artistName;
final String? coverUrl;
final String sampleFilePath;
final List<DownloadHistoryItem> 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<LocalLibraryItem> 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<DownloadHistoryItem> historyItems;
final List<UnifiedLibraryItem> unifiedItems;
final List<UnifiedLibraryItem> 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<UnifiedLibraryItem> items;
final List<DownloadHistoryItem> historyItems;
final List<LocalLibraryItem> 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 = <UnifiedLibraryItem>[];
final historyItems = <DownloadHistoryItem>[];
final localItems = <LocalLibraryItem>[];
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.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 = <UnifiedLibraryItem>[];
final historyItems = <DownloadHistoryItem>[];
final localItems = <LocalLibraryItem>[];
for (final row in rows) {
final source = row['source'] as String? ?? '';
final itemJson = Map<String, dynamic>.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.family<QueueLibraryCounts, _QueueLibraryCountsRequest>((
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<String> 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<String, bool> _cache = {};
final Map<String, int> _missCounts = {};
final Map<String, ValueNotifier<bool>> _notifiers = {};
final ValueNotifier<bool> _alwaysMissingNotifier = ValueNotifier(false);
final Set<String> _pendingChecks = {};
ValueListenable<bool> 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<bool>(_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();
}
}