feat(web+android): search play queues only the tapped song (radio fills the rest)

Playing a song from search results queued the whole result list. Now it plays only
the clicked song; Radio mode, if on, fills the rest — closer to Spotify. Both
platforms already trigger a radio top-up on the first track (web playQueue →
fillRadioQueue; Android onMediaItemTransition → checkRadioRefill), so no extra wiring
is needed. Album/playlist play still queues the full list. Bump SW cache to ns-v44.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Opus 4.8 committed 2026-07-25 22:00:58 +02:00
1 parent 14fd5119f2
commit 1e242796fe
5 files changed
+9 -7

No files matched your search

@@ -343,7 +343,8 @@ private fun LibrarySearchContent(
coverUrl = coverUrl(song.coverArt), coverUrl = coverUrl(song.coverArt),
isPlaying = playerState.currentSong?.id == song.id, isPlaying = playerState.currentSong?.id == song.id,
starred = song.starred != null, starred = song.starred != null,
onPlay = { onSongPlay(songs, idx) }, // Play only the tapped song; Radio mode (if on) fills the rest.
onPlay = { onSongPlay(listOf(song), 0) },
onMore = onShowContextMenu?.let { cb -> { cb(song) } }, onMore = onShowContextMenu?.let { cb -> { cb(song) } },
) )
} }
@@ -57,12 +57,12 @@ fun SearchScreen(
if (r.song.isNotEmpty()) { if (r.song.isNotEmpty()) {
item { ResultHeader("Songs") } item { ResultHeader("Songs") }
items(r.song, key = { it.id }) { song -> items(r.song, key = { it.id }) { song ->
val idx = r.song.indexOf(song)
SongRow( SongRow(
song = song, song = song,
coverUrl = coverUrl(song.coverArt), coverUrl = coverUrl(song.coverArt),
isPlaying = playerState.currentSong?.id == song.id, isPlaying = playerState.currentSong?.id == song.id,
onPlay = { onSongPlay(r.song, idx) }, // Play only the tapped song; Radio mode (if on) fills the rest.
onPlay = { onSongPlay(listOf(song), 0) },
) )
} }
} }
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'ns-v43'; const CACHE = 'ns-v44';
const PRECACHE = ['/', 'index.html', 'manifest.json', 'icon.svg']; const PRECACHE = ['/', 'index.html', 'manifest.json', 'icon.svg'];
const SKIP_PATHS = ['/rest/stream', '/rest/download', '/config.js']; const SKIP_PATHS = ['/rest/stream', '/rest/download', '/config.js'];
// Auth token params stripped from cache keys so artwork survives across sessions/token refreshes. // Auth token params stripped from cache keys so artwork survives across sessions/token refreshes.
+3 -2
View File
@@ -35,7 +35,7 @@ export function makeAlbumCard(a) {
} }
// ─── SONG LIST ITEM (library) ─── // ─── SONG LIST ITEM (library) ───
export function makeSongItem(s, idx, list, { noArtistLink = false } = {}) { export function makeSongItem(s, idx, list, { noArtistLink = false, singlePlay = false } = {}) {
const el = document.createElement('div'); const el = document.createElement('div');
if(s.starred && !s._starred) s._starred = true; if(s.starred && !s._starred) s._starred = true;
el.className = 'song-item' + (S.queue[S.qIdx]?.id === s.id ? ' playing' : ''); el.className = 'song-item' + (S.queue[S.qIdx]?.id === s.id ? ' playing' : '');
@@ -69,7 +69,8 @@ export function makeSongItem(s, idx, list, { noArtistLink = false } = {}) {
const btns = el.querySelectorAll('.icon-btn'); const btns = el.querySelectorAll('.icon-btn');
btns[0].onclick = (e) => { e.stopPropagation(); toggleStar(s, btns[0]); }; btns[0].onclick = (e) => { e.stopPropagation(); toggleStar(s, btns[0]); };
btns[1].onclick = (e) => { e.stopPropagation(); showCtxMenu(e, s, list, idx); }; btns[1].onclick = (e) => { e.stopPropagation(); showCtxMenu(e, s, list, idx); };
el.onclick = () => playQueue(list, idx, { noShuffle: true }); // singlePlay (search results): play only the clicked song; Radio mode (if on) fills the rest.
el.onclick = () => singlePlay ? playQueue([s], 0, { noShuffle: true }) : playQueue(list, idx, { noShuffle: true });
el.addEventListener('contextmenu', e => { e.preventDefault(); showCtxMenu(e, s, list, idx); }); el.addEventListener('contextmenu', e => { e.preventDefault(); showCtxMenu(e, s, list, idx); });
return el; return el;
} }
+1 -1
View File
@@ -82,7 +82,7 @@ export async function doLocalSearch(q, el) {
if(songs.length) { if(songs.length) {
const sec = mkSection('Titel'); const sec = mkSection('Titel');
const list = document.createElement('div'); list.className = 'song-list'; const list = document.createElement('div'); list.className = 'song-list';
songs.forEach((so,i) => list.appendChild(makeSongItem(so, i, songs))); songs.forEach((so,i) => list.appendChild(makeSongItem(so, i, songs, { singlePlay: true })));
sec.appendChild(list); el.appendChild(sec); sec.appendChild(list); el.appendChild(sec);
} }
} catch(e) { el.innerHTML = `<p style="color:var(--danger)">${e.message}</p>`; } } catch(e) { el.innerHTML = `<p style="color:var(--danger)">${e.message}</p>`; }