Web UI: player progress bar with seek

Follows /api/player's position, moves on locally between polls while
playing, seeks on release; disabled when the source can't seek.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 23:07:39 +10:00
parent a55a3ce14a
commit fd88e02e96
2 changed files with 20 additions and 4 deletions
+19 -3
View File
@@ -39,6 +39,9 @@ pre{overflow-x:auto}
<button onclick="ctl('toggle')">&#x23EF;</button>
<button onclick="ctl('next')">&#x23ED;</button>
<button onclick="ctl('stop')">&#x23F9;</button>
<label><span>Position</span><input id="pos" type="range" min="0" max="0" value="0" step="1000" disabled
oninput="seekDrag=true; showPos()" onchange="seekDrag=false; ctl('seek',{ms:+this.value})">
<small id="postime"></small></label>
<label><span>Volume</span><input id="vol" type="range" min="0" max="100"
onchange="ctl('volume',{value:+this.value})"></label>
<label><span>Force source</span><select id="force" onchange="ctl('source',{source:this.value})">
@@ -237,12 +240,25 @@ const PROJECT = {
async function ctl(cmd, body) {
try { showPlayer(await api('POST', '/api/player/' + cmd, body)); } catch {}
}
// Progress bar: follows the polled position, moves on locally while playing, seeks on release.
let np = null, npAt = 0, seekDrag = false;
const mmss = ms => `${Math.floor(ms/60000)}:${String(Math.floor(ms/1000)%60).padStart(2,'0')}`;
function showPos() {
const pos = document.getElementById('pos'), dur = np?.duration_ms;
pos.disabled = !(dur && np.can?.seek);
if (!seekDrag) {
const at = !dur ? 0 : np.state === 'playing' ? Math.min(np.position_ms + Date.now() - npAt, dur) : np.position_ms;
pos.max = dur || 0;
pos.value = at;
}
document.getElementById('postime').textContent = dur ? `${mmss(+pos.value)} / ${mmss(dur)}` : '';
}
setInterval(showPos, 500);
function showPlayer(p) {
if (!p || !p.state) return;
const t = s => s == null ? '' : `${Math.floor(s/60000)}:${String(Math.floor(s/1000)%60).padStart(2,'0')}`;
np = p; npAt = Date.now(); showPos();
document.getElementById('np').textContent =
`[${p.source}] ${p.state}: ${[p.artist, p.title].filter(Boolean).join(' – ') || '–'}` +
(p.duration_ms ? ` ${t(p.position_ms)} / ${t(p.duration_ms)}` : '');
`[${p.source}] ${p.state}: ${[p.artist, p.title].filter(Boolean).join(' – ') || '–'}`;
const v = document.getElementById('vol');
if (document.activeElement !== v) v.value = p.volume;
document.getElementById('force').value = p.forced || 'config';