From 1e242796fea4ad9a64d5ce2e4c5558dc2ad15e9f Mon Sep 17 00:00:00 2001 From: Honey Date: Sat, 25 Jul 2026 22:00:58 +0200 Subject: [PATCH] feat(web+android): search play queues only the tapped song (radio fills the rest) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../main/kotlin/com/novasonic/app/ui/screen/ExploreScreen.kt | 3 ++- .../main/kotlin/com/novasonic/app/ui/screen/SearchScreen.kt | 4 ++-- public/sw.js | 2 +- src/ui/components.js | 5 +++-- src/views/search.js | 2 +- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/android/app/src/main/kotlin/com/novasonic/app/ui/screen/ExploreScreen.kt b/android/app/src/main/kotlin/com/novasonic/app/ui/screen/ExploreScreen.kt index f305195..e303ca1 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/ui/screen/ExploreScreen.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/ui/screen/ExploreScreen.kt @@ -343,7 +343,8 @@ private fun LibrarySearchContent( coverUrl = coverUrl(song.coverArt), isPlaying = playerState.currentSong?.id == song.id, 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) } }, ) } diff --git a/android/app/src/main/kotlin/com/novasonic/app/ui/screen/SearchScreen.kt b/android/app/src/main/kotlin/com/novasonic/app/ui/screen/SearchScreen.kt index 7cb87ca..6c518f4 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/ui/screen/SearchScreen.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/ui/screen/SearchScreen.kt @@ -57,12 +57,12 @@ fun SearchScreen( if (r.song.isNotEmpty()) { item { ResultHeader("Songs") } items(r.song, key = { it.id }) { song -> - val idx = r.song.indexOf(song) SongRow( song = song, coverUrl = coverUrl(song.coverArt), 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) }, ) } } diff --git a/public/sw.js b/public/sw.js index bbd1951..3eef3ec 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'ns-v43'; +const CACHE = 'ns-v44'; const PRECACHE = ['/', 'index.html', 'manifest.json', 'icon.svg']; const SKIP_PATHS = ['/rest/stream', '/rest/download', '/config.js']; // Auth token params stripped from cache keys so artwork survives across sessions/token refreshes. diff --git a/src/ui/components.js b/src/ui/components.js index 76e3fa2..ae0d3ba 100644 --- a/src/ui/components.js +++ b/src/ui/components.js @@ -35,7 +35,7 @@ export function makeAlbumCard(a) { } // ─── 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'); if(s.starred && !s._starred) s._starred = true; 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'); btns[0].onclick = (e) => { e.stopPropagation(); toggleStar(s, btns[0]); }; 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); }); return el; } diff --git a/src/views/search.js b/src/views/search.js index 7965367..8ca75d9 100644 --- a/src/views/search.js +++ b/src/views/search.js @@ -82,7 +82,7 @@ export async function doLocalSearch(q, el) { if(songs.length) { const sec = mkSection('Titel'); 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); } } catch(e) { el.innerHTML = `

${e.message}

`; }