From f43ea6af5b235d9d918743e1f1b12d5f77d3c16c Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sat, 2 May 2026 22:45:13 -0400 Subject: [PATCH] feat(player): playNext for M7 #372 track-actions menu Inserts at _queue[_index + 1] so the next-up slot is overwritten with the chosen track. Empty-queue seeding mirrors enqueueTrack's behavior. Clears _radioSeedId since user-driven enqueue invalidates the M4c auto-refresh trigger. --- web/src/lib/player/store.svelte.ts | 16 ++++++++++++++++ web/src/lib/player/store.test.ts | 18 +++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/web/src/lib/player/store.svelte.ts b/web/src/lib/player/store.svelte.ts index 2078894b..2676d10b 100644 --- a/web/src/lib/player/store.svelte.ts +++ b/web/src/lib/player/store.svelte.ts @@ -211,6 +211,22 @@ export function enqueueTrack(t: TrackRef): void { if (_state === 'idle') _index = 0; } +/** + * M7 #372: insert a track immediately after the currently-playing one + * so it plays next. If the queue is empty, behaves like enqueueTrack + * and starts the queue with this track at index 0. + */ +export function playNext(t: TrackRef): void { + _radioSeedId = null; // M4c: any user-driven enqueue clears the radio refresh state + if (_queue.length === 0) { + _queue = [t]; + _index = 0; + return; + } + const next = _index + 1; + _queue = [..._queue.slice(0, next), t, ..._queue.slice(next)]; +} + export function enqueueTracks(ts: TrackRef[]): void { _radioSeedId = null; // M4c if (ts.length === 0) return; diff --git a/web/src/lib/player/store.test.ts b/web/src/lib/player/store.test.ts index 57a50a30..97fc7642 100644 --- a/web/src/lib/player/store.test.ts +++ b/web/src/lib/player/store.test.ts @@ -280,7 +280,7 @@ describe('player store — shuffle + repeat + audio reports', () => { }); }); -import { enqueueTrack, enqueueTracks, playRadio } from './store.svelte'; +import { enqueueTrack, enqueueTracks, playNext, playRadio } from './store.svelte'; import * as client from '$lib/api/client'; describe('player store — enqueue + playRadio', () => { @@ -334,6 +334,22 @@ describe('player store — enqueue + playRadio', () => { }); }); +describe('playNext', () => { + test('splices at _index + 1, leaving _index unchanged', () => { + playQueue([track('a'), track('b')], 0); + playNext(track('c')); + expect(player.queue.map((t) => t.id)).toEqual(['a', 'c', 'b']); + expect(player.index).toBe(0); + }); + + test('on empty queue, seeds with the track at index 0', () => { + playQueue([]); + playNext(track('x')); + expect(player.queue.map((t) => t.id)).toEqual(['x']); + expect(player.index).toBe(0); + }); +}); + describe('M4c radio auto-refresh at 80%', () => { test('refresh fires at 80% queue consumption', async () => { const tracks = [track('seed'), track('t1'), track('t2'), track('t3'), track('t4')];