player scrubber
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user