Preserve and display bilingual and phonetic lyric cues

This commit is contained in:
Jonathan Sykes
2026-10-03 15:20:52 +08:00
parent c8ab741d72
commit d4354d220e
13 changed files with 78 additions and 11 deletions

View File

@@ -3298,7 +3298,7 @@ function fitLyricLines(list, { maxShare = 0.17, maxPx = 160, minPx = 18, uniform
const fits = [];
for (const el of rows) {
// Size a grouped cue by its widest visual line, preserving tight breaks.
const parts = el.textContent.split('\n');
const parts = (el.querySelector('.lyric-primary') || el).textContent.split('\n');
widthAt.text = parts.reduce((widest, part) => {
widthAt.text = part; const width = widthAt(100);
return width > widest.width ? { text: part, width } : widest;
@@ -3483,7 +3483,7 @@ const Notes = (() => {
if (l.kind === 'cue') {
return `<div class="lyr-cue" data-i="${i}"><span>🥁 ${escapeHtml(l.text)}</span><span class="lyr-cue-in"></span></div>`;
}
return `<div class="lyr-line${time ? '' : ' untimed'}" data-i="${i}"><span class="lyr-time">${time}</span><span class="lyr-text">${escapeHtml(l.text)}</span></div>`;
return `<div class="lyr-line${time ? '' : ' untimed'}" data-i="${i}"><span class="lyr-time">${time}</span><span class="lyr-text">${LyricsDisplay.html(l)}</span></div>`;
}).join('');
if (st.sync) renderSyncPointer();
else tick(now(), true);
@@ -4160,7 +4160,7 @@ const Notes = (() => {
return;
}
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('');
`<div class="sl-line${l.t === null || l.t === undefined ? ' untimed' : ''}" data-i="${l.i}">${LyricsDisplay.html(l)}</div>`).join('');
paintFlags();
loadFlags();
applyFits();
@@ -4271,7 +4271,7 @@ const Notes = (() => {
return;
}
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('');
`<div class="sl-line${l.t === null || l.t === undefined ? ' untimed' : ''}" data-i="${l.i}">${LyricsDisplay.html(l)}</div>`).join('');
fitLyricLines(list, { maxShare: 0.15, maxPx: 96 });
tickStage(now(), true);
clearTimeout(stg.refit);
@@ -6060,7 +6060,7 @@ const Presenter = (() => {
const pad = Math.round(list.clientHeight / 2);
list.style.paddingTop = pad + 'px';
list.style.paddingBottom = pad + 'px';
list.innerHTML = sung.map((l) => `<div class="sl-line" data-i="${l.i}">${escapeHtml(l.text)}</div>`).join('');
list.innerHTML = sung.map((l) => `<div class="sl-line" data-i="${l.i}">${LyricsDisplay.html(l)}</div>`).join('');
refit();
st.idx = -2;
tick(true);
@@ -12449,6 +12449,7 @@ async function boot() {
PartyDJ.register(() => $('navPartyBtn').click());
SetlistImport.configure({ library: async () => { let uploads = []; try { const j = await webFetch('/api/uploads?limit=200'); uploads = j.uploads || []; } catch {} return [...data.history, ...data.playlists.flatMap(p => p.videos || []), ...uploads]; }, search: async q => (await API.search(q)).results || [], modal: showModal, close: closeModal, create: (name, videos) => { const pl = { id: uid(), name, videos: videos.map(v => ({ ...slim(v), tags: v.tags, key: v.key, chordPro: v.chordPro })) }; data.playlists.push(pl); persist(); view = { type: 'playlist', id: pl.id }; render(); toast(`Imported ${videos.length} songs`); } });
Remote.boot();
LyricsDisplay.register();
LowerThird.register(() => $('remoteChip').click());
Transition.renderButton();
PiP.render();