player scrubber
This commit is contained in:
@@ -62,6 +62,11 @@
|
|||||||
let currentTime = $state(0);
|
let currentTime = $state(0);
|
||||||
let duration = $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
|
// local UI derived from viewport; not persisted
|
||||||
let isMobile = $state(false);
|
let isMobile = $state(false);
|
||||||
|
|
||||||
@@ -105,7 +110,7 @@
|
|||||||
// Media Session bindings
|
// Media Session bindings
|
||||||
const media = createMediaSessionBindings({
|
const media = createMediaSessionBindings({
|
||||||
play: () => void audioEl?.play(),
|
play: () => void audioEl?.play(),
|
||||||
pause: () => audioEl?.pause(),
|
pause: () => void audioEl?.pause(),
|
||||||
next: () => {
|
next: () => {
|
||||||
next();
|
next();
|
||||||
void syncAndAutoplay();
|
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() {
|
function syncAudioToCurrentTrack() {
|
||||||
if (!audioEl) return;
|
if (!audioEl) return;
|
||||||
|
|
||||||
@@ -240,6 +260,12 @@
|
|||||||
function onAudioTimeUpdate() {
|
function onAudioTimeUpdate() {
|
||||||
if (!audioEl) return;
|
if (!audioEl) return;
|
||||||
currentTime = audioEl.currentTime || 0;
|
currentTime = audioEl.currentTime || 0;
|
||||||
|
|
||||||
|
// Keep the shared scrubber value synced to playback time unless the user is dragging.
|
||||||
|
if (!isScrubbing) {
|
||||||
|
scrubValue = currentTime;
|
||||||
|
}
|
||||||
|
|
||||||
media.updatePositionState({
|
media.updatePositionState({
|
||||||
duration,
|
duration,
|
||||||
position: currentTime,
|
position: currentTime,
|
||||||
@@ -249,6 +275,12 @@
|
|||||||
function onAudioLoadedMetadata() {
|
function onAudioLoadedMetadata() {
|
||||||
if (!audioEl) return;
|
if (!audioEl) return;
|
||||||
duration = Number.isFinite(audioEl.duration) ? audioEl.duration : 0;
|
duration = Number.isFinite(audioEl.duration) ? audioEl.duration : 0;
|
||||||
|
|
||||||
|
// On metadata load (track change), ensure scrubber aligns with currentTime.
|
||||||
|
if (!isScrubbing) {
|
||||||
|
scrubValue = currentTime;
|
||||||
|
}
|
||||||
|
|
||||||
media.updatePositionState({
|
media.updatePositionState({
|
||||||
duration,
|
duration,
|
||||||
position: currentTime,
|
position: currentTime,
|
||||||
@@ -519,6 +551,24 @@
|
|||||||
<div class="text-xs text-muted-foreground">
|
<div class="text-xs text-muted-foreground">
|
||||||
{formatTime(currentTime)} / {formatTime(duration)}
|
{formatTime(currentTime)} / {formatTime(duration)}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -761,6 +811,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
class="inline-flex h-8 w-8 items-center justify-center rounded border"
|
class="inline-flex h-8 w-8 items-center justify-center rounded border"
|
||||||
|
|||||||
Reference in New Issue
Block a user