|
|
|
|
@@ -248,6 +248,12 @@
|
|
|
|
|
audioStore.nextChapter = nextChapter ?? null;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Keep chapters list in store up to date so the layout's onended announce
|
|
|
|
|
// can find titles even if startPlayback() hasn't been called yet on this mount.
|
|
|
|
|
$effect(() => {
|
|
|
|
|
if (chapters.length > 0) audioStore.chapters = chapters;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Keep voices in store up to date whenever prop changes.
|
|
|
|
|
$effect(() => {
|
|
|
|
|
if (voices.length > 0) audioStore.voices = voices;
|
|
|
|
|
@@ -1160,78 +1166,7 @@
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
<!-- ── Chapter picker overlay ────────────────────────────────────────── -->
|
|
|
|
|
{#if showChapterPanel && audioStore.chapters.length > 0}
|
|
|
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
|
|
|
<div
|
|
|
|
|
class="fixed inset-0 z-[60] flex flex-col"
|
|
|
|
|
style="background: var(--color-surface);"
|
|
|
|
|
>
|
|
|
|
|
<!-- Header -->
|
|
|
|
|
<div class="flex items-center gap-3 px-4 py-3 border-b border-(--color-border) shrink-0">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={() => { showChapterPanel = false; chapterSearch = ''; }}
|
|
|
|
|
class="p-2 rounded-full text-(--color-muted) hover:text-(--color-text) hover:bg-(--color-surface-2) transition-colors"
|
|
|
|
|
aria-label="Close chapter picker"
|
|
|
|
|
>
|
|
|
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
<span class="text-xs font-semibold text-(--color-muted) uppercase tracking-wider flex-1">Chapters</span>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- Search -->
|
|
|
|
|
<div class="px-4 py-3 shrink-0 border-b border-(--color-border)">
|
|
|
|
|
<div class="relative">
|
|
|
|
|
<svg class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-(--color-muted)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
<input
|
|
|
|
|
type="search"
|
|
|
|
|
placeholder="Search chapters…"
|
|
|
|
|
bind:value={chapterSearch}
|
|
|
|
|
class="w-full pl-9 pr-4 py-2 text-sm bg-(--color-surface-2) border border-(--color-border) rounded-lg text-(--color-text) placeholder:text-(--color-muted) focus:outline-none focus:border-(--color-brand) transition-colors"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- Chapter list -->
|
|
|
|
|
<div class="flex-1 overflow-y-auto">
|
|
|
|
|
{#each filteredChapters as ch (ch.number)}
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={() => playChapter(ch.number)}
|
|
|
|
|
class={cn(
|
|
|
|
|
'w-full flex items-center gap-3 px-4 py-3 border-b border-(--color-border)/40 transition-colors text-left',
|
|
|
|
|
ch.number === chapter ? 'bg-(--color-brand)/8' : 'hover:bg-(--color-surface-2)'
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<!-- Chapter number badge (mirrors voice radio indicator) -->
|
|
|
|
|
<span class={cn(
|
|
|
|
|
'w-8 h-8 shrink-0 rounded-full border-2 flex items-center justify-center tabular-nums text-xs font-semibold transition-colors',
|
|
|
|
|
ch.number === chapter
|
|
|
|
|
? 'border-(--color-brand) bg-(--color-brand) text-(--color-surface)'
|
|
|
|
|
: 'border-(--color-border) text-(--color-muted)'
|
|
|
|
|
)}>{ch.number}</span>
|
|
|
|
|
<!-- Title -->
|
|
|
|
|
<span class={cn(
|
|
|
|
|
'flex-1 text-sm truncate',
|
|
|
|
|
ch.number === chapter ? 'font-semibold text-(--color-brand)' : 'text-(--color-text)'
|
|
|
|
|
)}>{ch.title || `Chapter ${ch.number}`}</span>
|
|
|
|
|
<!-- Now-playing indicator -->
|
|
|
|
|
{#if ch.number === chapter}
|
|
|
|
|
<svg class="w-4 h-4 shrink-0 text-(--color-brand)" fill="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path d="M8 5v14l11-7z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
{/if}
|
|
|
|
|
</button>
|
|
|
|
|
{/each}
|
|
|
|
|
{#if filteredChapters.length === 0}
|
|
|
|
|
<p class="px-4 py-8 text-sm text-(--color-muted) text-center">No chapters match "{chapterSearch}"</p>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{#if audioStore.isCurrentChapter(slug, chapter)}
|
|
|
|
|
<!-- ── This chapter is the active one ── -->
|
|
|
|
|
@@ -1312,3 +1247,79 @@
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
<!-- ── Chapter picker overlay ─────────────────────────────────────────────────
|
|
|
|
|
Rendered as a top-level sibling (outside all player containers) so that
|
|
|
|
|
the fixed inset-0 positioning is never clipped by overflow-hidden or
|
|
|
|
|
border-radius on any ancestor wrapping the AudioPlayer component. -->
|
|
|
|
|
{#if showChapterPanel && audioStore.chapters.length > 0}
|
|
|
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
|
|
|
<div
|
|
|
|
|
class="fixed inset-0 z-[60] flex flex-col"
|
|
|
|
|
style="background: var(--color-surface);"
|
|
|
|
|
>
|
|
|
|
|
<!-- Header -->
|
|
|
|
|
<div class="flex items-center gap-3 px-4 py-3 border-b border-(--color-border) shrink-0">
|
|
|
|
|
<span class="text-sm font-semibold text-(--color-text) flex-1">Chapters</span>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={() => { showChapterPanel = false; chapterSearch = ''; }}
|
|
|
|
|
class="w-9 h-9 flex items-center justify-center rounded-full text-(--color-muted) hover:text-(--color-text) hover:bg-(--color-surface-2) transition-colors"
|
|
|
|
|
aria-label="Close chapter picker"
|
|
|
|
|
>
|
|
|
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- Search -->
|
|
|
|
|
<div class="px-4 py-3 shrink-0 border-b border-(--color-border)">
|
|
|
|
|
<div class="relative">
|
|
|
|
|
<svg class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-(--color-muted)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
<input
|
|
|
|
|
type="search"
|
|
|
|
|
placeholder="Search chapters…"
|
|
|
|
|
bind:value={chapterSearch}
|
|
|
|
|
class="w-full pl-9 pr-4 py-2 text-sm bg-(--color-surface-2) border border-(--color-border) rounded-lg text-(--color-text) placeholder:text-(--color-muted) focus:outline-none focus:border-(--color-brand) transition-colors"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- Chapter list -->
|
|
|
|
|
<div class="flex-1 overflow-y-auto">
|
|
|
|
|
{#each filteredChapters as ch (ch.number)}
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={() => playChapter(ch.number)}
|
|
|
|
|
class={cn(
|
|
|
|
|
'w-full flex items-center gap-3 px-4 py-3 border-b border-(--color-border)/40 transition-colors text-left',
|
|
|
|
|
ch.number === chapter ? 'bg-(--color-brand)/8' : 'hover:bg-(--color-surface-2)'
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<!-- Chapter number badge -->
|
|
|
|
|
<span class={cn(
|
|
|
|
|
'w-8 h-8 shrink-0 rounded-full border-2 flex items-center justify-center tabular-nums text-xs font-semibold transition-colors',
|
|
|
|
|
ch.number === chapter
|
|
|
|
|
? 'border-(--color-brand) bg-(--color-brand) text-(--color-surface)'
|
|
|
|
|
: 'border-(--color-border) text-(--color-muted)'
|
|
|
|
|
)}>{ch.number}</span>
|
|
|
|
|
<!-- Title -->
|
|
|
|
|
<span class={cn(
|
|
|
|
|
'flex-1 text-sm truncate',
|
|
|
|
|
ch.number === chapter ? 'font-semibold text-(--color-brand)' : 'text-(--color-text)'
|
|
|
|
|
)}>{ch.title || `Chapter ${ch.number}`}</span>
|
|
|
|
|
<!-- Now-playing indicator -->
|
|
|
|
|
{#if ch.number === chapter}
|
|
|
|
|
<svg class="w-4 h-4 shrink-0 text-(--color-brand)" fill="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path d="M8 5v14l11-7z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
{/if}
|
|
|
|
|
</button>
|
|
|
|
|
{/each}
|
|
|
|
|
{#if filteredChapters.length === 0}
|
|
|
|
|
<p class="px-4 py-8 text-sm text-(--color-muted) text-center">No chapters match "{chapterSearch}"</p>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
|