Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f4d67dc77 | ||
|
|
b0e23cb50a |
@@ -71,8 +71,11 @@
|
||||
voices?: Voice[];
|
||||
/** Called when the server returns 402 (free daily limit reached). */
|
||||
onProRequired?: () => void;
|
||||
/** Visual style of the player card. 'standard' = inline card; 'float' = draggable overlay. */
|
||||
playerStyle?: 'standard' | 'float';
|
||||
/** Visual style of the player card.
|
||||
* 'standard' = full inline card with voice/chapter controls;
|
||||
* 'minimal' = compact single-row bar (play + seek + time only);
|
||||
* 'float' = draggable overlay anchored bottom-right. */
|
||||
playerStyle?: 'standard' | 'minimal' | 'float';
|
||||
/** Approximate word count for the chapter, used to show estimated listen time in the idle state. */
|
||||
wordCount?: number;
|
||||
}
|
||||
@@ -1034,7 +1037,8 @@
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Track info -->
|
||||
<!-- Track info (hidden in minimal style) -->
|
||||
{#if playerStyle !== 'minimal'}
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-semibold text-(--color-text) leading-tight truncate">
|
||||
{m.reader_play_narration()}
|
||||
@@ -1091,9 +1095,10 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Chapters button (right side) -->
|
||||
{#if chapters.length > 0}
|
||||
<!-- Chapters button (right side, hidden in minimal style) -->
|
||||
{#if chapters.length > 0 && playerStyle !== 'minimal'}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => { showChapterPanel = !showChapterPanel; showVoicePanel = false; stopSample(); }}
|
||||
@@ -1167,6 +1172,64 @@
|
||||
{:else}
|
||||
<!-- ── Non-idle states (loading / generating / ready / other-chapter-playing) ── -->
|
||||
{#if !(playerStyle === 'float' && audioStore.isCurrentChapter(slug, chapter) && audioStore.active)}
|
||||
|
||||
{#if playerStyle === 'minimal' && audioStore.isCurrentChapter(slug, chapter) && audioStore.active}
|
||||
<!-- ── Minimal style: compact bar — seek + play/pause + skip + time ────────── -->
|
||||
<div class="px-3 py-2.5 flex items-center gap-2">
|
||||
<!-- Skip back 15s -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => { audioStore.seekRequest = Math.max(0, audioStore.currentTime - 15); }}
|
||||
class="flex-shrink-0 w-7 h-7 flex items-center justify-center rounded-full text-(--color-muted) hover:text-(--color-text) hover:bg-(--color-surface-3) transition-colors"
|
||||
title="-15s"
|
||||
>
|
||||
<svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M11.99 5V1l-5 5 5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6h-2c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Play/pause -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => { audioStore.toggleRequest++; }}
|
||||
class="flex-shrink-0 w-8 h-8 rounded-full bg-(--color-brand) text-(--color-surface) flex items-center justify-center hover:bg-(--color-brand-dim) active:scale-95 transition-all"
|
||||
>
|
||||
{#if audioStore.isPlaying}
|
||||
<svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24"><path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z"/></svg>
|
||||
{:else}
|
||||
<svg class="w-3.5 h-3.5 ml-0.5" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<!-- Skip forward 30s -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => { audioStore.seekRequest = Math.min(audioStore.duration || 0, audioStore.currentTime + 30); }}
|
||||
class="flex-shrink-0 w-7 h-7 flex items-center justify-center rounded-full text-(--color-muted) hover:text-(--color-text) hover:bg-(--color-surface-3) transition-colors"
|
||||
title="+30s"
|
||||
>
|
||||
<svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M18 13c0 3.31-2.69 6-6 6s-6-2.69-6-6 2.69-6 6-6v4l5-5-5-5v4c-4.42 0-8 3.58-8 8s3.58 8 8 8 8-3.58 8-8h-2z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Seek bar -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
||||
<div
|
||||
role="none"
|
||||
class="flex-1 h-1.5 bg-(--color-surface-3) rounded-full overflow-hidden cursor-pointer"
|
||||
onclick={seekFromBar}
|
||||
>
|
||||
<div class="h-full bg-(--color-brand) rounded-full transition-none" style="width: {playPct}%"></div>
|
||||
</div>
|
||||
|
||||
<!-- Time -->
|
||||
<span class="flex-shrink-0 text-[11px] tabular-nums text-(--color-muted)">
|
||||
{formatTime(audioStore.currentTime)}<span class="opacity-40">/</span>{formatDuration(audioStore.duration)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{:else}
|
||||
<div class="p-4">
|
||||
<div class="flex items-center justify-end gap-2 mb-3">
|
||||
<!-- Chapter picker button -->
|
||||
@@ -1372,6 +1435,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- ── Chapter picker overlay ─────────────────────────────────────────────────
|
||||
Rendered as a top-level sibling (outside all player containers) so that
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
type LineSpacing = 'compact' | 'normal' | 'relaxed';
|
||||
type ReadWidth = 'narrow' | 'normal' | 'wide';
|
||||
type ParaStyle = 'spaced' | 'indented';
|
||||
type PlayerStyle = 'standard' | 'float';
|
||||
type PlayerStyle = 'standard' | 'minimal' | 'float';
|
||||
/** Controls how many lines fit on a page by adjusting the container height offset. */
|
||||
type PageLines = 'less' | 'normal' | 'more';
|
||||
|
||||
@@ -596,27 +596,27 @@
|
||||
</button>
|
||||
{#if audioExpanded}
|
||||
<div class="border border-t-0 border-(--color-border) rounded-b-lg overflow-hidden">
|
||||
{#if audioStore.slug === data.book.slug && audioStore.chapter === data.chapter.number && audioStore.active}
|
||||
<!-- Mini-player is already playing this chapter — don't duplicate controls -->
|
||||
<div class="px-4 py-3 flex items-center gap-2 text-sm text-(--color-muted)">
|
||||
<svg class="w-4 h-4 text-(--color-brand) shrink-0" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 3v10.55A4 4 0 1014 17V7h4V3h-6z"/>
|
||||
</svg>
|
||||
<span>Controls are in the player bar below.</span>
|
||||
</div>
|
||||
{:else}
|
||||
<AudioPlayer
|
||||
slug={data.book.slug}
|
||||
chapter={data.chapter.number}
|
||||
chapterTitle={cleanTitle}
|
||||
bookTitle={data.book.title}
|
||||
cover={data.book.cover}
|
||||
nextChapter={data.next}
|
||||
chapters={data.chapters}
|
||||
voices={data.voices}
|
||||
playerStyle={layout.playerStyle}
|
||||
wordCount={wordCount}
|
||||
onProRequired={() => { audioProRequired = true; }}
|
||||
{#if audioStore.slug === data.book.slug && audioStore.chapter === data.chapter.number && audioStore.active && layout.playerStyle === 'standard'}
|
||||
<!-- Mini-player is already playing this chapter — don't duplicate controls (standard/minimal mode) -->
|
||||
<div class="px-4 py-3 flex items-center gap-2 text-sm text-(--color-muted)">
|
||||
<svg class="w-4 h-4 text-(--color-brand) shrink-0" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 3v10.55A4 4 0 1014 17V7h4V3h-6z"/>
|
||||
</svg>
|
||||
<span>Controls are in the player bar below.</span>
|
||||
</div>
|
||||
{:else}
|
||||
<AudioPlayer
|
||||
slug={data.book.slug}
|
||||
chapter={data.chapter.number}
|
||||
chapterTitle={cleanTitle}
|
||||
bookTitle={data.book.title}
|
||||
cover={data.book.cover}
|
||||
nextChapter={data.next}
|
||||
chapters={data.chapters}
|
||||
voices={data.voices}
|
||||
playerStyle={layout.playerStyle}
|
||||
wordCount={wordCount}
|
||||
onProRequired={() => { audioProRequired = true; }}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -793,6 +793,67 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- ── Scroll mode floating nav buttons ──────────────────────────────────── -->
|
||||
{#if layout.readMode === 'scroll' && !layout.focusMode}
|
||||
{@const atTop = scrollProgress <= 0.01}
|
||||
{@const atBottom = scrollProgress >= 0.99}
|
||||
<div class="fixed right-4 {audioStore.active ? 'bottom-[5.5rem]' : 'bottom-8'} z-40 flex flex-col gap-2 transition-all">
|
||||
<!-- Up button / Prev chapter -->
|
||||
{#if atTop && data.prev}
|
||||
<a
|
||||
href="/books/{data.book.slug}/chapters/{data.prev}"
|
||||
class="flex items-center justify-center w-10 h-10 rounded-full bg-(--color-surface-2)/90 backdrop-blur border border-(--color-border) shadow-lg text-(--color-brand) hover:bg-(--color-surface-3) transition-colors"
|
||||
title="Previous chapter"
|
||||
aria-label="Previous chapter"
|
||||
>
|
||||
<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="M5 15l7-7 7 7"/>
|
||||
</svg>
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => window.scrollBy({ top: -Math.round(window.innerHeight * 0.85), behavior: 'smooth' })}
|
||||
disabled={atTop}
|
||||
class="flex items-center justify-center w-10 h-10 rounded-full bg-(--color-surface-2)/90 backdrop-blur border border-(--color-border) shadow-lg text-(--color-muted) hover:text-(--color-text) hover:bg-(--color-surface-3) disabled:opacity-20 disabled:cursor-default transition-colors"
|
||||
title="Scroll up"
|
||||
aria-label="Scroll up"
|
||||
>
|
||||
<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="M5 15l7-7 7 7"/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<!-- Down button / Next chapter -->
|
||||
{#if atBottom && data.next}
|
||||
<a
|
||||
href="/books/{data.book.slug}/chapters/{data.next}"
|
||||
class="flex items-center justify-center w-10 h-10 rounded-full bg-(--color-brand) shadow-lg text-black hover:bg-(--color-brand-dim) transition-colors"
|
||||
title="Next chapter"
|
||||
aria-label="Next chapter"
|
||||
>
|
||||
<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>
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => window.scrollBy({ top: Math.round(window.innerHeight * 0.85), behavior: 'smooth' })}
|
||||
disabled={atBottom && !data.next}
|
||||
class="flex items-center justify-center w-10 h-10 rounded-full bg-(--color-surface-2)/90 backdrop-blur border border-(--color-border) shadow-lg text-(--color-muted) hover:text-(--color-text) hover:bg-(--color-surface-3) disabled:opacity-20 disabled:cursor-default transition-colors"
|
||||
title="Scroll down"
|
||||
aria-label="Scroll down"
|
||||
>
|
||||
<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>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- ── Focus mode floating nav ───────────────────────────────────────────── -->
|
||||
{#if layout.focusMode}
|
||||
<div class="fixed {audioStore.active ? 'bottom-[4.5rem]' : 'bottom-6'} left-1/2 -translate-x-1/2 z-50 max-w-[calc(100vw-2rem)]">
|
||||
@@ -1108,7 +1169,7 @@
|
||||
<div class="flex items-center gap-3 px-3 py-2.5">
|
||||
<span class="text-xs text-(--color-muted) w-14 shrink-0">Style</span>
|
||||
<div class="flex gap-1.5 flex-1">
|
||||
{#each ([['standard', 'Standard'], ['float', 'Float']] as const) as [s, lbl]}
|
||||
{#each ([['standard', 'Standard'], ['minimal', 'Minimal'], ['float', 'Float']] as const) as [s, lbl]}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => setLayout('playerStyle', s)}
|
||||
@@ -1121,6 +1182,17 @@
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-3 pb-2.5">
|
||||
<p class="text-[11px] text-(--color-muted)/70 leading-snug">
|
||||
{#if layout.playerStyle === 'standard'}
|
||||
Full panel with voice picker and chapter browser.
|
||||
{:else if layout.playerStyle === 'minimal'}
|
||||
Compact bar: play/pause, seek, and time only.
|
||||
{:else}
|
||||
Draggable overlay — stays visible while you read.
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Speed -->
|
||||
<div class="flex items-center gap-3 px-3 py-2.5">
|
||||
|
||||
Reference in New Issue
Block a user