fix: player float mode now works; add minimal player style
Float mode was broken because AudioPlayer was unmounted the moment audioStore.active became true — exactly when the float overlay needs to render. Fix: keep AudioPlayer mounted in float and minimal modes regardless of audioStore.active; only standard mode shows the 'Controls below' message. Adds a third 'minimal' style: a compact single-row bar (skip ±, play/pause, seek, time) with no voice picker or chapter browser. Voice picker and chapter button are hidden in the idle pill too. Settings UI updated to show all three options with a live description of what each style does.
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user