perf(library): lazy-load collections and dispose queries

This commit is contained in:
zarzet
2026-07-15 21:32:43 +07:00
parent 7f1c3e60db
commit 17dbcc1338
11 changed files with 217 additions and 85 deletions
+15 -20
View File
@@ -677,8 +677,7 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
) async {
final normalized = filePath.trim();
if (normalized.isEmpty) return;
final stored =
prefs.getStringList(_audioProbeFailedPathsKey) ?? <String>[];
final stored = prefs.getStringList(_audioProbeFailedPathsKey) ?? <String>[];
if (stored.contains(normalized)) return;
stored.add(normalized);
while (stored.length > _audioProbeFailedPathsMax) {
@@ -1528,11 +1527,11 @@ class DownloadHistoryPageRequest {
int get hashCode => Object.hash(limit, offset);
}
final downloadHistoryPageProvider =
FutureProvider.family<
List<DownloadHistoryItem>,
DownloadHistoryPageRequest
>((ref, request) async {
final downloadHistoryPageProvider = FutureProvider.autoDispose
.family<List<DownloadHistoryItem>, DownloadHistoryPageRequest>((
ref,
request,
) async {
ref.watch(
downloadHistoryProvider.select((state) => state.loadedIndexVersion),
);
@@ -1574,19 +1573,16 @@ HistoryLookupRequest historyLookupForTrack(Track track) {
);
}
final downloadHistoryExistsProvider =
FutureProvider.family<bool, HistoryLookupRequest>((ref, request) async {
final downloadHistoryExistsProvider = FutureProvider.autoDispose
.family<bool, HistoryLookupRequest>((ref, request) async {
ref.watch(
downloadHistoryProvider.select((state) => state.loadedIndexVersion),
);
return HistoryDatabase.instance.existsTrack(request);
});
final downloadHistoryBatchExistsProvider =
FutureProvider.family<Set<String>, HistoryBatchLookupRequest>((
ref,
request,
) async {
final downloadHistoryBatchExistsProvider = FutureProvider.autoDispose
.family<Set<String>, HistoryBatchLookupRequest>((ref, request) async {
ref.watch(
downloadHistoryProvider.select((state) => state.loadedIndexVersion),
);
@@ -1612,11 +1608,11 @@ class DownloadedAlbumTracksRequest {
int get hashCode => Object.hash(albumName, artistName);
}
final downloadedAlbumTracksProvider =
FutureProvider.family<
List<DownloadHistoryItem>,
DownloadedAlbumTracksRequest
>((ref, request) async {
final downloadedAlbumTracksProvider = FutureProvider.autoDispose
.family<List<DownloadHistoryItem>, DownloadedAlbumTracksRequest>((
ref,
request,
) async {
ref.watch(
downloadHistoryProvider.select((state) => state.loadedIndexVersion),
);
@@ -1626,4 +1622,3 @@ final downloadedAlbumTracksProvider =
);
return rows.map(DownloadHistoryItem.fromJson).toList(growable: false);
});
+79 -15
View File
@@ -118,6 +118,8 @@ class UserPlaylistCollection {
final DateTime createdAt;
final DateTime updatedAt;
final List<CollectionTrackEntry> tracks;
final String? previewCover;
final bool tracksLoaded;
final Set<String> _trackKeys;
UserPlaylistCollection({
@@ -127,6 +129,8 @@ class UserPlaylistCollection {
required this.createdAt,
required this.updatedAt,
required this.tracks,
this.previewCover,
this.tracksLoaded = true,
Set<String>? trackKeys,
}) : _trackKeys = trackKeys ?? tracks.map((entry) => entry.key).toSet();
@@ -137,6 +141,8 @@ class UserPlaylistCollection {
DateTime? createdAt,
DateTime? updatedAt,
List<CollectionTrackEntry>? tracks,
String? previewCover,
bool? tracksLoaded,
}) {
final nextTracks = tracks ?? this.tracks;
final keepTrackIndex = identical(nextTracks, this.tracks);
@@ -149,6 +155,10 @@ class UserPlaylistCollection {
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
tracks: nextTracks,
previewCover: previewCover ?? this.previewCover,
tracksLoaded:
tracksLoaded ??
(identical(nextTracks, this.tracks) ? this.tracksLoaded : true),
trackKeys: keepTrackIndex ? _trackKeys : null,
);
}
@@ -162,6 +172,8 @@ class UserPlaylistCollection {
return _trackKeys.contains(trackKey);
}
int get trackCount => _trackKeys.length;
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
@@ -409,9 +421,7 @@ class LibraryCollectionsState {
Set<String> _buildPlaylistTrackKeys(List<UserPlaylistCollection> playlists) {
final keys = <String>{};
for (final playlist in playlists) {
for (final entry in playlist.tracks) {
keys.add(entry.key);
}
keys.addAll(playlist._trackKeys);
}
return keys;
}
@@ -429,6 +439,7 @@ class PlaylistAddBatchResult {
class LibraryCollectionsNotifier extends Notifier<LibraryCollectionsState> {
final LibraryCollectionsDatabase _db = LibraryCollectionsDatabase.instance;
Future<void>? _loadFuture;
final Map<String, Future<void>> _playlistLoadFutures = {};
void _invalidatePlaylistPickerSummaries() {
ref.invalidate(libraryPlaylistPickerSummariesProvider);
@@ -469,13 +480,13 @@ class LibraryCollectionsNotifier extends Notifier<LibraryCollectionsState> {
}
}
final tracksByPlaylist = <String, List<CollectionTrackEntry>>{};
final trackKeysByPlaylist = <String, Set<String>>{};
for (final row in snapshot.playlistTrackRows) {
final playlistId = row['playlist_id'] as String?;
if (playlistId == null || playlistId.isEmpty) continue;
final parsed = _parseTrackEntryRow(row);
if (parsed == null) continue;
tracksByPlaylist.putIfAbsent(playlistId, () => []).add(parsed);
final trackKey = row['track_key'] as String?;
if (trackKey == null || trackKey.isEmpty) continue;
trackKeysByPlaylist.putIfAbsent(playlistId, () => {}).add(trackKey);
}
final playlists = <UserPlaylistCollection>[];
@@ -488,6 +499,16 @@ class LibraryCollectionsNotifier extends Notifier<LibraryCollectionsState> {
final createdAt =
DateTime.tryParse(createdAtRaw ?? '') ?? DateTime.now();
final updatedAt = DateTime.tryParse(updatedAtRaw ?? '') ?? createdAt;
String? previewCover;
final previewTrackJson = row['preview_track_json'] as String?;
if (previewTrackJson != null && previewTrackJson.isNotEmpty) {
try {
final decoded = jsonDecode(previewTrackJson);
if (decoded is Map) {
previewCover = decoded['coverUrl']?.toString();
}
} catch (_) {}
}
playlists.add(
UserPlaylistCollection(
@@ -496,7 +517,10 @@ class LibraryCollectionsNotifier extends Notifier<LibraryCollectionsState> {
coverImagePath: row['cover_image_path'] as String?,
createdAt: createdAt,
updatedAt: updatedAt,
tracks: tracksByPlaylist[id] ?? const <CollectionTrackEntry>[],
tracks: const <CollectionTrackEntry>[],
previewCover: previewCover,
tracksLoaded: false,
trackKeys: trackKeysByPlaylist[id],
),
);
}
@@ -518,6 +542,38 @@ class LibraryCollectionsNotifier extends Notifier<LibraryCollectionsState> {
await (_loadFuture ?? _load());
}
Future<void> ensurePlaylistLoaded(String playlistId) async {
await _ensureLoaded();
final playlist = state.playlistById(playlistId);
if (playlist == null || playlist.tracksLoaded) return;
final pending = _playlistLoadFutures[playlistId];
if (pending != null) return pending;
final load = () async {
final rows = await _db.loadPlaylistTracks(playlistId);
final tracks = rows
.map(_parseTrackEntryRow)
.whereType<CollectionTrackEntry>()
.toList(growable: false);
_replacePlaylistById(
playlistId,
(current) => current.copyWith(tracks: tracks, tracksLoaded: true),
);
}();
_playlistLoadFutures[playlistId] = load;
try {
await load;
} finally {
if (identical(_playlistLoadFutures[playlistId], load)) {
_playlistLoadFutures.remove(playlistId);
}
}
}
Future<void> ensurePlaylistsLoaded(Iterable<String> playlistIds) async {
await Future.wait(playlistIds.toSet().map(ensurePlaylistLoaded));
}
CollectionTrackEntry? _parseTrackEntryRow(Map<String, dynamic> row) {
final key = row['track_key'] as String?;
final trackJson = row['track_json'] as String?;
@@ -785,11 +841,14 @@ class LibraryCollectionsNotifier extends Notifier<LibraryCollectionsState> {
Future<bool> addTrackToPlaylist(String playlistId, Track track) async {
await _ensureLoaded();
final playlist = state.playlistById(playlistId);
var playlist = state.playlistById(playlistId);
if (playlist == null) return false;
final key = trackCollectionKey(track);
if (playlist.containsTrackKey(key)) return false;
await ensurePlaylistLoaded(playlistId);
playlist = state.playlistById(playlistId);
if (playlist == null) return false;
final now = DateTime.now();
final entry = CollectionTrackEntry(key: key, track: track, addedAt: now);
@@ -817,6 +876,7 @@ class LibraryCollectionsNotifier extends Notifier<LibraryCollectionsState> {
Iterable<Track> tracks,
) async {
await _ensureLoaded();
await ensurePlaylistLoaded(playlistId);
final playlist = state.playlistById(playlistId);
if (playlist == null) {
return const PlaylistAddBatchResult(
@@ -887,8 +947,11 @@ class LibraryCollectionsNotifier extends Notifier<LibraryCollectionsState> {
String trackKey,
) async {
await _ensureLoaded();
final playlist = state.playlistById(playlistId);
var playlist = state.playlistById(playlistId);
if (playlist == null || !playlist.containsTrackKey(trackKey)) return;
await ensurePlaylistLoaded(playlistId);
playlist = state.playlistById(playlistId);
if (playlist == null) return;
final now = DateTime.now();
await _db.deletePlaylistTrack(
@@ -973,6 +1036,7 @@ class LibraryCollectionsNotifier extends Notifier<LibraryCollectionsState> {
/// favorite artists) for a backup, ensuring data is loaded first.
Future<Map<String, dynamic>> exportCollections() async {
await _ensureLoaded();
await ensurePlaylistsLoaded(state.playlists.map((playlist) => playlist.id));
return state.toJson();
}
@@ -1059,11 +1123,11 @@ final libraryCollectionsProvider =
LibraryCollectionsNotifier.new,
);
final libraryPlaylistPickerSummariesProvider =
FutureProvider.family<
List<PlaylistPickerSummary>,
PlaylistPickerSummaryRequest
>((ref, request) async {
final libraryPlaylistPickerSummariesProvider = FutureProvider.autoDispose
.family<List<PlaylistPickerSummary>, PlaylistPickerSummaryRequest>((
ref,
request,
) async {
final db = LibraryCollectionsDatabase.instance;
await db.migrateFromSharedPreferences();
final rows = await db.loadPlaylistPickerSummaries(request.trackKeys);
+15 -21
View File
@@ -1021,8 +1021,8 @@ String? _nonEmptyCoverPath(Map<String, dynamic>? json) {
return trimmed == null || trimmed.isEmpty ? null : trimmed;
}
final localLibraryCoverProvider =
FutureProvider.family<String?, LocalLibraryCoverRequest>((ref, request) {
final localLibraryCoverProvider = FutureProvider.autoDispose
.family<String?, LocalLibraryCoverRequest>((ref, request) {
ref.watch(
localLibraryProvider.select((state) => state.loadedIndexVersion),
);
@@ -1035,11 +1035,8 @@ final localLibraryCoverProvider =
.then(_nonEmptyCoverPath);
});
final localLibraryFirstCoverProvider =
FutureProvider.family<String?, LocalLibraryCoverBatchRequest>((
ref,
request,
) async {
final localLibraryFirstCoverProvider = FutureProvider.autoDispose
.family<String?, LocalLibraryCoverBatchRequest>((ref, request) async {
ref.watch(
localLibraryProvider.select((state) => state.loadedIndexVersion),
);
@@ -1056,8 +1053,8 @@ final localLibraryFirstCoverProvider =
return null;
});
final localLibraryPageProvider =
FutureProvider.family<List<LocalLibraryItem>, LocalLibraryPageRequest>((
final localLibraryPageProvider = FutureProvider.autoDispose
.family<List<LocalLibraryItem>, LocalLibraryPageRequest>((
ref,
request,
) async {
@@ -1068,8 +1065,8 @@ final localLibraryPageProvider =
return rows.map(LocalLibraryItem.fromJson).toList(growable: false);
});
final localLibraryPageCountProvider =
FutureProvider.family<int, LocalLibraryPageRequest>((ref, request) async {
final localLibraryPageCountProvider = FutureProvider.autoDispose
.family<int, LocalLibraryPageRequest>((ref, request) async {
ref.watch(
localLibraryProvider.select((state) => state.loadedIndexVersion),
);
@@ -1106,11 +1103,11 @@ class LocalLibraryAlbumPageRequest {
Object.hash(limit, offset, filterMode, sortMode, searchQuery);
}
final localLibraryAlbumPageProvider =
FutureProvider.family<
List<LocalLibraryAlbumGroup>,
LocalLibraryAlbumPageRequest
>((ref, request) async {
final localLibraryAlbumPageProvider = FutureProvider.autoDispose
.family<List<LocalLibraryAlbumGroup>, LocalLibraryAlbumPageRequest>((
ref,
request,
) async {
ref.watch(
localLibraryProvider.select((state) => state.loadedIndexVersion),
);
@@ -1123,11 +1120,8 @@ final localLibraryAlbumPageProvider =
);
});
final localLibraryAlbumCountProvider =
FutureProvider.family<int, LocalLibraryAlbumPageRequest>((
ref,
request,
) async {
final localLibraryAlbumCountProvider = FutureProvider.autoDispose
.family<int, LocalLibraryAlbumPageRequest>((ref, request) async {
ref.watch(
localLibraryProvider.select((state) => state.loadedIndexVersion),
);