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:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user