diff --git a/ui/src/lib/components/AudioPlayer.svelte b/ui/src/lib/components/AudioPlayer.svelte
index a1a6b63..a4ddbe0 100644
--- a/ui/src/lib/components/AudioPlayer.svelte
+++ b/ui/src/lib/components/AudioPlayer.svelte
@@ -70,8 +70,8 @@
voices?: Voice[];
/** Called when the server returns 402 (free daily limit reached). */
onProRequired?: () => void;
- /** Visual style of the player card. 'standard' = full controls; 'compact' = slim seekable player. */
- playerStyle?: 'standard' | 'compact';
+ /** Visual style of the player card. 'standard' = inline card; 'float' = draggable overlay. */
+ playerStyle?: 'standard' | 'float';
/** Approximate word count for the chapter, used to show estimated listen time in the idle state. */
wordCount?: number;
}
@@ -891,18 +891,32 @@
audioStore.duration > 0 ? (audioStore.currentTime / audioStore.duration) * 100 : 0
);
- const SPEED_OPTIONS = [0.75, 1, 1.25, 1.5, 2] as const;
- function cycleSpeed() {
- const idx = SPEED_OPTIONS.indexOf(audioStore.speed as (typeof SPEED_OPTIONS)[number]);
- audioStore.speed = SPEED_OPTIONS[(idx + 1) % SPEED_OPTIONS.length];
- }
-
- function seekFromCompactBar(e: MouseEvent) {
+ function seekFromBar(e: MouseEvent) {
if (audioStore.duration <= 0) return;
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
audioStore.seekRequest = pct * audioStore.duration;
}
+
+ // ── Float player drag state ──────────────────────────────────────────────
+ /** Position of the floating overlay (bottom-right anchor by default). */
+ let floatPos = $state({ x: 0, y: 0 });
+ let floatDragging = $state(false);
+ let floatDragStart = $state({ mx: 0, my: 0, ox: 0, oy: 0 });
+
+ function onFloatPointerDown(e: PointerEvent) {
+ floatDragging = true;
+ floatDragStart = { mx: e.clientX, my: e.clientY, ox: floatPos.x, oy: floatPos.y };
+ (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
+ }
+ function onFloatPointerMove(e: PointerEvent) {
+ if (!floatDragging) return;
+ floatPos = {
+ x: floatDragStart.ox + (e.clientX - floatDragStart.mx),
+ y: floatDragStart.oy + (e.clientY - floatDragStart.my)
+ };
+ }
+ function onFloatPointerUp() { floatDragging = false; }
@@ -953,121 +967,6 @@
{/snippet}
-{#if playerStyle === 'compact'}
-
-
- {#if audioStore.isCurrentChapter(slug, chapter)}
- {#if audioStore.status === 'idle' || audioStore.status === 'error'}
- {#if audioStore.status === 'error'}
-
{audioStore.errorMsg || 'Failed to load audio.'}
- {/if}
-
-
- {:else if audioStore.status === 'loading'}
-
-
- {m.player_loading()}
-
-
- {:else if audioStore.status === 'generating'}
-
-
- {m.reader_generating_narration()}
- {Math.round(audioStore.progress)}%
-
-
-
-
- {:else if audioStore.status === 'ready'}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {formatTime(audioStore.currentTime)} / {formatDuration(audioStore.duration)}
-
-
-
-
-
- {/if}
-
- {:else if audioStore.active}
-
-
- {m.reader_now_playing({ title: audioStore.chapterTitle || `Ch.${audioStore.chapter}` })}
-
-
-
-
- {:else}
-
- {/if}
-
-{:else}
{#if
@@ -1197,6 +1096,7 @@
{:else}
+{#if !(playerStyle === 'float' && audioStore.isCurrentChapter(slug, chapter) && audioStore.active)}
@@ -1402,7 +1302,6 @@
{/if}
{/if}
-
+{#if playerStyle === 'float' && audioStore.isCurrentChapter(slug, chapter) && audioStore.active}
+
+
+
+
+
+
+
+
+
+ {audioStore.chapterTitle || `Chapter ${audioStore.chapter}`}
+
+
+ {#if audioStore.isPlaying}
+
+ {/if}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {formatTime(audioStore.currentTime)}
+ /
+ {formatDuration(audioStore.duration)}
+
+
+
+
+ {audioStore.speed}×
+
+
+
+
+{/if}
diff --git a/ui/src/routes/books/[slug]/chapters/[n]/+page.svelte b/ui/src/routes/books/[slug]/chapters/[n]/+page.svelte
index 9f201aa..c9e88bb 100644
--- a/ui/src/routes/books/[slug]/chapters/[n]/+page.svelte
+++ b/ui/src/routes/books/[slug]/chapters/[n]/+page.svelte
@@ -52,7 +52,7 @@
type LineSpacing = 'compact' | 'normal' | 'relaxed';
type ReadWidth = 'narrow' | 'normal' | 'wide';
type ParaStyle = 'spaced' | 'indented';
- type PlayerStyle = 'standard' | 'compact';
+ type PlayerStyle = 'standard' | 'float';
/** Controls how many lines fit on a page by adjusting the container height offset. */
type PageLines = 'less' | 'normal' | 'more';
@@ -66,7 +66,7 @@
pageLines: PageLines;
}
- const LAYOUT_KEY = 'reader_layout_v1';
+ const LAYOUT_KEY = 'reader_layout_v2';
const LINE_HEIGHTS: Record
= { compact: 1.55, normal: 1.85, relaxed: 2.2 };
const READ_WIDTHS: Record = { narrow: '58ch', normal: '72ch', wide: 'min(90ch, 100%)' };
/**
@@ -950,7 +950,7 @@
Style
- {#each ([['standard', 'Standard'], ['compact', 'Compact']] as const) as [s, lbl]}
+ {#each ([['standard', 'Standard'], ['float', 'Float']] as const) as [s, lbl]}