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]}