|
|
|
|
@@ -50,6 +50,7 @@
|
|
|
|
|
|
|
|
|
|
import { audioStore } from '$lib/audio.svelte';
|
|
|
|
|
import { goto } from '$app/navigation';
|
|
|
|
|
import { untrack } from 'svelte';
|
|
|
|
|
import { Button } from '$lib/components/ui/button';
|
|
|
|
|
import { cn } from '$lib/utils';
|
|
|
|
|
import type { Voice } from '$lib/types';
|
|
|
|
|
@@ -981,7 +982,7 @@
|
|
|
|
|
let floatMoved = $state(false);
|
|
|
|
|
|
|
|
|
|
function onFloatPointerDown(e: PointerEvent) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
floatDragging = true;
|
|
|
|
|
floatMoved = false;
|
|
|
|
|
floatDragStart = { mx: e.clientX, my: e.clientY, ox: audioStore.floatPos.x, oy: audioStore.floatPos.y };
|
|
|
|
|
@@ -994,25 +995,32 @@
|
|
|
|
|
// Only start moving if dragged > 6px to preserve tap detection
|
|
|
|
|
if (!floatMoved && Math.hypot(dx, dy) < 6) return;
|
|
|
|
|
floatMoved = true;
|
|
|
|
|
// right = MARGIN - x → drag right (dx>0) should decrease right → x increases → x = ox + dx
|
|
|
|
|
// bottom = MARGIN - y → drag down (dy>0) should decrease bottom → y increases → y = oy + dy
|
|
|
|
|
const raw = {
|
|
|
|
|
x: floatDragStart.ox - dx, // x increases toward left (away from right edge)
|
|
|
|
|
y: floatDragStart.oy - dy, // y increases toward top (away from bottom edge)
|
|
|
|
|
x: floatDragStart.ox + dx,
|
|
|
|
|
y: floatDragStart.oy + dy,
|
|
|
|
|
};
|
|
|
|
|
audioStore.floatPos = clampFloatPos(raw.x, raw.y);
|
|
|
|
|
}
|
|
|
|
|
function onFloatPointerUp() {
|
|
|
|
|
function onFloatPointerUp(e: PointerEvent) {
|
|
|
|
|
if (!floatDragging) return;
|
|
|
|
|
if (floatDragging && !floatMoved) {
|
|
|
|
|
// Tap: toggle play/pause
|
|
|
|
|
audioStore.toggleRequest++;
|
|
|
|
|
}
|
|
|
|
|
floatDragging = false;
|
|
|
|
|
try { (e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId); } catch { /* ignore */ }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clamp saved position to viewport on mount and on resize
|
|
|
|
|
// Clamp saved position to viewport on mount and on resize.
|
|
|
|
|
// Use untrack() when reading floatPos to avoid a reactive loop
|
|
|
|
|
// (reading + writing the same state inside $effect would re-trigger forever).
|
|
|
|
|
$effect(() => {
|
|
|
|
|
if (typeof window === 'undefined') return;
|
|
|
|
|
const clamp = () => {
|
|
|
|
|
audioStore.floatPos = clampFloatPos(audioStore.floatPos.x, audioStore.floatPos.y);
|
|
|
|
|
const { x, y } = untrack(() => audioStore.floatPos);
|
|
|
|
|
audioStore.floatPos = clampFloatPos(x, y);
|
|
|
|
|
};
|
|
|
|
|
clamp();
|
|
|
|
|
window.addEventListener('resize', clamp);
|
|
|
|
|
@@ -1268,15 +1276,18 @@
|
|
|
|
|
</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>
|
|
|
|
|
<!-- Seek bar — proper range input so drag works on iOS too -->
|
|
|
|
|
<input
|
|
|
|
|
type="range"
|
|
|
|
|
aria-label="Seek"
|
|
|
|
|
min="0"
|
|
|
|
|
max={audioStore.duration || 0}
|
|
|
|
|
value={audioStore.currentTime}
|
|
|
|
|
oninput={(e) => { audioStore.seekRequest = parseFloat((e.target as HTMLInputElement).value); }}
|
|
|
|
|
onchange={(e) => { audioStore.seekRequest = parseFloat((e.target as HTMLInputElement).value); }}
|
|
|
|
|
class="flex-1 h-1.5 cursor-pointer"
|
|
|
|
|
style="accent-color: var(--color-brand);"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<!-- Time -->
|
|
|
|
|
<span class="flex-shrink-0 text-[11px] tabular-nums text-(--color-muted)">
|
|
|
|
|
@@ -1525,7 +1536,7 @@
|
|
|
|
|
onpointerdown={onFloatPointerDown}
|
|
|
|
|
onpointermove={onFloatPointerMove}
|
|
|
|
|
onpointerup={onFloatPointerUp}
|
|
|
|
|
onpointercancel={onFloatPointerUp}
|
|
|
|
|
onpointercancel={(e) => { floatDragging = false; try { (e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId); } catch { /* ignore */ } }}
|
|
|
|
|
>
|
|
|
|
|
<!-- Pulsing ring when playing -->
|
|
|
|
|
{#if audioStore.isPlaying}
|
|
|
|
|
|