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 @@