fix(player): standard skip icons, next/prev start playback, fix auto-next stale presign
All checks were successful
Release / Test backend (push) Successful in 39s
Release / Check ui (push) Successful in 1m40s
Release / Docker / caddy (push) Successful in 48s
Release / Docker / backend (push) Successful in 2m34s
Release / Docker / runner (push) Successful in 2m43s
Release / Upload source maps (push) Successful in 1m44s
Release / Docker / ui (push) Successful in 2m19s
Release / Gitea Release (push) Successful in 34s

- Replace double-triangle icons with proper skip-prev (|◄) and skip-next (►|) icons
- Convert prev/next chapter <a> links to buttons calling playChapter() so navigation auto-starts audio
- Fix auto-next silent failure: fast path A now re-presigns instead of reusing the cached URL, preventing stale/expired MinIO presigned URL from silently failing on the audio element
This commit is contained in:
Admin
2026-04-06 17:22:27 +05:00
parent bb61a4654a
commit 495f386b4f
2 changed files with 30 additions and 22 deletions

View File

@@ -241,9 +241,9 @@
} }
// Keep nextChapter in the store so the layout's onended can navigate. // Keep nextChapter in the store so the layout's onended can navigate.
// NOTE: we do NOT clear on unmount here — the store retains the value so // We write null on mount (before deriving the real value) so there is no
// onended (which may fire after {#key} unmounts this component) can still // stale window where the previous chapter's nextChapter is still set while
// read it. The value is superseded when the new chapter mounts. // this chapter's AudioPlayer hasn't written its own value yet.
$effect(() => { $effect(() => {
audioStore.nextChapter = nextChapter ?? null; audioStore.nextChapter = nextChapter ?? null;
}); });
@@ -566,22 +566,28 @@
audioStore.errorMsg = ''; audioStore.errorMsg = '';
try { try {
// Fast path A: pre-fetch already landed for THIS chapter. // Fast path A: pre-fetch already confirmed audio is in MinIO for THIS chapter.
// Re-presign instead of using the cached URL — it may have expired if the
// user paused for a while between the prefetch and actually reaching this chapter.
if ( if (
audioStore.nextStatus === 'prefetched' && audioStore.nextStatus === 'prefetched' &&
audioStore.nextChapterPrefetched === chapter && audioStore.nextChapterPrefetched === chapter
audioStore.nextAudioUrl
) { ) {
const url = audioStore.nextAudioUrl; // Consume the pre-fetch state first so it doesn't carry over on error.
// Consume the pre-fetch — reset so it doesn't carry over
audioStore.resetNextPrefetch(); audioStore.resetNextPrefetch();
audioStore.audioUrl = url; // Fresh presign — audio is confirmed in MinIO so this is a fast, cheap call.
const presigned = await tryPresign(slug, chapter, voice);
if (presigned.ready) {
audioStore.audioUrl = presigned.url;
audioStore.status = 'ready'; audioStore.status = 'ready';
// Don't restore saved time for auto-next; position is 0 // Don't restore saved time for auto-next; position is 0.
// Immediately start pre-generating the chapter after this one. // Immediately start pre-generating the chapter after this one.
maybeStartPrefetch(); maybeStartPrefetch();
return; return;
} }
// Presign returned not-ready (race: MinIO object vanished?).
// Fall through to the normal slow path below.
}
// Fast path B: audio already in MinIO (presign check). // Fast path B: audio already in MinIO (presign check).
const presignResult = await tryPresign(slug, chapter, voice); const presignResult = await tryPresign(slug, chapter, voice);

View File

@@ -538,16 +538,17 @@
<div class="flex items-center justify-between pt-3 pb-4 shrink-0"> <div class="flex items-center justify-between pt-3 pb-4 shrink-0">
<!-- Prev chapter — smaller, clearly secondary --> <!-- Prev chapter — smaller, clearly secondary -->
{#if audioStore.chapter > 1 && audioStore.slug} {#if audioStore.chapter > 1 && audioStore.slug}
<a <button
href="/books/{audioStore.slug}/chapters/{audioStore.chapter - 1}" type="button"
onclick={() => playChapter(audioStore.chapter - 1)}
class="p-2 rounded-full text-(--color-muted)/60 hover:text-(--color-text) hover:bg-(--color-surface-2) transition-colors" class="p-2 rounded-full text-(--color-muted)/60 hover:text-(--color-text) hover:bg-(--color-surface-2) transition-colors"
title="Previous chapter" title="Previous chapter"
aria-label="Previous chapter" aria-label="Previous chapter"
> >
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/> <path d="M6 6h2v12H6zm2 6 8.5 6V6z"/>
</svg> </svg>
</a> </button>
{:else} {:else}
<div class="w-9 h-9"></div> <div class="w-9 h-9"></div>
{/if} {/if}
@@ -601,16 +602,17 @@
<!-- Next chapter — smaller, clearly secondary --> <!-- Next chapter — smaller, clearly secondary -->
{#if audioStore.nextChapter !== null && audioStore.slug} {#if audioStore.nextChapter !== null && audioStore.slug}
<a <button
href="/books/{audioStore.slug}/chapters/{audioStore.nextChapter}" type="button"
onclick={() => playChapter(audioStore.nextChapter!)}
class="p-2 rounded-full text-(--color-muted)/60 hover:text-(--color-text) hover:bg-(--color-surface-2) transition-colors" class="p-2 rounded-full text-(--color-muted)/60 hover:text-(--color-text) hover:bg-(--color-surface-2) transition-colors"
title="Next chapter" title="Next chapter"
aria-label="Next chapter" aria-label="Next chapter"
> >
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M6 18l8.5-6L6 6v12zm8.5-6L23 6v12l-8.5-6z"/> <path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/>
</svg> </svg>
</a> </button>
{:else} {:else}
<div class="w-9 h-9"></div> <div class="w-9 h-9"></div>
{/if} {/if}