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);
}