mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-29 07:18:49 +02:00
462 lines
14 KiB
Dart
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.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 = <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.autoDispose
|
|
.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();
|
|
}
|
|
}
|