Radio: cover art for the song that is on (iTunes), else the station logo or a tile; LIVE instead of a length; no seek bar or queue for a live station

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaC9HmtHMdRP8dAbN4wAKj
This commit is contained in:
Claude committed 2026-10-25 16:51:49 +00:00
1 parent 424d797ecf
commit 2d975baea2
6 files changed
+50 -12

No files matched your search

+6 -5
View File
@@ -214,19 +214,20 @@ function modeUI() {
document.querySelectorAll('#mr, #npr, #cnr').forEach(b => { b.classList.toggle('on', pl.repeat !== 'off'); b.dataset.r = pl.repeat === 'one' ? '1' : ''; b.setAttribute('aria-label', 'Repeat: ' + pl.repeat); });
}
function syncUI() {
const a = pl.a, d = a.duration || (cur()?.dur || 0) / 1000, pct = d ? (a.currentTime / d) * 100 : 0;
const a = pl.a, liveT = /^radio:/.test(String(cur() && cur().key)), d = liveT ? 0 : a.duration || (cur()?.dur || 0) / 1000, pct = d ? (a.currentTime / d) * 100 : 0;
const state = pl.buf && !a.paused ? 'buf' : String(a.paused), ic = state === 'buf' ? icon('spin') : icon(a.paused ? 'play' : 'pause');
const mt = $('#mt'); if (mt && mt.dataset.s !== state) { mt.innerHTML = ic; mt.dataset.s = state; }
const bf = $('#mbuf'); if (bf) { let e = 0; try { for (let i = 0; i < a.buffered.length; i++) if (a.buffered.start(i) <= a.currentTime + 1) e = Math.max(e, a.buffered.end(i)); } catch {} bf.style.width = d ? Math.min(100, (e / d) * 100) + '%' : '0'; }
const tm = $('#mtm'); if (tm) { const v = fmtDur(a.currentTime * 1000) + ' / ' + fmtDur(d * 1000); if (tm.textContent !== v) tm.textContent = v; }
const tm = $('#mtm'); if (tm) { const v = liveT ? 'LIVE' : fmtDur(a.currentTime * 1000) + ' / ' + fmtDur(d * 1000); if (tm.textContent !== v) tm.textContent = v; }
const mp = $('#mp'); if (mp) mp.style.width = pct + '%';
const t = cur(), sub = t ? (pl.buf ? (navigator.onLine ? 'Buffering…' : 'Waiting for connection…') : t.artist) : '';
const ms = $('#mo small'); if (ms && ms.textContent !== sub) ms.textContent = sub;
const ns = $('#npsub'); if (ns && t) { const v = pl.buf ? sub : t.artist + (t.album ? ' · ' + t.album : ''); if (ns.textContent !== v) ns.textContent = v; }
const nt = $('#npt'); if (nt && nt.dataset.s !== state) { nt.innerHTML = ic; nt.dataset.s = state; }
const sk = $('#npk'); if (sk && !sk.dataset.drag) { sk.max = d || 0; sk.value = a.currentTime; }
const c = $('#npc'); if (c) c.textContent = fmtDur(a.currentTime * 1000);
const dd = $('#npd'); if (dd) dd.textContent = fmtDur(d * 1000);
const sk = $('#npk'); if (sk && !sk.dataset.drag) { sk.max = d || 0; sk.value = liveT ? 0 : a.currentTime; } if (sk) sk.hidden = liveT;
const c = $('#npc'); if (c) c.textContent = liveT ? '\u25CF LIVE' : fmtDur(a.currentTime * 1000);
const dd = $('#npd'); if (dd) dd.textContent = liveT ? '' : fmtDur(d * 1000);
for (const id of ['nps', 'npr', 'npq', 'npp', 'npn', 'mpv', 'mn', 'mseek']) { const e = $('#' + id); if (e) e.hidden = liveT; } // (a live station has no queue, no skipping and no seeking)
const lt = $('#lyrt'); if (lt && lt.dataset.s !== state) { lt.innerHTML = ic; lt.dataset.s = state; }
// the Now Playing card on the Music tab
const ct = $('#cnt'); if (ct && ct.dataset.s !== state) { ct.innerHTML = ic; ct.dataset.s = state; }
+3 -1
View File
@@ -3,8 +3,9 @@
// out of it), so stations that only offer plain http still play, and everything plays in the same player as Music.
const rdoApi = (p, opts) => jfetch('/api/radio' + p, opts);
const rdoOpt = (m, o) => ({ method: m, headers: { 'content-type': 'application/json' }, body: o ? JSON.stringify(o) : undefined });
const rdoArt = (s, song) => '/api/radio/art?s=' + encodeURIComponent(s.id) + (song && song.title ? '&artist=' + encodeURIComponent(song.artist || '') + '&title=' + encodeURIComponent(song.title) : '');
const rdoIcon = s => s.favicon ? '/api/radio/icon?u=' + encodeURIComponent(s.favicon) : '';
const rdoTrack = s => ({ key: 'radio:' + s.id, title: s.name, artist: 'Live radio', album: [s.state, s.country].filter(Boolean).join(', '), album0: [s.state, s.country].filter(Boolean).join(', '), thumb: rdoIcon(s), src: '/api/radio/play/' + encodeURIComponent(s.id), nocache: true, dur: 0, station: s.name });
const rdoTrack = s => ({ key: 'radio:' + s.id, title: s.name, artist: 'Live radio', album: [s.state, s.country].filter(Boolean).join(', '), album0: [s.state, s.country].filter(Boolean).join(', '), thumb: rdoArt(s), src: '/api/radio/play/' + encodeURIComponent(s.id), nocache: true, dur: 0, station: s.name });
const rdoPlaying = () => { const t = cur(); return t && String(t.key).startsWith('radio:') ? t : null; };
const rdoMeta = s => [s.tags && s.tags.split(',').slice(0, 2).join(', '), s.state || s.country, s.codec && (s.codec + (s.bitrate ? ' ' + s.bitrate + 'k' : ''))].filter(Boolean).join(' · ');
const rdoChips = [['Christian', 'christian'], ['Worship', 'worship'], ['Gospel', 'gospel'], ['Talk', 'talk'], ['News', 'news'], ['Classical', 'classical'], ['Jazz', 'jazz'], ['Country', 'country'], ['Oldies', 'oldies']];
@@ -26,6 +27,7 @@ function rdoApplyNow(t, n) {
const was = t.song ? t.song.title + '|' + t.song.artist : '';
t.song = n.title ? { title: n.title, artist: n.artist || '', startedAt: n.startedAt || 0 } : null;
t.title = n.title || t.station; t.artist = n.title ? (n.artist || t.station) : 'Live radio'; t.album = n.title ? t.station : t.album0;
t.thumb = rdoArt({ id: t.key.slice(6) }, t.song); // the cover of the song that is on, else the station's logo
const now = t.song ? t.song.title + '|' + t.song.artist : '';
if (was === now && t.shown) return;
t.shown = true;
+1 -1
View File
@@ -1,5 +1,5 @@
// App-shell cache only; Seafile/API traffic always goes to the network.
const CACHE = 'hub-v46';
const CACHE = 'hub-v47';
const SHARE = 'hub-share'; // files shared into the app from other apps, held until the person picks a folder
const SHELL = ['/icon.svg', '/manifest.webmanifest', '/icons/icon-192.png', '/icons/apple-touch-icon.png'];