From 963ecdd89b8dcc3d0ae0e2252a9ac6e48ee1d359 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 6 Apr 2026 21:51:47 +0500 Subject: [PATCH] fix: auto-next transition deadlock and resume-at-end bug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug 1 — Auto-next not transitioning: audioExpanded defaulted to false on the new chapter page because audioStore.chapter still held the old chapter number when the page script initialized. The $effect only opened the panel when isPlaying was already true — a circular dependency (can't play without the panel, panel only opens when playing). Fix: also set audioExpanded=true when autoStartChapter targets this chapter, both in the initial $state and in the reactive $effect. Bug 2 — Resume starts at the end: onended called saveAudioTime() which captured currentTime≈duration and fired a PATCH 2 seconds later (after navigation had already completed). Next visit to that chapter restored the end-of-file position. Fix: in onended, cancel the debounced timer (clearTimeout) and immediately PATCH audioTime=0 for the finished chapter, so it always resumes from the beginning on re-visit. --- ui/src/routes/+layout.svelte | 15 ++++++++++++++- .../routes/books/[slug]/chapters/[n]/+page.svelte | 14 ++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/ui/src/routes/+layout.svelte b/ui/src/routes/+layout.svelte index 81d9583..0c88659 100644 --- a/ui/src/routes/+layout.svelte +++ b/ui/src/routes/+layout.svelte @@ -363,7 +363,20 @@ }} onended={() => { audioStore.isPlaying = false; - saveAudioTime(); + // Cancel any pending debounced save and reset the position to 0 for + // the chapter that just finished. Without this, the 2s debounce fires + // after navigation and saves currentTime≈duration, causing resume to + // start at the very end next time the user returns to this chapter. + clearTimeout(audioTimeSaveTimer); + if (audioStore.slug && audioStore.chapter) { + const slug = audioStore.slug; + const chapter = audioStore.chapter; + fetch('/api/progress/audio-time', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ slug, chapter, audioTime: 0 }) + }).catch(() => {}); + } // If sleep-after-chapter is set, just pause instead of navigating if (audioStore.sleepAfterChapter) { audioStore.sleepAfterChapter = false; diff --git a/ui/src/routes/books/[slug]/chapters/[n]/+page.svelte b/ui/src/routes/books/[slug]/chapters/[n]/+page.svelte index d5a7593..226ac7d 100644 --- a/ui/src/routes/books/[slug]/chapters/[n]/+page.svelte +++ b/ui/src/routes/books/[slug]/chapters/[n]/+page.svelte @@ -311,14 +311,20 @@ return t || `Chapter ${data.chapter.number}`; }); - // Audio panel: auto-open if this chapter is already loaded/playing in the store + // Audio panel: auto-open if this chapter is already loaded/playing in the store, + // OR if auto-next is about to start it (autoStartChapter is set before navigation). // svelte-ignore state_referenced_locally let audioExpanded = $state( - audioStore.slug === data.book.slug && audioStore.chapter === data.chapter.number + (audioStore.slug === data.book.slug && audioStore.chapter === data.chapter.number) || + audioStore.autoStartChapter === data.chapter.number ); $effect(() => { - // Expand automatically when the store starts playing this chapter - if (audioStore.slug === data.book.slug && audioStore.chapter === data.chapter.number && audioStore.isPlaying) { + // Expand automatically when the store starts playing this chapter, + // or when auto-next targets this chapter (before startPlayback has run). + if ( + (audioStore.slug === data.book.slug && audioStore.chapter === data.chapter.number && audioStore.isPlaying) || + audioStore.autoStartChapter === data.chapter.number + ) { audioExpanded = true; } });