Add a global selection store backing a checkbox per track row and a
floating SelectionBar above PlayerBar with Play next / Add to queue /
Add to playlist / Like all / Clear.
- selection/store.svelte.ts: id Set + TrackRef Map + anchor index;
toggleOne, selectRange (shift+click), clearSelection. Singleton —
layout effect clears on pathname change.
- TrackRow: checkbox slot replaces the track number on hover; row
click toggles selection once any row is picked; shift+click extends
the range. Esc clears (takes priority over closing the queue
drawer).
- SelectionBar: floating pill above PlayerBar, mounted inside the
QueryClientProvider so the Like-all action can resolve.
- player.playNextMany: bulk variant of playNext for "Play next" on a
multi-track selection.
Covered by selection-store unit tests and three new TrackRow tests
(checkbox toggle, sticky select-mode row click, shift+click range).
Cross-platform consistency: when a track is currently playing, its row
on the album / liked / history / search / playlist detail surfaces
gets the same accent-border + bg-lift treatment that QueueTrackRow
applies on the queue panel. Closes the inconsistency caught during
the #375 DRY audit (the queue row had a "you are here" indicator;
other track-row surfaces did not).
Web — TrackRow.svelte + PlaylistTrackRow.svelte:
isCurrent = player.current?.id === track.id
→ border-l-2 border-l-accent bg-surface-hover when true.
PlaylistTrackRow additionally gates on !isUnavailable so deleted
rows never match a phantom playing id.
Flutter — TrackRow + _PlaylistTrackRow:
TrackRow becomes a ConsumerWidget, watches mediaItemProvider, and
wraps its InkWell in a Container whose BoxDecoration carries the
fs.iron background + 2px fs.accent left border when current. Title
text also shifts to fs.accent and FontWeight.w500. Same pattern for
_PlaylistTrackRow.
No "Now playing" pill on these surfaces — the player bar already
names the track, so the accent band alone reads as enough cue.
For #401 / #356 umbrella.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two carry-overs from M3 verification, bundled with the search-input
fix already on dev (b7a59a9).
1. Genre splitting in BuildSessionVector
The library has many tracks whose genre tag is a denormalized
multi-genre string ("Indie Pop; Pop; Alternative Pop"). Reading them
as one opaque tag means a single-genre "Pop" track and a multi-genre
track listing Pop both fail to share the Pop key, so the tags axis
in similarity scoring (weight 0.7!) returned 0 in nearly all cases
on real libraries. Result: contextual scoring couldn't differentiate.
Add splitGenres() that splits on ; and , and trims whitespace;
strings without a delimiter come back as a single-element slice
(so the existing single-genre cases keep working unchanged).
Concatenated-without-separator output ("ElectronicComplextroGlitch
Hop") stays opaque — that needs a genre dictionary, out of scope.
2. Play-radio button on TrackRow
playRadio() was only wired to the click handler on /search and
/search/tracks, leaving /library/liked, album pages, and search
results' inner clicks with no way to start a radio. Adds a small
radio button to TrackRow (between LikeButton and the +queue button).
Click → playRadio(track.id), with stopPropagation so the row's own
activate handler doesn't also fire.
Tests:
- 3 new sessionvector tests: TestSplitGenres, multi-genre semicolon
split, no-separator stays opaque. Existing single-genre tests pass
unchanged.
- 1 new TrackRow test: radio button calls playRadio with track id and
does NOT trigger row play.
Verified locally: go -short -race ./... clean, golangci-lint clean,
svelte-check 0/0, 175 vitest tests (was 174), web build succeeds.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Heart appears on every entity rendering surface. ArtistRow restructured
to a <div> with the link as a positioned overlay so the heart button
can nest without invalid <button>-in-<a> markup. Shell nav grows a
'Liked' entry pointing at /library/liked.
Outer <button> becomes <div role="button" tabindex="0"> with keyboard
handlers (Enter/Space) so a real <button> for + queue can nest without
invalid HTML. New optional onPlay prop overrides the default
playQueue(tracks, index); used by the search Tracks section to call
playRadio(track.id) instead. + queue button calls enqueueTrack and
stops propagation so the row click does not fire.
TrackRow becomes a <button> that dispatches playQueue(tracks, index).
Album page supplies the full track list + each index so the rest of
the album enqueues automatically when the user clicks a track.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>