feat(ui): pre-fetch next chapter audio at 90% playback progress

- Add NextStatus type and prefetch state (nextStatus, nextAudioUrl, nextProgress, nextChapterPrefetched) to AudioStore
- AudioPlayer triggers prefetchNext() when currentTime/duration >= 0.9, autoNext is on, and a next chapter exists
- startPlayback() uses the pre-fetched URL if available (skipping presign round-trip and generation wait)
- Auto-next button in layout shows a pulsing dot while prefetching and a green dot when ready
- AudioPlayer shows inline prefetch progress and ready state below the controls
- Fix indentation regression in layout onended handler
This commit is contained in:
Admin
2026-03-04 17:09:51 +05:00
parent d14644238f
commit 0402c408e4
3 changed files with 274 additions and 45 deletions

View File

@@ -179,11 +179,16 @@
audioStore.isPlaying = false;
saveAudioTime();
if (audioStore.autoNext && audioStore.nextChapter !== null && audioStore.slug) {
audioStore.autoStartPending = true;
goto(`/books/${audioStore.slug}/chapters/${audioStore.nextChapter}`).catch(() => {
audioStore.autoStartPending = false;
});
}
// Capture values synchronously before any async work — the AudioPlayer
// component will unmount during navigation, but we've already read what
// we need.
const targetSlug = audioStore.slug;
const targetChapter = audioStore.nextChapter;
audioStore.autoStartPending = true;
goto(`/books/${targetSlug}/chapters/${targetChapter}`).catch(() => {
audioStore.autoStartPending = false;
});
}
}}
preload="metadata"
style="display:none"
@@ -341,21 +346,33 @@
{audioStore.speed}×
</button>
<!-- Auto-next toggle -->
<button
onclick={() => (audioStore.autoNext = !audioStore.autoNext)}
class="p-1.5 rounded flex-shrink-0 transition-colors {audioStore.autoNext
? 'text-amber-400 bg-amber-400/15 hover:bg-amber-400/25'
: 'text-zinc-600 hover:text-zinc-300 hover:bg-zinc-800'}"
title={audioStore.autoNext ? 'Auto-next on' : 'Auto-next off'}
aria-label="Auto-next {audioStore.autoNext ? 'on' : 'off'}"
aria-pressed={audioStore.autoNext}
>
<!-- "skip to end" / auto-advance icon -->
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M6 18l8.5-6L6 6v12zm8.5-6L23 6v12l-8.5-6z"/>
</svg>
</button>
<!-- Auto-next toggle (with prefetch indicator) -->
<button
onclick={() => (audioStore.autoNext = !audioStore.autoNext)}
class="relative p-1.5 rounded flex-shrink-0 transition-colors {audioStore.autoNext
? 'text-amber-400 bg-amber-400/15 hover:bg-amber-400/25'
: 'text-zinc-600 hover:text-zinc-300 hover:bg-zinc-800'}"
title={audioStore.autoNext
? audioStore.nextStatus === 'prefetched'
? `Auto-next on Ch.${audioStore.nextChapter} ready`
: audioStore.nextStatus === 'prefetching'
? `Auto-next on preparing Ch.${audioStore.nextChapter}`
: 'Auto-next on'
: 'Auto-next off'}
aria-label="Auto-next {audioStore.autoNext ? 'on' : 'off'}"
aria-pressed={audioStore.autoNext}
>
<!-- "skip to end" / auto-advance icon -->
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M6 18l8.5-6L6 6v12zm8.5-6L23 6v12l-8.5-6z"/>
</svg>
<!-- Prefetch status dot -->
{#if audioStore.autoNext && audioStore.nextStatus === 'prefetching'}
<span class="absolute top-0.5 right-0.5 w-1.5 h-1.5 rounded-full bg-amber-400 animate-pulse"></span>
{:else if audioStore.autoNext && audioStore.nextStatus === 'prefetched'}
<span class="absolute top-0.5 right-0.5 w-1.5 h-1.5 rounded-full bg-green-400"></span>
{/if}
</button>
{:else if audioStore.status === 'generating'}
<!-- Spinner during generation -->
<svg class="w-6 h-6 text-amber-400 animate-spin flex-shrink-0" fill="none" viewBox="0 0 24 24">