player scrubber

This commit is contained in:
2026-02-06 03:54:42 -08:00
parent db670d1979
commit 4794d2fbb0

View File

@@ -62,6 +62,11 @@
let currentTime = $state(0);
let duration = $state(0);
// Seek scrubber value that both mobile + desktop sliders bind to.
// We keep it in sync with `currentTime` unless the user is actively dragging.
let scrubValue = $state(0);
let isScrubbing = $state(false);
// local UI derived from viewport; not persisted
let isMobile = $state(false);
@@ -105,7 +110,7 @@
// Media Session bindings
const media = createMediaSessionBindings({
play: () => void audioEl?.play(),
pause: () => audioEl?.pause(),
pause: () => void audioEl?.pause(),
next: () => {
next();
void syncAndAutoplay();
@@ -124,6 +129,21 @@
},
});
function onScrubInput(e: Event) {
if (!audioEl) return;
const next = Number((e.currentTarget as HTMLInputElement).value);
if (!Number.isFinite(next)) return;
isScrubbing = true;
scrubValue = next;
audioEl.currentTime = Math.max(0, next);
}
function onScrubCommit() {
// Release "user is dragging" mode so timeupdate can drive the UI again.
isScrubbing = false;
}
function syncAudioToCurrentTrack() {
if (!audioEl) return;
@@ -240,6 +260,12 @@
function onAudioTimeUpdate() {
if (!audioEl) return;
currentTime = audioEl.currentTime || 0;
// Keep the shared scrubber value synced to playback time unless the user is dragging.
if (!isScrubbing) {
scrubValue = currentTime;
}
media.updatePositionState({
duration,
position: currentTime,
@@ -249,6 +275,12 @@
function onAudioLoadedMetadata() {
if (!audioEl) return;
duration = Number.isFinite(audioEl.duration) ? audioEl.duration : 0;
// On metadata load (track change), ensure scrubber aligns with currentTime.
if (!isScrubbing) {
scrubValue = currentTime;
}
media.updatePositionState({
duration,
position: currentTime,
@@ -519,6 +551,24 @@
<div class="text-xs text-muted-foreground">
{formatTime(currentTime)} / {formatTime(duration)}
</div>
<label class="mt-2 block">
<span class="sr-only">Seek</span>
<input
class="w-full"
type="range"
min="0"
max={Math.max(0, duration)}
step="0.1"
value={Math.min(scrubValue, duration || 0)}
disabled={!snap.currentTrack || duration <= 0}
oninput={onScrubInput}
onchange={onScrubCommit}
onpointerup={onScrubCommit}
ontouchend={onScrubCommit}
onkeyup={onScrubCommit}
/>
</label>
</div>
<button
@@ -761,6 +811,24 @@
</div>
</div>
<label class="block">
<span class="sr-only">Seek</span>
<input
class="w-full"
type="range"
min="0"
max={Math.max(0, duration)}
step="0.1"
value={Math.min(scrubValue, duration || 0)}
disabled={!snap.currentTrack || duration <= 0}
oninput={onScrubInput}
onchange={onScrubCommit}
onpointerup={onScrubCommit}
ontouchend={onScrubCommit}
onkeyup={onScrubCommit}
/>
</label>
<div class="flex items-center gap-2">
<button
class="inline-flex h-8 w-8 items-center justify-center rounded border"