Files
SpotiFLAC-Mobile/lib/screens/queue_tab_helpers.dart
T
zarzet 3a7419ec9f refactor: split large screen files into part files and DRY platform bridge
- Extract home_tab.dart helpers/widgets into home_tab_helpers.dart and home_tab_widgets.dart using Dart part files
- Extract queue_tab.dart helpers/widgets into queue_tab_helpers.dart and queue_tab_widgets.dart using Dart part files
- Extract track_metadata_edit_sheet.dart from track_metadata_screen.dart using Dart part file
- Refactor _FileExistsListenableCache into a standalone class in queue_tab_helpers.dart
- Fix artist_screen.dart: replace unreliable findAncestorStateOfType with GlobalKey for _FetchingProgressDialog progress updates
- DRY platform_bridge.dart: extract common JSON decode patterns into reusable helper methods (_decodeRequiredMapResult, _decodeNullableMapResult, _decodeMapListResult, _decodeStringListResult)
2026-05-02 00:27:51 +07:00

1102 lines
30 KiB
Dart

part of 'queue_tab.dart';
enum LibraryItemSource { downloaded, local }
class UnifiedLibraryItem {
final String id;
final String trackName;
final String artistName;
final String albumName;
final String? coverUrl;
final String? localCoverPath;
final String filePath;
final String? quality;
final DateTime addedAt;
final LibraryItemSource source;
final DownloadHistoryItem? historyItem;
final LocalLibraryItem? localItem;
UnifiedLibraryItem({
required this.id,
required this.trackName,
required this.artistName,
required this.albumName,
this.coverUrl,
this.localCoverPath,
required this.filePath,
this.quality,
required this.addedAt,
required this.source,
this.historyItem,
this.localItem,
});
factory UnifiedLibraryItem.fromDownloadHistory(DownloadHistoryItem item) {
return UnifiedLibraryItem(
id: 'dl_${item.id}',
trackName: item.trackName,
artistName: item.artistName,
albumName: item.albumName,
coverUrl: item.coverUrl,
filePath: item.filePath,
quality: buildDisplayAudioQuality(
bitDepth: item.bitDepth,
sampleRate: item.sampleRate,
storedQuality: item.quality,
),
addedAt: item.downloadedAt,
source: LibraryItemSource.downloaded,
historyItem: item,
);
}
factory UnifiedLibraryItem.fromLocalLibrary(LocalLibraryItem item) {
String? quality;
if (item.bitrate != null && item.bitrate! > 0) {
quality = buildDisplayAudioQuality(
bitrateKbps: item.bitrate,
format: item.format,
);
} else if (item.bitDepth != null &&
item.bitDepth! > 0 &&
item.sampleRate != null) {
quality = buildDisplayAudioQuality(
bitDepth: item.bitDepth,
sampleRate: item.sampleRate,
);
}
return UnifiedLibraryItem(
id: 'local_${item.id}',
trackName: item.trackName,
artistName: item.artistName,
albumName: item.albumName,
coverUrl: null,
localCoverPath: item.coverPath,
filePath: item.filePath,
quality: quality,
addedAt: item.fileModTime != null
? DateTime.fromMillisecondsSinceEpoch(item.fileModTime!)
: item.scannedAt,
source: LibraryItemSource.local,
localItem: item,
);
}
bool get hasCover =>
coverUrl != null ||
(localCoverPath != null && localCoverPath!.isNotEmpty);
String? get albumArtist => historyItem?.albumArtist ?? localItem?.albumArtist;
String? get releaseDate => historyItem?.releaseDate ?? localItem?.releaseDate;
String? get genre => historyItem?.genre ?? localItem?.genre;
String get searchKey =>
'${trackName.toLowerCase()}|${artistName.toLowerCase()}|${albumName.toLowerCase()}';
String get albumKey =>
'${albumName.toLowerCase()}|${artistName.toLowerCase()}';
/// Returns the collection key used to match this item against playlist
/// entries. Uses the same logic as [trackCollectionKey] from the collections
/// provider: prefer ISRC, fall back to source:id.
String get collectionKey {
if (historyItem != null) {
final isrc = historyItem!.isrc?.trim();
if (isrc != null && isrc.isNotEmpty) return 'isrc:${isrc.toUpperCase()}';
final source = historyItem!.service.trim().isNotEmpty
? historyItem!.service.trim()
: 'builtin';
return '$source:${historyItem!.id}';
}
if (localItem != null) {
final isrc = localItem!.isrc?.trim();
if (isrc != null && isrc.isNotEmpty) return 'isrc:${isrc.toUpperCase()}';
return 'local:${localItem!.id}';
}
return 'builtin:$id';
}
Track toTrack() {
if (historyItem != null) {
final h = historyItem!;
return Track(
id: h.id,
name: h.trackName,
artistName: h.artistName,
albumName: h.albumName,
albumArtist: h.albumArtist,
coverUrl: h.coverUrl,
isrc: h.isrc,
duration: h.duration ?? 0,
trackNumber: h.trackNumber,
discNumber: h.discNumber,
releaseDate: h.releaseDate,
source: h.service,
);
}
if (localItem != null) {
final l = localItem!;
return Track(
id: l.id,
name: l.trackName,
artistName: l.artistName,
albumName: l.albumName,
albumArtist: l.albumArtist,
coverUrl: l.coverPath,
isrc: l.isrc,
duration: l.duration ?? 0,
trackNumber: l.trackNumber,
discNumber: l.discNumber,
releaseDate: l.releaseDate,
source: 'local',
);
}
return Track(
id: id,
name: trackName,
artistName: artistName,
albumName: albumName,
coverUrl: coverUrl,
duration: 0,
);
}
}
class _GroupedAlbum {
final String albumName;
final String artistName;
final String? coverUrl;
final String sampleFilePath;
final List<DownloadHistoryItem> tracks;
final DateTime latestDownload;
final String searchKey;
_GroupedAlbum({
required this.albumName,
required this.artistName,
this.coverUrl,
required this.sampleFilePath,
required this.tracks,
required this.latestDownload,
}) : searchKey = '${albumName.toLowerCase()}|${artistName.toLowerCase()}';
String get key => '$albumName|$artistName';
}
class _GroupedLocalAlbum {
final String albumName;
final String artistName;
final String? coverPath;
final List<LocalLibraryItem> tracks;
final DateTime latestScanned;
final String searchKey;
_GroupedLocalAlbum({
required this.albumName,
required this.artistName,
this.coverPath,
required this.tracks,
required this.latestScanned,
}) : searchKey = '${albumName.toLowerCase()}|${artistName.toLowerCase()}';
String get key => '$albumName|$artistName';
}
class _HistoryStats {
final Map<String, int> albumCounts;
final Map<String, int> localAlbumCounts;
final List<_GroupedAlbum> groupedAlbums;
final List<_GroupedLocalAlbum> groupedLocalAlbums;
final int albumCount;
final int singleTracks;
final int localAlbumCount;
final int localSingleTracks;
const _HistoryStats({
required this.albumCounts,
this.localAlbumCounts = const {},
required this.groupedAlbums,
this.groupedLocalAlbums = const [],
required this.albumCount,
required this.singleTracks,
this.localAlbumCount = 0,
this.localSingleTracks = 0,
});
int get totalAlbumCount => albumCount + localAlbumCount;
int get totalSingleTracks => singleTracks + localSingleTracks;
}
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;
const _FilterContentData({
required this.historyItems,
required this.unifiedItems,
required this.filteredUnifiedItems,
required this.filteredGroupedAlbums,
required this.filteredGroupedLocalAlbums,
required this.showFilteringIndicator,
});
int get totalTrackCount => filteredUnifiedItems.length;
int get totalAlbumCount =>
filteredGroupedAlbums.length + filteredGroupedLocalAlbums.length;
}
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;
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 _QueueGroupedAlbumFilterRequest {
final String searchQuery;
final String? filterSource;
final String? filterQuality;
final String? filterFormat;
final String? filterMetadata;
final String sortMode;
const _QueueGroupedAlbumFilterRequest({
required this.searchQuery,
required this.filterSource,
required this.filterQuality,
required this.filterFormat,
required this.filterMetadata,
required this.sortMode,
});
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is _QueueGroupedAlbumFilterRequest &&
searchQuery == other.searchQuery &&
filterSource == other.filterSource &&
filterQuality == other.filterQuality &&
filterFormat == other.filterFormat &&
filterMetadata == other.filterMetadata &&
sortMode == other.sortMode;
@override
int get hashCode => Object.hash(
searchQuery,
filterSource,
filterQuality,
filterFormat,
filterMetadata,
sortMode,
);
}
class _QueueHistoryStatsMemoEntry {
final List<DownloadHistoryItem> historyItems;
final List<LocalLibraryItem> localItems;
final _HistoryStats stats;
const _QueueHistoryStatsMemoEntry({
required this.historyItems,
required this.localItems,
required this.stats,
});
}
_QueueHistoryStatsMemoEntry? _queueHistoryStatsMemo;
class _FileExistsListenableCache {
static const int _maxCacheSize = 500;
final Map<String, bool> _cache = {};
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 {
final exists = await fileExists(cleanPath);
_pendingChecks.remove(cleanPath);
_cache[cleanPath] = exists;
final notifier = _notifiers[cleanPath];
if (notifier != null && notifier.value != exists) {
notifier.value = exists;
}
});
}
void dispose() {
for (final notifier in _notifiers.values) {
notifier.dispose();
}
_notifiers.clear();
_alwaysMissingNotifier.dispose();
}
}
String _queueHistoryAlbumKey(String albumName, String artistName) {
return '${albumName.toLowerCase()}|${artistName.toLowerCase()}';
}
String _queueFileExtLower(String filePath) {
final slashIndex = filePath.lastIndexOf('/');
final dotIndex = filePath.lastIndexOf('.');
if (dotIndex == -1 || dotIndex < slashIndex + 1) {
return '';
}
return filePath.substring(dotIndex + 1).toLowerCase();
}
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? albumArtist,
required String? releaseDate,
required String? genre,
}) {
if (filterMetadata == null) {
return true;
}
final hasAlbumArtist = _queueHasMetadataValue(albumArtist);
final hasReleaseDate = _queueParseReleaseDate(releaseDate) != null;
final hasGenre = _queueHasMetadataValue(genre);
final isComplete = hasAlbumArtist && hasReleaseDate && hasGenre;
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;
default:
return true;
}
}
bool _queueUnifiedItemMatchesMetadataFilter(
UnifiedLibraryItem item,
String? filterMetadata,
) {
return _queueMatchesMetadataFilter(
filterMetadata: filterMetadata,
albumArtist: item.albumArtist,
releaseDate: item.releaseDate,
genre: item.genre,
);
}
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;
}
DateTime? _queueGroupedAlbumReleaseDate(_GroupedAlbum album) {
for (final track in album.tracks) {
final releaseDate = _queueParseReleaseDate(track.releaseDate);
if (releaseDate != null) {
return releaseDate;
}
}
return null;
}
DateTime? _queueGroupedLocalAlbumReleaseDate(_GroupedLocalAlbum album) {
for (final track in album.tracks) {
final releaseDate = _queueParseReleaseDate(track.releaseDate);
if (releaseDate != null) {
return releaseDate;
}
}
return null;
}
String? _queueGroupedAlbumGenre(_GroupedAlbum album) {
for (final track in album.tracks) {
if (_queueHasMetadataValue(track.genre)) {
return track.genre;
}
}
return null;
}
String? _queueGroupedLocalAlbumGenre(_GroupedLocalAlbum album) {
for (final track in album.tracks) {
if (_queueHasMetadataValue(track.genre)) {
return track.genre;
}
}
return null;
}
String? _queueLocalQualityLabel(LocalLibraryItem item) {
if (item.bitrate != null && item.bitrate! > 0) {
return '${item.bitrate}kbps';
}
if (item.bitDepth == null || item.bitDepth == 0 || item.sampleRate == null) {
return null;
}
return '${item.bitDepth}bit/${(item.sampleRate! / 1000).toStringAsFixed(1)}kHz';
}
bool _queuePassesQualityFilter(String? filterQuality, String? quality) {
if (filterQuality == null) return true;
if (quality == null) return filterQuality == 'lossy';
final normalized = quality.toLowerCase();
switch (filterQuality) {
case 'hires':
return normalized.startsWith('24');
case 'cd':
return normalized.startsWith('16');
case 'lossy':
return !normalized.startsWith('24') && !normalized.startsWith('16');
default:
return true;
}
}
bool _queuePassesFormatFilter(String? filterFormat, String filePath) {
if (filterFormat == null) return true;
return _queueFileExtLower(filePath) == filterFormat;
}
_HistoryStats _buildQueueHistoryStats(
List<DownloadHistoryItem> items, [
List<LocalLibraryItem> localItems = const [],
]) {
final memo = _queueHistoryStatsMemo;
if (memo != null &&
identical(memo.historyItems, items) &&
identical(memo.localItems, localItems)) {
return memo.stats;
}
final albumCounts = <String, int>{};
final albumMap = <String, List<DownloadHistoryItem>>{};
for (final item in items) {
final key = _queueHistoryAlbumKey(
item.albumName,
item.albumArtist ?? item.artistName,
);
albumCounts[key] = (albumCounts[key] ?? 0) + 1;
albumMap.putIfAbsent(key, () => []).add(item);
}
var singleTracks = 0;
var albumCount = 0;
for (final count in albumCounts.values) {
if (count > 1) {
albumCount++;
} else {
singleTracks += count;
}
}
final groupedAlbums = <_GroupedAlbum>[];
albumMap.forEach((_, tracks) {
if (tracks.length <= 1) return;
tracks.sort((a, b) {
final aNum = a.trackNumber ?? 999;
final bNum = b.trackNumber ?? 999;
return aNum.compareTo(bNum);
});
groupedAlbums.add(
_GroupedAlbum(
albumName: tracks.first.albumName,
artistName: tracks.first.albumArtist ?? tracks.first.artistName,
coverUrl: tracks.first.coverUrl,
sampleFilePath: tracks.first.filePath,
tracks: tracks,
latestDownload: tracks
.map((t) => t.downloadedAt)
.reduce((a, b) => a.isAfter(b) ? a : b),
),
);
});
groupedAlbums.sort((a, b) => b.latestDownload.compareTo(a.latestDownload));
final downloadedPathKeys = <String>{};
for (final item in items) {
downloadedPathKeys.addAll(buildPathMatchKeys(item.filePath));
}
final dedupedLocalItems = localItems
.where((item) {
final localPathKeys = buildPathMatchKeys(item.filePath);
return !localPathKeys.any(downloadedPathKeys.contains);
})
.toList(growable: false);
final localAlbumCounts = <String, int>{};
final localAlbumMap = <String, List<LocalLibraryItem>>{};
for (final item in dedupedLocalItems) {
final key = _queueHistoryAlbumKey(
item.albumName,
item.albumArtist ?? item.artistName,
);
localAlbumCounts[key] = (localAlbumCounts[key] ?? 0) + 1;
localAlbumMap.putIfAbsent(key, () => []).add(item);
}
var localAlbumCount = 0;
var localSingleTracks = 0;
for (final count in localAlbumCounts.values) {
if (count > 1) {
localAlbumCount++;
} else {
localSingleTracks++;
}
}
final groupedLocalAlbums = <_GroupedLocalAlbum>[];
localAlbumMap.forEach((_, tracks) {
if (tracks.length <= 1) return;
tracks.sort((a, b) {
final aNum = a.trackNumber ?? 999;
final bNum = b.trackNumber ?? 999;
return aNum.compareTo(bNum);
});
groupedLocalAlbums.add(
_GroupedLocalAlbum(
albumName: tracks.first.albumName,
artistName: tracks.first.albumArtist ?? tracks.first.artistName,
coverPath: tracks
.firstWhere(
(t) => t.coverPath != null && t.coverPath!.isNotEmpty,
orElse: () => tracks.first,
)
.coverPath,
tracks: tracks,
latestScanned: tracks
.map((t) => t.scannedAt)
.reduce((a, b) => a.isAfter(b) ? a : b),
),
);
});
groupedLocalAlbums.sort((a, b) => b.latestScanned.compareTo(a.latestScanned));
final stats = _HistoryStats(
albumCounts: albumCounts,
localAlbumCounts: localAlbumCounts,
groupedAlbums: groupedAlbums,
groupedLocalAlbums: groupedLocalAlbums,
albumCount: albumCount,
singleTracks: singleTracks,
localAlbumCount: localAlbumCount,
localSingleTracks: localSingleTracks,
);
_queueHistoryStatsMemo = _QueueHistoryStatsMemoEntry(
historyItems: items,
localItems: localItems,
stats: stats,
);
return stats;
}
List<_GroupedAlbum> _queueFilterGroupedAlbums(
List<_GroupedAlbum> albums,
_QueueGroupedAlbumFilterRequest request,
) {
if (request.filterSource == 'local') return const [];
if (request.filterSource == null &&
request.filterQuality == null &&
request.filterFormat == null &&
request.filterMetadata == null &&
request.searchQuery.isEmpty &&
request.sortMode == 'latest') {
return albums;
}
final result = <_GroupedAlbum>[];
for (final album in albums) {
if (request.searchQuery.isNotEmpty &&
!album.searchKey.contains(request.searchQuery)) {
continue;
}
if (request.filterQuality != null ||
request.filterFormat != null ||
request.filterMetadata != null) {
var hasMatchingTrack = false;
for (final track in album.tracks) {
if (!_queuePassesQualityFilter(request.filterQuality, track.quality)) {
continue;
}
if (!_queuePassesFormatFilter(request.filterFormat, track.filePath)) {
continue;
}
if (!_queueMatchesMetadataFilter(
filterMetadata: request.filterMetadata,
albumArtist: track.albumArtist,
releaseDate: track.releaseDate,
genre: track.genre,
)) {
continue;
}
hasMatchingTrack = true;
break;
}
if (!hasMatchingTrack) continue;
}
result.add(album);
}
switch (request.sortMode) {
case 'oldest':
result.sort((a, b) => a.latestDownload.compareTo(b.latestDownload));
case 'artist-asc':
result.sort((a, b) {
final comparison = _queueCompareOptionalText(
a.artistName,
b.artistName,
);
if (comparison != 0) {
return comparison;
}
return _queueCompareOptionalText(a.albumName, b.albumName);
});
case 'artist-desc':
result.sort((a, b) {
final comparison = _queueCompareOptionalText(
a.artistName,
b.artistName,
descending: true,
);
if (comparison != 0) {
return comparison;
}
return _queueCompareOptionalText(a.albumName, b.albumName);
});
case 'a-z':
result.sort(
(a, b) =>
a.albumName.toLowerCase().compareTo(b.albumName.toLowerCase()),
);
case 'z-a':
result.sort(
(a, b) =>
b.albumName.toLowerCase().compareTo(a.albumName.toLowerCase()),
);
case 'album-asc':
result.sort(
(a, b) => _queueCompareOptionalText(a.albumName, b.albumName),
);
case 'album-desc':
result.sort(
(a, b) => _queueCompareOptionalText(
a.albumName,
b.albumName,
descending: true,
),
);
case 'release-oldest':
result.sort((a, b) {
final comparison = _queueCompareOptionalDate(
_queueGroupedAlbumReleaseDate(a),
_queueGroupedAlbumReleaseDate(b),
);
if (comparison != 0) {
return comparison;
}
return _queueCompareOptionalText(a.albumName, b.albumName);
});
case 'release-newest':
result.sort((a, b) {
final comparison = _queueCompareOptionalDate(
_queueGroupedAlbumReleaseDate(a),
_queueGroupedAlbumReleaseDate(b),
descending: true,
);
if (comparison != 0) {
return comparison;
}
return _queueCompareOptionalText(a.albumName, b.albumName);
});
case 'genre-asc':
result.sort((a, b) {
final comparison = _queueCompareOptionalText(
_queueGroupedAlbumGenre(a),
_queueGroupedAlbumGenre(b),
);
if (comparison != 0) {
return comparison;
}
return _queueCompareOptionalText(a.albumName, b.albumName);
});
case 'genre-desc':
result.sort((a, b) {
final comparison = _queueCompareOptionalText(
_queueGroupedAlbumGenre(a),
_queueGroupedAlbumGenre(b),
descending: true,
);
if (comparison != 0) {
return comparison;
}
return _queueCompareOptionalText(a.albumName, b.albumName);
});
default:
break;
}
return result;
}
List<_GroupedLocalAlbum> _queueFilterGroupedLocalAlbums(
List<_GroupedLocalAlbum> albums,
_QueueGroupedAlbumFilterRequest request,
) {
if (request.filterSource == 'downloaded') return const [];
if (request.filterSource == null &&
request.filterQuality == null &&
request.filterFormat == null &&
request.filterMetadata == null &&
request.searchQuery.isEmpty &&
request.sortMode == 'latest') {
return albums;
}
final result = <_GroupedLocalAlbum>[];
for (final album in albums) {
if (request.searchQuery.isNotEmpty &&
!album.searchKey.contains(request.searchQuery)) {
continue;
}
if (request.filterQuality != null ||
request.filterFormat != null ||
request.filterMetadata != null) {
var hasMatchingTrack = false;
for (final track in album.tracks) {
if (!_queuePassesQualityFilter(
request.filterQuality,
_queueLocalQualityLabel(track),
)) {
continue;
}
if (!_queuePassesFormatFilter(request.filterFormat, track.filePath)) {
continue;
}
if (!_queueMatchesMetadataFilter(
filterMetadata: request.filterMetadata,
albumArtist: track.albumArtist,
releaseDate: track.releaseDate,
genre: track.genre,
)) {
continue;
}
hasMatchingTrack = true;
break;
}
if (!hasMatchingTrack) continue;
}
result.add(album);
}
switch (request.sortMode) {
case 'oldest':
result.sort((a, b) => a.latestScanned.compareTo(b.latestScanned));
case 'artist-asc':
result.sort((a, b) {
final comparison = _queueCompareOptionalText(
a.artistName,
b.artistName,
);
if (comparison != 0) {
return comparison;
}
return _queueCompareOptionalText(a.albumName, b.albumName);
});
case 'artist-desc':
result.sort((a, b) {
final comparison = _queueCompareOptionalText(
a.artistName,
b.artistName,
descending: true,
);
if (comparison != 0) {
return comparison;
}
return _queueCompareOptionalText(a.albumName, b.albumName);
});
case 'a-z':
result.sort(
(a, b) =>
a.albumName.toLowerCase().compareTo(b.albumName.toLowerCase()),
);
case 'z-a':
result.sort(
(a, b) =>
b.albumName.toLowerCase().compareTo(a.albumName.toLowerCase()),
);
case 'album-asc':
result.sort(
(a, b) => _queueCompareOptionalText(a.albumName, b.albumName),
);
case 'album-desc':
result.sort(
(a, b) => _queueCompareOptionalText(
a.albumName,
b.albumName,
descending: true,
),
);
case 'release-oldest':
result.sort((a, b) {
final comparison = _queueCompareOptionalDate(
_queueGroupedLocalAlbumReleaseDate(a),
_queueGroupedLocalAlbumReleaseDate(b),
);
if (comparison != 0) {
return comparison;
}
return _queueCompareOptionalText(a.albumName, b.albumName);
});
case 'release-newest':
result.sort((a, b) {
final comparison = _queueCompareOptionalDate(
_queueGroupedLocalAlbumReleaseDate(a),
_queueGroupedLocalAlbumReleaseDate(b),
descending: true,
);
if (comparison != 0) {
return comparison;
}
return _queueCompareOptionalText(a.albumName, b.albumName);
});
case 'genre-asc':
result.sort((a, b) {
final comparison = _queueCompareOptionalText(
_queueGroupedLocalAlbumGenre(a),
_queueGroupedLocalAlbumGenre(b),
);
if (comparison != 0) {
return comparison;
}
return _queueCompareOptionalText(a.albumName, b.albumName);
});
case 'genre-desc':
result.sort((a, b) {
final comparison = _queueCompareOptionalText(
_queueGroupedLocalAlbumGenre(a),
_queueGroupedLocalAlbumGenre(b),
descending: true,
);
if (comparison != 0) {
return comparison;
}
return _queueCompareOptionalText(a.albumName, b.albumName);
});
default:
break;
}
return result;
}
final _queueHistoryStatsProvider = Provider<_HistoryStats>((ref) {
final historyItems = ref.watch(
downloadHistoryProvider.select((s) => s.items),
);
final localLibraryEnabled = ref.watch(
settingsProvider.select((s) => s.localLibraryEnabled),
);
final localItems = localLibraryEnabled
? ref.watch(localLibraryProvider.select((s) => s.items))
: const <LocalLibraryItem>[];
return _buildQueueHistoryStats(historyItems, localItems);
});
final _queueFilteredAlbumsProvider =
Provider.family<
({List<_GroupedAlbum> albums, List<_GroupedLocalAlbum> localAlbums}),
_QueueGroupedAlbumFilterRequest
>((ref, request) {
final historyStats = ref.watch(_queueHistoryStatsProvider);
return (
albums: _queueFilterGroupedAlbums(historyStats.groupedAlbums, request),
localAlbums: _queueFilterGroupedLocalAlbums(
historyStats.groupedLocalAlbums,
request,
),
);
});
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};
}