From b87e758303b65a37c3bc9832d94276a4c1093359 Mon Sep 17 00:00:00 2001 From: Admin Date: Wed, 4 Mar 2026 16:01:26 +0500 Subject: [PATCH] feat(ui): auto-next chapter navigation with auto-start audio MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When autoNext is enabled, audio automatically advances to the next chapter when a track ends — navigating the page and starting the new chapter's audio. - audioStore: add autoNext (toggle flag), nextChapter (written by AudioPlayer), and autoStartPending (set before goto, cleared after auto-start fires) - layout: update onended to call goto() and set autoStartPending when autoNext is on and nextChapter is available; add auto-next toggle button to mini-player bar (double-chevron icon, amber when active) - AudioPlayer: accept nextChapter prop; write it to audioStore via $effect; auto-start playback when autoStartPending is set on component mount; show inline 'Auto' toggle button in ready controls when a next chapter exists - Chapter page: pass data.next as nextChapter prop to AudioPlayer --- ui/src/lib/audio.svelte.ts | 20 +++++++++ ui/src/lib/components/AudioPlayer.svelte | 45 ++++++++++++++++--- ui/src/routes/+layout.svelte | 25 ++++++++++- .../books/[slug]/chapters/[n]/+page.svelte | 1 + 4 files changed, 85 insertions(+), 6 deletions(-) diff --git a/ui/src/lib/audio.svelte.ts b/ui/src/lib/audio.svelte.ts index 2fce32e..fc74338 100644 --- a/ui/src/lib/audio.svelte.ts +++ b/ui/src/lib/audio.svelte.ts @@ -46,6 +46,26 @@ class AudioStore { */ seekRequest = $state(null); + // ── Auto-next ──────────────────────────────────────────────────────────── + /** + * When true, navigates to the next chapter when the current one ends + * and auto-starts its audio. + */ + autoNext = $state(false); + + /** + * The next chapter number for the currently playing chapter, or null if + * there is no next chapter. Written by the chapter page's AudioPlayer. + */ + nextChapter = $state(null); + + /** + * Set to true by the layout's onended handler (when autoNext fires a + * navigation). The AudioPlayer on the new page checks this on mount + * and, if true, immediately starts the play flow then clears the flag. + */ + autoStartPending = $state(false); + /** Whether the mini-bar at the bottom is visible */ get active(): boolean { return this.status === 'ready' || this.status === 'generating' || this.status === 'loading'; diff --git a/ui/src/lib/components/AudioPlayer.svelte b/ui/src/lib/components/AudioPlayer.svelte index 22f38d5..97fadf6 100644 --- a/ui/src/lib/components/AudioPlayer.svelte +++ b/ui/src/lib/components/AudioPlayer.svelte @@ -15,6 +15,10 @@ * When the same chapter is already loaded/playing: show inline playback state. * When a different chapter is playing: show "Now playing Ch.X" with a * "Load this chapter" button. + * + * Auto-next: when audioStore.autoNext is true, the layout navigates to the + * next chapter on track end and sets autoStartPending = true. This component + * detects that flag on mount and auto-starts playback. */ import { audioStore } from '$lib/audio.svelte'; @@ -24,6 +28,8 @@ chapter: number; chapterTitle?: string; bookTitle?: string; + /** Next chapter number, or null/undefined if this is the last chapter. */ + nextChapter?: number | null; voice?: string; speed?: number; } @@ -33,10 +39,25 @@ chapter, chapterTitle = '', bookTitle = '', + nextChapter = null, voice = 'af_bella', speed = 1.0 }: Props = $props(); + // Keep nextChapter in the store so the layout's onended can navigate. + // Run as an effect so it stays in sync if the prop ever changes. + $effect(() => { + audioStore.nextChapter = nextChapter ?? null; + }); + + // Auto-start: if the layout navigated here via auto-next, kick off playback. + $effect(() => { + if (audioStore.autoStartPending) { + audioStore.autoStartPending = false; + startPlayback(); + } + }); + // ── Pseudo progress helpers ──────────────────────────────────────────────── let progressRafId = 0; @@ -107,6 +128,7 @@ audioStore.bookTitle = bookTitle; audioStore.voice = voice; audioStore.speed = speed; + audioStore.nextChapter = nextChapter ?? null; audioStore.status = 'loading'; audioStore.errorMsg = ''; @@ -149,10 +171,6 @@ // Already loaded this chapter: toggle play/pause. if (isCurrent && audioStore.status === 'ready') { - // The layout owns the audio element; we signal via a store flag. - // We don't have a direct reference here — the layout toggles on its own. - // Dispatch a custom event that the layout can listen to, or simply - // expose a toggle helper on the store (simplest: use a counter). audioStore.toggleRequest = (audioStore.toggleRequest ?? 0) + 1; return; } @@ -252,9 +270,26 @@ {/if} - + {formatTime(audioStore.currentTime)} / {formatTime(audioStore.duration)} + + + {#if nextChapter !== null && nextChapter !== undefined} + + {/if} {/if} diff --git a/ui/src/routes/+layout.svelte b/ui/src/routes/+layout.svelte index 481ff93..3552474 100644 --- a/ui/src/routes/+layout.svelte +++ b/ui/src/routes/+layout.svelte @@ -1,6 +1,7 @@