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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user