diff --git a/web.py b/web.py index 55c83fb..0594e0e 100644 --- a/web.py +++ b/web.py @@ -554,6 +554,9 @@ svg.wave{display:block;width:100%;height:56px} .chips{display:flex;flex-wrap:wrap;gap:5px;margin-top:8px} .chip{background:#431407;color:var(--orange);border:1px solid #7c2d12;border-radius:4px; padding:2px 8px;font-size:11px;font-family:ui-monospace,monospace} +button.chip{cursor:pointer} +button.chip:hover{background:#6c1f08;border-color:#9a3412} +button.chip:focus-visible{outline:2px solid var(--accent);outline-offset:2px} .quiet{color:var(--muted);font-size:12px;margin-top:6px} .spin{color:var(--muted);font-style:italic;font-size:12px;padding:6px 0} .empty{text-align:center;padding:60px;color:var(--muted)} @@ -617,6 +620,9 @@ const pad = n => String(n).padStart(2,'0'); // idx -> filename, for live-status polling const recMap = new Map(); +// idx -> [{start,end}], populated after analysis +const sectionMap = new Map(); +let activePlayerIdx = null; function togglePlayer(idx, filename) { const prow = document.getElementById('prow-'+idx); @@ -631,6 +637,7 @@ function togglePlayer(idx, filename) { audio.load(); audio.setAttribute('data-src-set','1'); } + activePlayerIdx = idx; prow.hidden = false; btn.setAttribute('aria-expanded','true'); btn.textContent = '⏹ Hide'; @@ -681,7 +688,17 @@ function drawWave(rms, sections, duration, filename) { return svg; } -async function analyse(filename, cell, btn) { +function seekToSection(idx, filename, startSec) { + const pbtn = document.getElementById('pbtn-'+idx); + if (pbtn.getAttribute('aria-expanded') !== 'true') togglePlayer(idx, filename); + activePlayerIdx = idx; + const audio = document.getElementById('aud-'+idx); + const doSeek = () => { audio.currentTime = startSec; }; + if (audio.readyState >= 1) doSeek(); + else audio.addEventListener('loadedmetadata', doSeek, {once: true}); +} + +async function analyse(idx, filename, cell, btn) { btn.disabled = true; btn.textContent = '…'; cell.innerHTML = '
Analysing…
'; @@ -701,15 +718,19 @@ async function analyse(filename, cell, btn) { const chips = document.createElement('div'); chips.className='chips'; chips.setAttribute('role','list'); - chips.setAttribute('aria-label','Loud sections'); + chips.setAttribute('aria-label','Loud sections — click to jump, J/K to step'); if (d.sections && d.sections.length) { + sectionMap.set(idx, d.sections); d.sections.forEach(s => { - const c = document.createElement('span'); + const c = document.createElement('button'); c.className='chip'; c.setAttribute('role','listitem'); + c.title = 'Jump to this section (or use J/K keys)'; c.textContent = `${fmtT(s.start)} – ${fmtT(s.end)}`; + c.addEventListener('click', () => seekToSection(idx, filename, s.start)); chips.appendChild(c); }); } else { + sectionMap.delete(idx); const q = document.createElement('span'); q.className='quiet'; q.setAttribute('role','listitem'); q.textContent='No loud sections found'; @@ -723,6 +744,31 @@ async function analyse(filename, cell, btn) { } } +// J = previous section, K = next section (only when focus is not in an input) +document.addEventListener('keydown', e => { + if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; + if (activePlayerIdx === null) return; + const sections = sectionMap.get(activePlayerIdx) || []; + if (!sections.length) return; + const audio = document.getElementById('aud-'+activePlayerIdx); + if (!audio) return; + if (e.key === 'j' || e.key === 'J') { + const cur = audio.currentTime; + let target = sections[0].start; + for (let i = sections.length - 1; i >= 0; i--) { + if (sections[i].start < cur - 1) { target = sections[i].start; break; } + } + audio.currentTime = target; + e.preventDefault(); + } else if (e.key === 'k' || e.key === 'K') { + const cur = audio.currentTime; + for (const s of sections) { + if (s.start > cur + 0.5) { audio.currentTime = s.start; break; } + } + e.preventDefault(); + } +}); + async function deleteFile(idx, filename) { if (!confirm(`Delete "${filename}"?\nThis cannot be undone.`)) return; const btn = document.getElementById('delbtn-'+idx); @@ -848,7 +894,7 @@ async function load() { abtn.disabled = true; abtn.title = 'Recording in progress — analyse after recording stops'; } else { - abtn.addEventListener('click', () => analyse(f.name, cell, abtn)); + abtn.addEventListener('click', () => analyse(i, f.name, cell, abtn)); } cell.appendChild(abtn); }