diff --git a/public/app.js b/public/app.js
index 9b502db..8fe8c6e 100644
--- a/public/app.js
+++ b/public/app.js
@@ -55,6 +55,9 @@ async function loadSettings() {
const ICONS = {
film: '',
+ queue: '',
+ visuals: '',
+ moon: '',
lyrics: '',
chat: '',
clip: '',
diff --git a/public/lyrics-view.js b/public/lyrics-view.js
index 197a775..938a75f 100644
--- a/public/lyrics-view.js
+++ b/public/lyrics-view.js
@@ -21,7 +21,7 @@ function lyrParseLrc(text) {
}
const lyrSpokenKey = t => !!t && /^(pod|ab):/.test(String(t.key)); // a podcast episode or an audiobook: words from a transcript instead of lyrics
const lyrLabel = () => lyrSpokenKey(typeof cur === 'function' ? cur() : null) ? 'Transcript' : 'Lyrics';
-const lyr = { open: false, cache: new Map(), data: null, key: null, idx: -2, raf: 0, el: null, loading: false };
+const lyr = { fs: false, open: false, cache: new Map(), data: null, key: null, idx: -2, raf: 0, el: null, loading: false };
const lyrPref = () => { try { return { size: 30, karaoke: false, ...JSON.parse(localStorage.getItem('lyr_pref')) }; } catch { return { size: 30, karaoke: false }; } };
const lyrSetPref = p => { try { localStorage.setItem('lyr_pref', JSON.stringify({ ...lyrPref(), ...p })); } catch {} };
// Radio: lyrics for the song that is on now. A station's own clock says when the song began (The Journey FM does; most do not), and what you hear is a few seconds
@@ -77,41 +77,49 @@ async function lyrLoad(t, force) {
return lyrForRadio(t, res);
}
-function lyrOpen() {
- const t = cur(); if (!t || lyr.open) return;
- lyr.open = true; document.body.classList.add('lyropen');
+// fs: the full-screen Now Playing view (art on the left, the words on the right, the player along the bottom)
+function lyrOpen(fs) {
+ const t = cur(); if (!t) return;
+ if (lyr.open) { if (!!fs !== lyr.fs) { lyr.fs = !!fs; lyrShell(); lyrReload(); } return; }
+ lyr.fs = fs === true; lyr.open = true; document.body.classList.add('lyropen');
const el = lyr.el = document.createElement('div'); el.id = 'lyr'; el.setAttribute('role', 'dialog'); el.setAttribute('aria-label', 'Lyrics');
document.body.appendChild(el); lyrShell(); lyrReload(); swipeToDismiss(el, lyrClose, { handle: '.lyrtop', always: true });
addEventListener('keydown', lyrKey); lyr.raf = requestAnimationFrame(lyrTick);
}
function lyrClose() {
if (!lyr.open) return; lyr.open = false; cancelAnimationFrame(lyr.raf); removeEventListener('keydown', lyrKey);
- if (lyr.el) lyr.el.remove(); lyr.el = null; lyr.idx = -2; document.body.classList.remove('lyropen');
+ if (lyr.el) lyr.el.remove(); lyr.el = null; lyr.fs = false; if (document.fullscreenElement) document.exitFullscreen().catch(() => {}); lyr.idx = -2; document.body.classList.remove('lyropen');
}
const lyrKey = e => { if (e.key === 'Escape' && !document.getElementById('sheet')) lyrClose(); };
+const lyrCls = () => (lyrPref().karaoke && !lyrSpokenKey(cur()) ? 'kar' : '') + (lyr.fs ? ' fs' : '');
function lyrShell() {
const t = cur(), pref = lyrPref(); if (!t || !lyr.el) return;
- lyr.el.className = pref.karaoke && !lyrSpokenKey(t) ? 'kar' : '';
+ lyr.el.className = lyrCls();
lyr.el.style.setProperty('--lf', pref.size + 'px');
lyr.el.innerHTML = `
})
${esc(t.title)}${esc(t.artist)}
+
${esc(t.title)}${esc(t.artist)}${t.album ? ' · ' + esc(t.album) : ''}
+ 0:000:00
`;
+ `;
const $l = q => lyr.el.querySelector(q);
if (lyrSpokenKey(t)) { $l('#lyrk').hidden = true; $l('#lyrs').hidden = true; $l('#lyrsh').hidden = true; lyr.el.setAttribute('aria-label', 'Transcript'); } // (karaoke and "wrong lyrics" make no sense for speech)
+ const fk = $l('#fsk'); fk.oninput = () => { fk.dataset.drag = 1; $l('#fsc').textContent = fmtDur(fk.value * 1000); }; fk.onchange = () => { pl.a.currentTime = +fk.value; delete fk.dataset.drag; };
+ $l('#fss').onclick = () => setShuffle(!pl.shuffle); $l('#fsr').onclick = () => setRepeat(pl.repeat === 'off' ? 'all' : pl.repeat === 'all' ? 'one' : 'off');
+ if (lyr.fs && !document.fullscreenElement && lyr.el.requestFullscreen) lyr.el.requestFullscreen().catch(() => {});
$l('#lyrx').onclick = lyrClose; $l('#lyrpv').onclick = prev; $l('#lyrnx').onclick = next; $l('#lyrt').onclick = toggle;
- $l('#lyrk').onclick = () => { lyrSetPref({ karaoke: !lyrPref().karaoke }); lyr.el.className = lyrPref().karaoke ? 'kar' : ''; $l('#lyrk').setAttribute('aria-pressed', lyrPref().karaoke); lyr.idx = -2; };
+ $l('#lyrk').onclick = () => { lyrSetPref({ karaoke: !lyrPref().karaoke }); lyr.el.className = lyrCls(); $l('#lyrk').setAttribute('aria-pressed', lyrPref().karaoke); lyr.idx = -2; };
const size = d => { const v = Math.max(18, Math.min(64, lyrPref().size + d)); lyrSetPref({ size: v }); lyr.el.style.setProperty('--lf', v + 'px'); };
$l('#lyrsm').onclick = () => size(-4); $l('#lyrbg').onclick = () => size(4);
$l('#lyrs').onclick = lyrSearchAgain; $l('#lyrsh').onclick = lyrShare;
const nudge = d => { const k = cur().key, v = lyrOffset(k) + d; lyrSetOffset(k, v); $l('#lyroff').textContent = (v / 1000).toFixed(1) + ' s'; };
$l('#lyrm').onclick = () => nudge(-lyrStep(cur())); $l('#lyrp').onclick = () => nudge(lyrStep(cur()));
- syncUI();
+ modeUI(); syncUI();
}
async function lyrReload(force) {
const t = cur(); if (!t || !lyr.el) return; lyr.key = lyrId(t); lyr.data = null; lyr.idx = -2;
@@ -129,14 +137,21 @@ function lyrDraw() {
body.innerHTML = `${d && d.error ? esc(d.error) : 'This one has no published transcript.'}
${d && d.why && !d.error ? `${esc(d.why)}
` : ''}${d && d.canTranscribe ? `Takes roughly a quarter of the running time or more on a CPU, and a long audiobook can take hours. It is kept afterwards.
` : 'An admin can add the whisper service to turn on transcribing.
'}`;
const b = body.querySelector('#lyrtr'); if (b) b.onclick = () => lyrReload('start'); return;
}
- body.innerHTML = `${d && d.error ? esc(d.error) : 'No lyrics found for this song.'}
`; return;
+ body.innerHTML = `${d && d.error ? esc(d.error) : 'No lyrics found for this song.'}
`; lyrQueue(body); return;
}
- if (d.instrumental) { src.textContent = ''; body.innerHTML = 'This one is instrumental.
'; return; }
+ if (d.instrumental) { src.textContent = ''; body.innerHTML = 'This one is instrumental.
'; lyrQueue(body); return; }
src.textContent = (d.source ? d.source + ' · ' : '') + (d.sync === 'word' ? 'word-synced' : d.sync === 'line' ? 'line-synced' : 'not time-synced') + (lyrIsRadio(t) && d.sync !== 'none' ? ' · live timing is approximate, use - and +' : '');
body.innerHTML = d.lines.map((l, i) => l.text || l.words ? `${l.words ? l.words.map((w, j) => `${esc(w.text)}`).join('') : esc(l.text)}
` : '').join('');
body.onclick = e => { const ln = e.target.closest('.ll'); if (!ln || d.sync === 'none' || lyrIsRadio(t)) return; const l = d.lines[+ln.dataset.i]; pl.a.currentTime = Math.max(0, (l.t - lyrOffset(t.key)) / 1000); };
body.scrollTop = 0; lyr.idx = -2;
}
+/** With no words to show, the space shows what is coming up instead (tap a song to jump to it). */
+function lyrQueue(body) {
+ if (lyrIsRadio(cur()) || pl.i < 0 || pl.q.length < 2) return;
+ const rows = pl.q.map((t, i) => ({ t, i })).slice(pl.i, pl.i + 40);
+ body.insertAdjacentHTML('beforeend', `Up next
${rows.map(({ t, i }) => ``).join('')}`);
+ body.querySelectorAll('.lyrqrow').forEach(b => b.onclick = () => { pl.i = +b.dataset.j; loadCur(0); });
+}
function lyrSearchAgain() {
const t = cur(); if (!t) return; const q = lyrQuery(t);
const el = openSheet('Search for the lyrics', '', `