Add deck tabs, uniform action buttons, lyric reports in the player panel and a song list in the lyrics studio

This commit is contained in:
Claude
2026-09-30 15:57:11 +00:00
parent e64880c163
commit 9d74769029
4 changed files with 160 additions and 10 deletions

View File

@@ -2770,6 +2770,9 @@ const Notes = (() => {
}).join('');
if (st.sync) renderSyncPointer();
else tick(now(), true);
paintFlags();
loadFlags();
setPanelFlagMode(sv.panelFlag && flagsEnabled());
}
function renderOffset() {
@@ -3306,7 +3309,7 @@ const Notes = (() => {
// The sung line sits in the middle of the screen at the largest size that
// still fits on ONE row (measured per line with a canvas), highlighted; its
// neighbours shrink and fade with distance. Auto-scroll keeps it centred.
const sv = { key: '', idx: -2, timer: null, flags: new Map(), flagsKey: '', flagMode: false, pressed: 0 };
const sv = { key: '', idx: -2, timer: null, flags: new Map(), flagsKey: '', flagMode: false, panelFlag: false, pressed: 0 };
const flagsEnabled = () => data.settings.lyricFlags !== false && !!window.FlagUI;
const serviceOn = () => isServiceModeOpen() && !!data.settings.serviceLyrics;
const serviceAuto = () => data.settings.serviceLyricsAutoscroll !== false;
@@ -3374,9 +3377,7 @@ const Notes = (() => {
// ---- reporting wrong lines (per-profile setting: settings.lyricFlags) ----
function paintFlags() {
const list = $('serviceLyricsList');
if (!list) return;
list.querySelectorAll('.sl-line').forEach((el) => {
document.querySelectorAll('#serviceLyricsList .sl-line, #lyricsList .lyr-line').forEach((el) => {
const l = lines()[Number(el.dataset.i)];
const f = flagsEnabled() && l ? sv.flags.get(l.text) : null;
el.classList.toggle('flagged', !!f);
@@ -3405,7 +3406,7 @@ const Notes = (() => {
sv.flagsKey = ''; // force a refresh from the server (counts, ids)
if (vid === st.videoId) await loadFlags();
toast(res.action === 'withdrawn' ? 'Report withdrawn' : 'Thanks — reported ✓');
if (res.action === 'saved') { sv.flagMode = false; renderServiceButtons(); }
if (res.action === 'saved') { sv.flagMode = false; renderServiceButtons(); setPanelFlagMode(false); }
}
function centreOn(list, el) {
@@ -3677,6 +3678,31 @@ const Notes = (() => {
renderOffset();
tick(now(), true);
};
function setPanelFlagMode(on) {
sv.panelFlag = !!on;
const b = $('lyricsFlagBtn');
if (b) {
b.setAttribute('aria-pressed', String(sv.panelFlag));
b.classList.toggle('hidden', !flagsEnabled());
b.querySelector('span').textContent = sv.panelFlag ? 'Tap the line that is wrong…' : 'Report a wrong line';
}
$('lyricsList').classList.toggle('flagging', sv.panelFlag);
}
if ($('lyricsFlagBtn')) $('lyricsFlagBtn').addEventListener('click', () => setPanelFlagMode(!sv.panelFlag));
{
// Hold a lyric line to report it without switching modes.
const pl = $('lyricsList');
let timer = null, sx = 0, sy = 0;
const cancel = () => { clearTimeout(timer); timer = null; };
pl.addEventListener('pointerdown', (e) => {
const row = e.target.closest('.lyr-line');
if (!row || !flagsEnabled() || st.sync) return;
sx = e.clientX; sy = e.clientY; cancel();
timer = setTimeout(() => { timer = null; sv.pressed = Date.now(); if (navigator.vibrate) navigator.vibrate(12); openFlag(row); }, 550);
});
pl.addEventListener('pointermove', (e) => { if (timer && Math.hypot(e.clientX - sx, e.clientY - sy) > 8) cancel(); });
['pointerup', 'pointercancel', 'pointerleave', 'scroll'].forEach((t) => pl.addEventListener(t, cancel, { passive: true }));
}
$('lyrOffMinus').addEventListener('click', () => nudge(-0.5));
$('lyrOffPlus').addEventListener('click', () => nudge(0.5));
$('lyrAutoscroll').addEventListener('change', (e) => {
@@ -3697,8 +3723,10 @@ const Notes = (() => {
}
const row = e.target.closest('[data-i]');
if (!row) return;
if (Date.now() - sv.pressed < 700) return; // that was the long-press
const i = Number(row.dataset.i);
if (st.sync) { st.sync.ptr = i; renderSyncPointer(); return; }
if (sv.panelFlag && row.classList.contains('lyr-line')) { openFlag(row); return; }
const l = lines()[i];
if (l && l.t !== null && l.t !== undefined) {
st.lastUserScroll = 0;
@@ -3726,11 +3754,26 @@ const Notes = (() => {
});
return {
load, tick, addBookmark, addNote, handleSyncKey, serviceRefresh, refreshFlags: () => { sv.flagsKey = ''; renderServiceButtons(); paintFlags(); loadFlags(); },
load, tick, addBookmark, addNote, handleSyncKey, serviceRefresh, refreshFlags: () => { sv.flagsKey = ''; renderServiceButtons(); setPanelFlagMode(sv.panelFlag && flagsEnabled()); paintFlags(); loadFlags(); },
stageLyrics: { render: renderStage, set: setStageLyrics, on: stageOn },
};
})();
// ---- Player deck tabs: Sound & view (default) / Practice ----
(function deckTabs() {
const tabs = document.querySelectorAll('.dk-tab');
if (!tabs.length) return;
let cur = 'sound';
try { cur = localStorage.getItem('ytpDeckTab') === 'practice' ? 'practice' : 'sound'; } catch { /* default */ }
const show = (name) => {
tabs.forEach((t) => { const on = t.dataset.dk === name; t.classList.toggle('active', on); t.setAttribute('aria-selected', String(on)); });
document.querySelectorAll('.dk-group').forEach((g) => g.classList.toggle('hidden', g.dataset.dk !== name));
try { localStorage.setItem('ytpDeckTab', name); } catch { /* private mode */ }
};
tabs.forEach((t) => t.addEventListener('click', () => show(t.dataset.dk)));
show(cur);
})();
// ============================================================================
// Phone remote (server/remote.js relays both ends over /ws/remote)
// ============================================================================