From 4609e598064ceae95c0154dd3fe05c2c7e88d0fa Mon Sep 17 00:00:00 2001 From: zarzet Date: Thu, 27 Aug 2026 13:56:48 +0700 Subject: [PATCH] fix(library): show play actions during batch downloads --- lib/providers/download_history_provider.dart | 55 +++++++++++++++++++ lib/screens/album_screen.dart | 12 ++-- lib/screens/artist_screen_widgets.dart | 12 ++-- lib/screens/home_tab_search_results.dart | 12 ++-- lib/screens/library_tracks_folder_screen.dart | 12 ++-- lib/screens/playlist_screen.dart | 12 ++-- test/download_history_logic_test.dart | 35 ++++++++++++ 7 files changed, 115 insertions(+), 35 deletions(-) diff --git a/lib/providers/download_history_provider.dart b/lib/providers/download_history_provider.dart index a2b15a11..5d0390ba 100644 --- a/lib/providers/download_history_provider.dart +++ b/lib/providers/download_history_provider.dart @@ -1135,6 +1135,61 @@ final downloadHistoryBatchExistsProvider = FutureProvider.autoDispose }; }); +bool historyLookupExistsInMemory( + DownloadHistoryState state, + HistoryLookupRequest request, +) { + for (final candidate in HistoryDatabase.spotifyLookupCandidates( + request.spotifyId, + )) { + if (state.getBySpotifyId(candidate) != null) return true; + } + + final isrc = request.isrc?.trim(); + if (isrc != null && isrc.isNotEmpty && state.getByIsrc(isrc) != null) { + return true; + } + + return state.findByTrackAndArtist(request.trackName, request.artistName) != + null; +} + +Set mergeVisibleHistoryLookupKeys({ + required DownloadHistoryState state, + required HistoryBatchLookupRequest request, + required Set persistedKeys, +}) { + final keys = {...persistedKeys}; + for (final track in request.tracks) { + if (historyLookupExistsInMemory(state, track)) { + keys.add(track.lookupKey); + } + } + return keys; +} + +/// UI-facing history lookup that keeps the last database result visible while +/// a batch query reloads and immediately merges downloads persisted in memory. +/// This prevents completed tracks from losing their Play action until the +/// remainder of a large download batch finishes. +final downloadHistoryVisibleBatchExistsProvider = Provider.autoDispose + .family, HistoryBatchLookupRequest>((ref, request) { + final state = ref.watch(downloadHistoryProvider); + final persistedKeys = ref + .watch(downloadHistoryBatchExistsProvider(request)) + .maybeWhen( + skipLoadingOnReload: true, + skipLoadingOnRefresh: true, + data: (keys) => keys, + orElse: () => const {}, + ); + return mergeVisibleHistoryLookupKeys( + state: state, + request: request, + persistedKeys: persistedKeys, + ); + }); + class DownloadedAlbumTracksRequest { final String albumName; final String artistName; diff --git a/lib/screens/album_screen.dart b/lib/screens/album_screen.dart index 654c98d1..8eb9575a 100644 --- a/lib/screens/album_screen.dart +++ b/lib/screens/album_screen.dart @@ -552,13 +552,11 @@ class _AlbumScreenState extends ConsumerState final historyLookups = tracks .map(historyLookupForTrack) .toList(growable: false); - final existingHistoryKeys = ref - .watch( - downloadHistoryBatchExistsProvider( - HistoryBatchLookupRequest(historyLookups), - ), - ) - .maybeWhen(data: (keys) => keys, orElse: () => const {}); + final existingHistoryKeys = ref.watch( + downloadHistoryVisibleBatchExistsProvider( + HistoryBatchLookupRequest(historyLookups), + ), + ); return SliverPadding( padding: EdgeInsets.symmetric(horizontal: wideListInset(context)), sliver: SliverList( diff --git a/lib/screens/artist_screen_widgets.dart b/lib/screens/artist_screen_widgets.dart index 7ea7e414..d9d72f0f 100644 --- a/lib/screens/artist_screen_widgets.dart +++ b/lib/screens/artist_screen_widgets.dart @@ -311,13 +311,11 @@ extension _ArtistScreenSections on _ArtistScreenState { final historyLookups = tracks .map(historyLookupForTrack) .toList(growable: false); - final existingHistoryKeys = ref - .watch( - downloadHistoryBatchExistsProvider( - HistoryBatchLookupRequest(historyLookups), - ), - ) - .maybeWhen(data: (keys) => keys, orElse: () => const {}); + final existingHistoryKeys = ref.watch( + downloadHistoryVisibleBatchExistsProvider( + HistoryBatchLookupRequest(historyLookups), + ), + ); const tracksPerPage = 5; final pageCount = (tracks.length / tracksPerPage).ceil(); diff --git a/lib/screens/home_tab_search_results.dart b/lib/screens/home_tab_search_results.dart index 7ff83d86..9dcfc2e3 100644 --- a/lib/screens/home_tab_search_results.dart +++ b/lib/screens/home_tab_search_results.dart @@ -406,13 +406,11 @@ extension _HomeTabSearchResultsUI on _HomeTabState { final historyLookups = sortedTracks .map(historyLookupForTrack) .toList(growable: false); - final existingHistoryKeys = ref - .watch( - downloadHistoryBatchExistsProvider( - HistoryBatchLookupRequest(historyLookups), - ), - ) - .maybeWhen(data: (keys) => keys, orElse: () => const {}); + final existingHistoryKeys = ref.watch( + downloadHistoryVisibleBatchExistsProvider( + HistoryBatchLookupRequest(historyLookups), + ), + ); slivers.addAll( _buildVirtualizedResultSection( title: context.l10n.searchSongs, diff --git a/lib/screens/library_tracks_folder_screen.dart b/lib/screens/library_tracks_folder_screen.dart index 0b144dba..9d4c85db 100644 --- a/lib/screens/library_tracks_folder_screen.dart +++ b/lib/screens/library_tracks_folder_screen.dart @@ -302,13 +302,11 @@ class _LibraryTracksFolderScreenState final historyLookups = folderTracks .map(historyLookupForTrack) .toList(growable: false); - final existingHistoryKeys = ref - .watch( - downloadHistoryBatchExistsProvider( - HistoryBatchLookupRequest(historyLookups), - ), - ) - .maybeWhen(data: (keys) => keys, orElse: () => const {}); + final existingHistoryKeys = ref.watch( + downloadHistoryVisibleBatchExistsProvider( + HistoryBatchLookupRequest(historyLookups), + ), + ); final bottomPadding = MediaQuery.paddingOf(context).bottom; final bottomInset = context.navBarBottomInset; diff --git a/lib/screens/playlist_screen.dart b/lib/screens/playlist_screen.dart index 030a4416..03d645cb 100644 --- a/lib/screens/playlist_screen.dart +++ b/lib/screens/playlist_screen.dart @@ -399,13 +399,11 @@ class _PlaylistScreenState extends ConsumerState final historyLookups = _tracks .map(historyLookupForTrack) .toList(growable: false); - final existingHistoryKeys = ref - .watch( - downloadHistoryBatchExistsProvider( - HistoryBatchLookupRequest(historyLookups), - ), - ) - .maybeWhen(data: (keys) => keys, orElse: () => const {}); + final existingHistoryKeys = ref.watch( + downloadHistoryVisibleBatchExistsProvider( + HistoryBatchLookupRequest(historyLookups), + ), + ); return SliverPadding( padding: EdgeInsets.symmetric(horizontal: wideListInset(context)), sliver: SliverList( diff --git a/test/download_history_logic_test.dart b/test/download_history_logic_test.dart index 63e9bdec..ccb3518d 100644 --- a/test/download_history_logic_test.dart +++ b/test/download_history_logic_test.dart @@ -93,6 +93,41 @@ void main() { expect(state.items, hasLength(2)); }); + test('recent in-memory completion is visible during batch reload', () { + final completed = _historyItem( + id: 'completed', + filePath: '/music/Album/Same Song.flac', + downloadedAt: DateTime.utc(2026, 8, 27), + ); + final state = DownloadHistoryState( + items: [completed], + lookupItems: [completed], + totalCount: 1, + ); + const completedLookup = HistoryLookupRequest( + spotifyId: 'same', + isrc: 'SAME-ISRC', + trackName: 'Same Song', + artistName: 'Same Artist', + ); + const olderLookup = HistoryLookupRequest( + spotifyId: 'older', + trackName: 'Older Song', + artistName: 'Same Artist', + ); + + final visible = mergeVisibleHistoryLookupKeys( + state: state, + request: const HistoryBatchLookupRequest([ + completedLookup, + olderLookup, + ]), + persistedKeys: {olderLookup.lookupKey}, + ); + + expect(visible, {completedLookup.lookupKey, olderLookup.lookupKey}); + }); + test('actual audio quality survives history serialization', () { final item = _historyItem(