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:
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user