Bring Glass Stage closer to the mockup: HUD chips, sleep card, chapter badge, A-B group, playlist banner and P2P strip

This commit is contained in:
Claude
2026-10-01 21:45:44 +00:00
parent 19973c75c0
commit 6bf2636864
3 changed files with 191 additions and 4 deletions

View File

@@ -2758,7 +2758,7 @@ function fitLyricLines(list, { maxShare = 0.17, maxPx = 160, minPx = 18, uniform
const Notes = (() => {
const LC = window.LyricsCore;
const panel = $('notesPanel');
const noop = { load() {}, tick() {}, addBookmark() {}, handleSyncKey() { return false; }, serviceRefresh() {} };
const noop = { load() {}, tick() {}, addBookmark() {}, handleSyncKey() { return false; }, serviceRefresh() {}, chapterAt() { return null; } };
if (!panel || !LC) return noop;
const CACHE_KEY = 'ytpNotesCache';
@@ -3897,6 +3897,7 @@ const Notes = (() => {
return {
load, tick, addBookmark, addNote, handleSyncKey, serviceRefresh, refreshFlags: () => { sv.flagsKey = ''; renderServiceButtons(); setPanelFlagMode(sv.panelFlag && flagsEnabled()); paintFlags(); loadFlags(); },
stageLyrics: { render: renderStage, set: setStageLyrics, on: stageOn },
chapterAt: (t) => { const c = chapters().slice().sort((a, b) => a.t - b.t); let k = -1; c.forEach((x, i) => { if (x.t <= t) k = i; }); return k < 0 ? null : { n: k + 1, of: c.length, title: c[k].title || '' }; },
};
})();
@@ -3905,6 +3906,7 @@ const Notes = (() => {
const skip = (d) => { try { Player.seek(Math.max(0, Player.master.currentTime + d)); } catch { /* not ready */ } };
const on = (id, fn) => { const el = document.getElementById(id); if (el) el.addEventListener('click', fn); };
on('back10Btn', () => skip(-10)); on('fwd10Btn', () => skip(10));
on('upnextClear', () => clearQueue());
on('stgBack', () => skip(-10)); on('stgFwd', () => skip(10));
on('stgPlay', () => $('playBtn').click());
const setAudio = (v) => { const t = $('audioOnlyToggle'); if (t && t.checked !== v) { t.checked = v; t.dispatchEvent(new Event('change', { bubbles: true })); } };
@@ -3932,6 +3934,19 @@ const Notes = (() => {
box.innerHTML = list.map(([t, a]) => `<button type="button" class="dchip${a ? ' on' : ''}"></button>`).join('');
[...box.children].forEach((b, i) => { b.textContent = list[i][0]; b.onclick = openMoreControls; });
}
const ch = $('chBadge');
if (ch) {
const c = Notes.chapterAt ? Notes.chapterAt((Player.master && Player.master.currentTime) || 0) : null;
ch.hidden = !c;
if (c) ch.textContent = `CH ${String(c.n).padStart(2, '0')}: ${String(c.title).toUpperCase().slice(0, 18)}`;
}
const V = $('video');
const zoom = $('stgZoom'), buf = $('stgBuf');
if (zoom && V) { const m = /scale\(([\d.]+)\)/.exec(V.style.transform || ''); zoom.textContent = 'Pinch ' + (m ? Number(m[1]).toFixed(1) : '1.0') + '×'; }
if (buf && V && V.duration && V.buffered.length) {
const end = V.buffered.end(V.buffered.length - 1);
buf.textContent = end >= V.duration - 1 ? 'Fully buffered' : 'Buffered ' + Math.round((end / V.duration) * 100) + '%';
}
const q = $('qualitySelect'), sq = $('stgQuality');
if (sq && q && q.options.length) sq.textContent = q.options[q.selectedIndex] ? q.options[q.selectedIndex].text : 'Auto';
const ao = !!data.settings.audioOnly;
@@ -7385,6 +7400,7 @@ function setListPaneHidden(hidden) {
}
function renderList() {
{ const ls = $('listSub'); if (ls) ls.textContent = ''; const ps = $('p2pStrip'); if (ps) ps.classList.add('hidden'); }
setListPaneHidden(view.type === 'search' && !searchResults.length && !searchAttempted);
if (view.type === 'settings') { renderSettings(); return; }
if (view.type === 'saved') { renderSaved(); return; }
@@ -7460,6 +7476,10 @@ function renderList() {
const pl = data.playlists.find((p) => p.id === view.id);
els.listTitle.textContent = pl ? pl.name : 'Playlist';
if (pl) {
{ const secs = (pl.videos || []).reduce((a, v) => a + (Number(v.duration) || 0), 0);
const h = Math.floor(secs / 3600), m = Math.round((secs % 3600) / 60);
$('listSub').textContent = `${pl.videos.length} track${pl.videos.length === 1 ? '' : 's'}${secs ? ' · ' + (h ? h + 'h ' : '') + m + 'm' : ''}`; }
renderP2pStrip();
const playAll = document.createElement('button');
playAll.textContent = '▶ Play all';
playAll.className = 'la-primary';
@@ -7467,6 +7487,7 @@ function renderList() {
const playFull = document.createElement('button');
playFull.textContent = '▶ Play in full';
playFull.className = 'la-main';
playFull.dataset.sub = 'Start to finish';
playFull.title = 'Play every video start to finish — ignores saved positions and A-B loops, stops after the last one';
playFull.onclick = () => {
if (!pl.videos.length) return;
@@ -7476,6 +7497,7 @@ function renderList() {
const serviceBtn = document.createElement('button');
serviceBtn.textContent = '🎤 Service mode';
serviceBtn.className = 'la-main';
serviceBtn.dataset.sub = 'Full lyrics';
serviceBtn.title = 'Set-list view with screen wake lock';
serviceBtn.onclick = () => {
if (!pl.videos || !pl.videos.length) { toast('Playlist is empty'); return; }
@@ -7484,6 +7506,7 @@ function renderList() {
const queueAll = document.createElement('button');
queueAll.textContent = '+ Queue';
queueAll.className = 'la-main';
queueAll.dataset.sub = `+${pl.videos.length} items`;
queueAll.onclick = () => { pl.videos.forEach((v) => addToQueue(v, { quiet: true })); toast('Added playlist to queue'); };
const rename = document.createElement('button');
rename.textContent = 'Rename';
@@ -7714,6 +7737,36 @@ async function exportToDevice(video) {
// ============================================================================
// "On other devices" — videos held by sharing devices (P2P), as a playlist
// ============================================================================
// Glass Stage: a compact "On other devices" card under a playlist (same data as the 📡 view).
let p2pStripAt = 0, p2pStripData = null;
async function renderP2pStrip() {
const box = $('p2pStrip');
if (!box || document.documentElement.dataset.layout !== 'glass-stage' || !WEB || data.settings.p2pReceive === false) return;
if (!p2pStripData || Date.now() - p2pStripAt > 60000) {
try { const r = await fetch('/api/p2p/available?limit=3'); p2pStripData = r.ok ? await r.json() : null; } catch { p2pStripData = null; }
p2pStripAt = Date.now();
}
if (view.type !== 'playlist') return;
const items = (p2pStripData && p2pStripData.ok && p2pStripData.items) || [];
if (!items.length) { box.classList.add('hidden'); return; }
const online = items.filter((i) => i.online).length;
box.innerHTML = `<div class="ps-head"><span>On other devices</span><i class="ps-dot${online ? ' on' : ''}"></i></div>
<p class="ps-sub">${items.length} video${items.length === 1 ? '' : 's'} held by other devices · ${online ? online + ' online now' : 'none online'}</p>`;
items.forEach((it) => {
const v = { id: it.id, title: it.title || videoTitleById(it.id) || it.id, channel: it.channel || '', duration: it.duration || 0, thumbnail: `https://i.ytimg.com/vi/${it.id}/mqdefault.jpg` };
const row = document.createElement('div');
row.className = 'ps-row';
row.innerHTML = '<span class="ps-ic">📡</span><span class="ps-txt"><b></b><small></small></span><button type="button" class="ps-dl" aria-label="Save to this device">⤓</button>';
row.querySelector('b').textContent = v.title;
row.querySelector('small').textContent = `${fmtBytes(it.size)}${it.height ? ' · ' + it.height + 'p' : ''} · ${it.online ? 'Online' : 'Offline'}`;
row.querySelector('.ps-dl').onclick = () => exportToDevice(v);
row.querySelector('.ps-txt').onclick = () => playFromList([v], 0, 'p2p');
box.appendChild(row);
});
box.classList.remove('hidden');
}
async function renderP2pList() {
els.listTitle.textContent = '📡 On other devices';
els.listActions.innerHTML = '';
@@ -10008,8 +10061,8 @@ function updateAbUI() {
const aSet = abA !== null, bSet = abB !== null;
const aBtn = $('abABtn'), bBtn = $('abBBtn'), clrBtn = $('abClearBtn');
const ind = $('abIndicator');
if (aBtn) aBtn.classList.toggle('active', aSet);
if (bBtn) bBtn.classList.toggle('active', bSet);
if (aBtn) { aBtn.classList.toggle('active', aSet); if (aSet) aBtn.dataset.t = fmtTime(abA); else delete aBtn.dataset.t; }
if (bBtn) { bBtn.classList.toggle('active', bSet); if (bSet) bBtn.dataset.t = fmtTime(abB); else delete bBtn.dataset.t; }
if (clrBtn) clrBtn.classList.toggle('hidden', !(aSet && bSet));
if (ind) ind.classList.toggle('hidden', !aSet && !bSet);
const q = document.querySelector('.dk-quick');