Redesign service-mode controls and add per-profile wrong-lyric-line reports
This commit is contained in:
106
frontend/app.js
106
frontend/app.js
@@ -3306,7 +3306,8 @@ 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 };
|
||||
const sv = { key: '', idx: -2, timer: null, flags: new Map(), flagsKey: '', flagMode: false, pressed: 0 };
|
||||
const flagsEnabled = () => data.settings.lyricFlags !== false && !!window.FlagUI;
|
||||
const serviceOn = () => isServiceModeOpen() && !!data.settings.serviceLyrics;
|
||||
const serviceAuto = () => data.settings.serviceLyricsAutoscroll !== false;
|
||||
const sungLines = () => lines().map((l, i) => ({ ...l, i })).filter((l) => l.kind === 'line');
|
||||
@@ -3324,13 +3325,18 @@ const Notes = (() => {
|
||||
function renderServiceButtons() {
|
||||
const on = !!data.settings.serviceLyrics;
|
||||
const lb = $('serviceLyricsBtn');
|
||||
lb.textContent = on ? '🎵 Lyrics on' : '🎵 Lyrics off';
|
||||
lb.classList.toggle('on', on);
|
||||
lb.setAttribute('aria-pressed', String(on));
|
||||
const fb = $('serviceFlagBtn');
|
||||
if (fb) {
|
||||
const show = on && flagsEnabled();
|
||||
fb.classList.toggle('hidden', !show);
|
||||
if (!show) sv.flagMode = false;
|
||||
fb.setAttribute('aria-pressed', String(sv.flagMode));
|
||||
$('serviceFlagHint').classList.toggle('hidden', !(show && sv.flagMode));
|
||||
$('serviceLyrics').classList.toggle('flagging', show && sv.flagMode);
|
||||
}
|
||||
const ab = $('serviceAutoscrollBtn');
|
||||
ab.classList.toggle('hidden', !on);
|
||||
ab.textContent = serviceAuto() ? '⇅ Auto-scroll on' : '⇅ Auto-scroll off';
|
||||
ab.classList.toggle('on', serviceAuto());
|
||||
ab.setAttribute('aria-pressed', String(serviceAuto()));
|
||||
}
|
||||
|
||||
@@ -3357,6 +3363,8 @@ const Notes = (() => {
|
||||
}
|
||||
list.innerHTML = items.map((l) =>
|
||||
`<div class="sl-line${l.t === null || l.t === undefined ? ' untimed' : ''}" data-i="${l.i}">${escapeHtml(l.text)}</div>`).join('');
|
||||
paintFlags();
|
||||
loadFlags();
|
||||
applyFits();
|
||||
tickService(now(), true);
|
||||
// One more fit once the view has settled (fonts, first layout).
|
||||
@@ -3364,6 +3372,42 @@ const Notes = (() => {
|
||||
sv.refit = setTimeout(() => { if (serviceOn()) applyFits(); }, 900);
|
||||
}
|
||||
|
||||
// ---- reporting wrong lines (per-profile setting: settings.lyricFlags) ----
|
||||
function paintFlags() {
|
||||
const list = $('serviceLyricsList');
|
||||
if (!list) return;
|
||||
list.querySelectorAll('.sl-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);
|
||||
el.classList.toggle('mine', !!(f && f.mine));
|
||||
if (f) el.dataset.n = f.count > 1 ? String(f.count) : ''; else delete el.dataset.n;
|
||||
});
|
||||
}
|
||||
async function loadFlags() {
|
||||
if (!flagsEnabled() || !st.videoId || !st.lyrics) return;
|
||||
const key = `${st.videoId}|${st.lyrics.rev}|${profileName()}`;
|
||||
if (key === sv.flagsKey) return;
|
||||
sv.flagsKey = key;
|
||||
const vid = st.videoId;
|
||||
const got = await FlagUI.list(vid, profileName());
|
||||
if (vid !== st.videoId) return; // moved on while the request was out
|
||||
sv.flags = new Map(got.map((f) => [f.text, f]));
|
||||
paintFlags();
|
||||
}
|
||||
async function openFlag(row) {
|
||||
const l = row && lines()[Number(row.dataset.i)];
|
||||
if (!l || !st.videoId) return;
|
||||
if (!requireProfile()) return;
|
||||
const vid = st.videoId;
|
||||
const res = await FlagUI.open({ videoId: vid, text: l.text, existing: sv.flags.get(l.text), profile: profileName() });
|
||||
if (!res) return;
|
||||
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(); }
|
||||
}
|
||||
|
||||
function centreOn(list, el) {
|
||||
const go = () => list.scrollTo({ top: Math.max(0, el.offsetTop - list.clientHeight / 2 + el.offsetHeight / 2), behavior: 'smooth' });
|
||||
go();
|
||||
@@ -3502,11 +3546,35 @@ const Notes = (() => {
|
||||
renderServiceButtons();
|
||||
if (serviceAuto()) tickService(now(), true);
|
||||
});
|
||||
$('serviceLyricsList').addEventListener('click', (e) => {
|
||||
const row = e.target.closest('.sl-line');
|
||||
const l = row && lines()[Number(row.dataset.i)];
|
||||
if (l && l.t !== null && l.t !== undefined) Player.seek(Math.max(0, l.t + offset()));
|
||||
$('serviceFlagBtn').addEventListener('click', () => {
|
||||
sv.flagMode = !sv.flagMode;
|
||||
renderServiceButtons();
|
||||
});
|
||||
{
|
||||
// Tap = seek (or report, in report mode). Holding a line reports it too, so
|
||||
// it works without switching modes.
|
||||
const list = $('serviceLyricsList');
|
||||
let timer = null, sx = 0, sy = 0;
|
||||
const cancel = () => { clearTimeout(timer); timer = null; };
|
||||
list.addEventListener('pointerdown', (e) => {
|
||||
if (!flagsEnabled() || !e.target.closest('.sl-line')) return;
|
||||
sx = e.clientX; sy = e.clientY;
|
||||
const row = e.target.closest('.sl-line');
|
||||
cancel();
|
||||
timer = setTimeout(() => { timer = null; sv.pressed = Date.now(); if (navigator.vibrate) navigator.vibrate(12); openFlag(row); }, 550);
|
||||
});
|
||||
list.addEventListener('pointermove', (e) => { if (timer && Math.hypot(e.clientX - sx, e.clientY - sy) > 8) cancel(); });
|
||||
['pointerup', 'pointercancel', 'pointerleave', 'scroll'].forEach((t) => list.addEventListener(t, cancel, { passive: true }));
|
||||
list.addEventListener('contextmenu', (e) => { if (flagsEnabled()) e.preventDefault(); });
|
||||
list.addEventListener('click', (e) => {
|
||||
if (Date.now() - sv.pressed < 700) return; // that was the long-press
|
||||
const row = e.target.closest('.sl-line');
|
||||
if (!row) return;
|
||||
if (sv.flagMode) { openFlag(row); return; }
|
||||
const l = lines()[Number(row.dataset.i)];
|
||||
if (l && l.t !== null && l.t !== undefined) Player.seek(Math.max(0, l.t + offset()));
|
||||
});
|
||||
}
|
||||
window.addEventListener('resize', () => { if (serviceOn()) renderService(); });
|
||||
// Sizes are measured with the web font; a measurement taken while it was
|
||||
// still loading used the narrower fallback, so re-fit once fonts land.
|
||||
@@ -3658,7 +3726,7 @@ const Notes = (() => {
|
||||
});
|
||||
|
||||
return {
|
||||
load, tick, addBookmark, addNote, handleSyncKey, serviceRefresh,
|
||||
load, tick, addBookmark, addNote, handleSyncKey, serviceRefresh, refreshFlags: () => { sv.flagsKey = ''; renderServiceButtons(); paintFlags(); loadFlags(); },
|
||||
stageLyrics: { render: renderStage, set: setStageLyrics, on: stageOn },
|
||||
};
|
||||
})();
|
||||
@@ -6732,7 +6800,7 @@ function updateWakeStateUI() {
|
||||
const el = $('serviceWakeState');
|
||||
if (!el) return;
|
||||
const isAwake = !!(serviceWakeLock && !serviceWakeLock.released);
|
||||
el.textContent = isAwake ? 'Screen will stay awake' : 'Screen may sleep';
|
||||
el.textContent = isAwake ? 'Screen stays awake' : 'Screen may sleep';
|
||||
el.classList.toggle('awake', isAwake);
|
||||
}
|
||||
|
||||
@@ -6752,6 +6820,8 @@ function updateServiceProgress(cur, dur) {
|
||||
const durEl = $('serviceDurTime');
|
||||
if (curEl) curEl.textContent = fmtTime(cur || 0);
|
||||
if (durEl) durEl.textContent = fmtTime(dur || 0);
|
||||
const fill = $('serviceProgressFill');
|
||||
if (fill) fill.style.width = (dur > 0 ? Math.min(100, Math.max(0, (cur || 0) / dur * 100)) : 0).toFixed(1) + '%';
|
||||
}
|
||||
|
||||
function updateServiceMode() {
|
||||
@@ -6830,9 +6900,7 @@ function updateServiceVideoUI() {
|
||||
applyServiceVideoLayout();
|
||||
const btn = $('serviceVideoBtn');
|
||||
if (btn) {
|
||||
btn.textContent = serviceVideoOn() ? '📺 Video on' : '📺 Video off';
|
||||
btn.setAttribute('aria-pressed', serviceVideoOn() ? 'true' : 'false');
|
||||
btn.classList.toggle('on', serviceVideoOn());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7543,6 +7611,13 @@ async function renderSettings() {
|
||||
</span>
|
||||
<input id="setSaveBeforePlay" type="checkbox" ${data.settings.saveBeforePlay ? 'checked' : ''} />
|
||||
</label>
|
||||
<label class="set-row">
|
||||
<span>
|
||||
Report wrong lyric lines
|
||||
<small>Service mode → Lyrics shows a Report button, and holding a line reports it. Saved with your online profile, so it follows you between devices.</small>
|
||||
</span>
|
||||
<input id="setLyricFlags" type="checkbox" ${data.settings.lyricFlags !== false ? 'checked' : ''} />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="set-group">
|
||||
@@ -7737,6 +7812,11 @@ async function renderSettings() {
|
||||
data.settings.saveBeforePlay = e.target.checked;
|
||||
persist();
|
||||
});
|
||||
$('setLyricFlags').addEventListener('change', (e) => {
|
||||
data.settings.lyricFlags = e.target.checked;
|
||||
persist();
|
||||
Notes.refreshFlags();
|
||||
});
|
||||
$('setAutoPreload').addEventListener('change', (e) => {
|
||||
data.settings.autoPreload = e.target.checked;
|
||||
persist();
|
||||
|
||||
Reference in New Issue
Block a user