Add admin analytics, metadata collection, and grouped lyric cues
Queue reviewable Whisper drafts from the song list and lyrics editor. Preserve line breaks within one timed cue across editing, saving, reporting, and service views. Add storage and listening analytics with a durable metadata collector, related-search depth, video limits, thumbnail storage, and a browsable metadata library.
This commit is contained in:
@@ -3247,7 +3247,12 @@ function fitLyricLines(list, { maxShare = 0.17, maxPx = 160, minPx = 18, uniform
|
||||
// re-laid-out every line (O(n²) — seconds on a phone for a long sermon).
|
||||
const fits = [];
|
||||
for (const el of rows) {
|
||||
widthAt.text = el.textContent;
|
||||
// Size a grouped cue by its widest visual line, preserving tight breaks.
|
||||
const parts = 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;
|
||||
}, { text: '', width: -1 }).text;
|
||||
const w100 = widthAt(100);
|
||||
let px = cap;
|
||||
if (w100) {
|
||||
@@ -3650,16 +3655,19 @@ const Notes = (() => {
|
||||
body.innerHTML = `
|
||||
<p class="lyr-editor-help">One lyric line per line. Optional: <code>[1:23.45]</code> times a line
|
||||
(or time them later with Tap-sync), <code># Chorus</code> is a section, <code>! Drums in</code> a band cue,
|
||||
<code>@ Key G, Capo 2, 70 BPM</code> adds tags. Pasted .lrc files work as-is.</p>
|
||||
<code>@ Key G, Capo 2, 70 BPM</code> adds tags. Use Shift+Enter or Line break to group words under the same timestamp (<code>| continuation</code> in text). Pasted .lrc files work as-is.</p>
|
||||
${note ? `<p class="lyr-editor-note">${escapeHtml(note)}</p>` : ''}
|
||||
<textarea class="lyr-editor-text" spellcheck="false" placeholder="Paste or type the lyrics…"></textarea>
|
||||
<div class="lyr-editor-tools">
|
||||
<button type="button" class="np-btn" data-act="captions">⤓ Import YouTube captions</button>
|
||||
<button type="button" class="np-btn" data-act="break">↵ Line break (same time)</button>
|
||||
<button type="button" class="np-btn" data-act="stamp">⏱ Stamp current time on this line</button>
|
||||
</div>`;
|
||||
const ta = body.querySelector('textarea');
|
||||
ta.value = prefill != null ? prefill : (st.lyrics ? LC.toText(st.lyrics.data) : '');
|
||||
ta.addEventListener('keydown', (e) => e.stopPropagation());
|
||||
const insertBreak = () => { ta.setRangeText('\n| ', ta.selectionStart, ta.selectionEnd, 'end'); ta.focus(); };
|
||||
body.querySelector('[data-act="break"]').addEventListener('click', insertBreak);
|
||||
ta.addEventListener('keydown', (e) => { e.stopPropagation(); if (e.key === 'Enter' && e.shiftKey) { e.preventDefault(); insertBreak(); } });
|
||||
const runCaptions = async () => {
|
||||
const text = await captionsText();
|
||||
if (text != null) ta.value = text;
|
||||
@@ -3667,7 +3675,8 @@ const Notes = (() => {
|
||||
body.querySelector('[data-act="captions"]').addEventListener('click', runCaptions);
|
||||
body.querySelector('[data-act="stamp"]').addEventListener('click', () => {
|
||||
const pos = ta.selectionStart;
|
||||
const start = ta.value.lastIndexOf('\n', pos - 1) + 1;
|
||||
let start = ta.value.lastIndexOf('\n', pos - 1) + 1;
|
||||
while (start > 0 && /^\s*\| /.test(ta.value.slice(start).split('\n')[0])) start = ta.value.lastIndexOf('\n', start - 2) + 1;
|
||||
const endNl = ta.value.indexOf('\n', start);
|
||||
const end = endNl < 0 ? ta.value.length : endNl;
|
||||
const lineText = ta.value.slice(start, end).replace(/^(\[[^\]]*\]\s*)+/, '');
|
||||
@@ -3675,7 +3684,11 @@ const Notes = (() => {
|
||||
ta.value = ta.value.slice(0, start) + stamp + lineText + ta.value.slice(end);
|
||||
// Move to the next line so repeated taps walk down the sheet.
|
||||
const next = ta.value.indexOf('\n', start);
|
||||
const caret = next < 0 ? ta.value.length : next + 1;
|
||||
let caret = next < 0 ? ta.value.length : next + 1;
|
||||
while (/^\s*\| /.test(ta.value.slice(caret).split('\n')[0])) {
|
||||
const following = ta.value.indexOf('\n', caret);
|
||||
caret = following < 0 ? ta.value.length : following + 1;
|
||||
}
|
||||
ta.focus();
|
||||
ta.setSelectionRange(caret, caret);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user