feat: click loud-section chips to seek audio; J/K keyboard shortcuts

Analysis chips are now buttons. Clicking one opens the player (if
not already open) and seeks to the section start. J skips to the
previous loud section, K to the next. Shortcuts are suppressed when
focus is inside an input field.
This commit is contained in:
2026-04-29 20:33:27 +02:00
parent 6d16b2c0a3
commit d583620f8c
+50 -4
View File
@@ -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 = '<div class="spin" aria-live="polite" aria-busy="true">Analysing…</div>';
@@ -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);
}