refactor: deduplicate web UI JS and improve screen reader support
- Remove fmtT (identical duplicate of fmtDur) - Extract closePlayer() shared by togglePlayer, day collapse, and cross-file day section jumps - Reuse seekToSection from jumpToDaySection instead of a copied open/seek/announce block - Fix chip semantics: role=listitem on <button> overrode the button role for assistive tech; use role=group on the container instead - Drop redundant aria-hidden toggling on the REC badge (hidden already removes it from the accessibility tree) - Respect prefers-reduced-motion for the REC pulse animation Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -680,6 +680,7 @@ tr.data-row:hover td{background:var(--surf)}
|
||||
color:var(--red);border:1px solid #7f1d1d;background:#2d0808;
|
||||
animation:pulse 1.5s ease-in-out infinite}
|
||||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.45}}
|
||||
@media (prefers-reduced-motion:reduce){.badge-rec{animation:none}}
|
||||
.muted{color:var(--muted)}
|
||||
button{cursor:pointer;border:1px solid var(--brd);background:var(--surf);
|
||||
color:var(--txt);padding:4px 10px;border-radius:5px;font-size:12px;white-space:nowrap}
|
||||
@@ -806,10 +807,6 @@ const fmtSize = b => {
|
||||
if (b<1<<30) return (b/(1<<20)).toFixed(1)+' MB';
|
||||
return (b/(1<<30)).toFixed(2)+' GB';
|
||||
};
|
||||
const fmtT = s => {
|
||||
const h=Math.floor(s/3600),m=Math.floor((s%3600)/60),sec=Math.floor(s%60);
|
||||
return h?`${h}:${pad(m)}:${pad(sec)}`:`${m}:${pad(sec)}`;
|
||||
};
|
||||
const pad = n => String(n).padStart(2,'0');
|
||||
|
||||
function announce(msg) {
|
||||
@@ -825,8 +822,8 @@ const getPreroll = () => {
|
||||
function setCutFields(idx, startSec, endSec) {
|
||||
const startEl = document.getElementById('cut-start-'+idx);
|
||||
const endEl = document.getElementById('cut-end-'+idx);
|
||||
if (startEl) startEl.value = fmtT(startSec);
|
||||
if (endEl && endSec != null) endEl.value = fmtT(endSec);
|
||||
if (startEl) startEl.value = fmtDur(startSec);
|
||||
if (endEl && endSec != null) endEl.value = fmtDur(endSec);
|
||||
}
|
||||
|
||||
// idx -> filename, for live-status polling
|
||||
@@ -853,32 +850,41 @@ function groupByDay(files) {
|
||||
return map;
|
||||
}
|
||||
|
||||
function closePlayer(idx) {
|
||||
const prow = document.getElementById('prow-'+idx);
|
||||
if (!prow || prow.hidden) return;
|
||||
document.getElementById('aud-'+idx)?.pause();
|
||||
prow.hidden = true;
|
||||
const btn = document.getElementById('pbtn-'+idx);
|
||||
if (btn) {
|
||||
btn.setAttribute('aria-expanded','false');
|
||||
btn.textContent = '▶ Play';
|
||||
btn.setAttribute('aria-label','Play '+(recMap.get(idx) || ''));
|
||||
}
|
||||
}
|
||||
|
||||
function togglePlayer(idx, filename) {
|
||||
const prow = document.getElementById('prow-'+idx);
|
||||
const btn = document.getElementById('pbtn-'+idx);
|
||||
const audio = document.getElementById('aud-'+idx);
|
||||
const open = btn.getAttribute('aria-expanded') === 'true';
|
||||
|
||||
if (!open) {
|
||||
if (!audio.getAttribute('data-src-set')) {
|
||||
audio.preload = 'auto';
|
||||
audio.src = '/stream/' + encodeURIComponent(filename);
|
||||
audio.load();
|
||||
audio.setAttribute('data-src-set','1');
|
||||
}
|
||||
activePlayerIdx = idx;
|
||||
prow.hidden = false;
|
||||
btn.setAttribute('aria-expanded','true');
|
||||
btn.textContent = '⏹ Hide';
|
||||
btn.setAttribute('aria-label','Hide player for '+filename);
|
||||
audio.focus();
|
||||
} else {
|
||||
audio.pause();
|
||||
prow.hidden = true;
|
||||
btn.setAttribute('aria-expanded','false');
|
||||
btn.textContent = '▶ Play';
|
||||
btn.setAttribute('aria-label','Play '+filename);
|
||||
if (open) {
|
||||
closePlayer(idx);
|
||||
return;
|
||||
}
|
||||
if (!audio.getAttribute('data-src-set')) {
|
||||
audio.preload = 'auto';
|
||||
audio.src = '/stream/' + encodeURIComponent(filename);
|
||||
audio.load();
|
||||
audio.setAttribute('data-src-set','1');
|
||||
}
|
||||
activePlayerIdx = idx;
|
||||
prow.hidden = false;
|
||||
btn.setAttribute('aria-expanded','true');
|
||||
btn.textContent = '⏹ Hide';
|
||||
btn.setAttribute('aria-label','Hide player for '+filename);
|
||||
audio.focus();
|
||||
}
|
||||
|
||||
function drawWave(rms, sections, duration, filename) {
|
||||
@@ -939,7 +945,7 @@ function seekToSection(idx, filename, startSec, endSec, sectionIdx) {
|
||||
setCutFields(idx, startSec, endSec);
|
||||
if (sectionIdx != null) {
|
||||
const total = (sectionMap.get(idx) || []).length;
|
||||
announce(`Section ${sectionIdx + 1} of ${total}: ${fmtT(startSec)} to ${fmtT(endSec)}`);
|
||||
announce(`Section ${sectionIdx + 1} of ${total}: ${fmtDur(startSec)} to ${fmtDur(endSec)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -972,22 +978,22 @@ async function analyse(idx, filename, cell, btn) {
|
||||
|
||||
const chips = document.createElement('div');
|
||||
chips.className='chips';
|
||||
chips.setAttribute('role','list');
|
||||
chips.setAttribute('role','group');
|
||||
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, si) => {
|
||||
const c = document.createElement('button');
|
||||
c.className='chip'; c.setAttribute('role','listitem');
|
||||
c.className='chip';
|
||||
c.title = 'Jump to this section (or use J/K keys)';
|
||||
c.textContent = `${fmtT(s.start)} – ${fmtT(s.end)}`;
|
||||
c.textContent = `${fmtDur(s.start)} – ${fmtDur(s.end)}`;
|
||||
c.addEventListener('click', () => seekToSection(idx, filename, s.start, s.end, si));
|
||||
chips.appendChild(c);
|
||||
});
|
||||
} else {
|
||||
sectionMap.delete(idx);
|
||||
const q = document.createElement('span');
|
||||
q.className='quiet'; q.setAttribute('role','listitem');
|
||||
q.className='quiet';
|
||||
q.textContent='No loud sections found';
|
||||
chips.appendChild(q);
|
||||
}
|
||||
@@ -1043,7 +1049,7 @@ document.addEventListener('keydown', e => {
|
||||
const s = sections[targetIdx];
|
||||
audio.currentTime = Math.max(0, s.start - preroll);
|
||||
setCutFields(activePlayerIdx, s.start, s.end);
|
||||
announce(`Section ${targetIdx + 1} of ${sections.length}: ${fmtT(s.start)} to ${fmtT(s.end)}`);
|
||||
announce(`Section ${targetIdx + 1} of ${sections.length}: ${fmtDur(s.start)} to ${fmtDur(s.end)}`);
|
||||
} else {
|
||||
announce('Beginning of sections');
|
||||
}
|
||||
@@ -1055,7 +1061,7 @@ document.addEventListener('keydown', e => {
|
||||
const s = sections[i];
|
||||
audio.currentTime = Math.max(0, s.start - preroll);
|
||||
setCutFields(activePlayerIdx, s.start, s.end);
|
||||
announce(`Section ${i + 1} of ${sections.length}: ${fmtT(s.start)} to ${fmtT(s.end)}`);
|
||||
announce(`Section ${i + 1} of ${sections.length}: ${fmtDur(s.start)} to ${fmtDur(s.end)}`);
|
||||
jumped = true;
|
||||
break;
|
||||
}
|
||||
@@ -1242,7 +1248,7 @@ function renderFiles(files) {
|
||||
tr.id = 'row-'+i;
|
||||
|
||||
const recBadge = `<span id="rec-${i}" class="badge-rec"${isRec?'':' hidden'}
|
||||
aria-label="Currently recording" aria-hidden="${isRec?'false':'true'}">
|
||||
aria-label="Currently recording">
|
||||
<span aria-hidden="true">●</span> REC</span>`;
|
||||
|
||||
tr.innerHTML = `
|
||||
@@ -1310,19 +1316,7 @@ function renderFiles(files) {
|
||||
headBar.classList.toggle('open', nowExp);
|
||||
document.getElementById('daytbl-' + dayId).hidden = !nowExp;
|
||||
if (!nowExp) {
|
||||
dayFiles.forEach(f => {
|
||||
const prow = document.getElementById('prow-' + f._idx);
|
||||
if (prow && !prow.hidden) {
|
||||
document.getElementById('aud-' + f._idx)?.pause();
|
||||
prow.hidden = true;
|
||||
const pbtn = document.getElementById('pbtn-' + f._idx);
|
||||
if (pbtn) {
|
||||
pbtn.setAttribute('aria-expanded', 'false');
|
||||
pbtn.textContent = '▶ Play';
|
||||
pbtn.setAttribute('aria-label', 'Play ' + f.name);
|
||||
}
|
||||
}
|
||||
});
|
||||
dayFiles.forEach(f => closePlayer(f._idx));
|
||||
document.getElementById('dayhl-' + dayId).hidden = true;
|
||||
if (dayActiveId === dayId) {
|
||||
dayActiveSections = [];
|
||||
@@ -1492,13 +1486,12 @@ async function dayHighlights(dayId, analyzableFiles) {
|
||||
} else {
|
||||
const chips = document.createElement('div');
|
||||
chips.className = 'chips';
|
||||
chips.setAttribute('role', 'list');
|
||||
chips.setAttribute('role', 'group');
|
||||
chips.setAttribute('aria-label', 'Day loud sections — click to jump, J/K to step across files');
|
||||
dayActiveSections.forEach((sec, si) => {
|
||||
const c = document.createElement('button');
|
||||
c.className = 'chip';
|
||||
c.setAttribute('role', 'listitem');
|
||||
c.title = sec.filename + ' @ ' + fmtT(sec.start);
|
||||
c.title = sec.filename + ' @ ' + fmtDur(sec.start);
|
||||
const d = new Date(sec.absStart * 1000);
|
||||
const hms = d.getHours().toString().padStart(2,'0') + ':'
|
||||
+ d.getMinutes().toString().padStart(2,'0') + ':'
|
||||
@@ -1525,39 +1518,13 @@ async function dayHighlights(dayId, analyzableFiles) {
|
||||
function jumpToDaySection(si) {
|
||||
if (si < 0 || si >= dayActiveSections.length) return;
|
||||
dayActiveSectionCursor = si;
|
||||
const sec = dayActiveSections[si];
|
||||
const { fileIdx, filename, start, end } = sec;
|
||||
const { fileIdx, filename, start, end } = dayActiveSections[si];
|
||||
|
||||
// Close the previous player if switching to a different file
|
||||
if (activePlayerIdx !== null && activePlayerIdx !== fileIdx) {
|
||||
const prevProw = document.getElementById('prow-' + activePlayerIdx);
|
||||
if (prevProw && !prevProw.hidden) {
|
||||
document.getElementById('aud-' + activePlayerIdx)?.pause();
|
||||
prevProw.hidden = true;
|
||||
const prevPbtn = document.getElementById('pbtn-' + activePlayerIdx);
|
||||
if (prevPbtn) {
|
||||
prevPbtn.setAttribute('aria-expanded', 'false');
|
||||
prevPbtn.textContent = '▶ Play';
|
||||
prevPbtn.setAttribute('aria-label', 'Play ' + (recMap.get(activePlayerIdx) || ''));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (activePlayerIdx !== null && activePlayerIdx !== fileIdx) closePlayer(activePlayerIdx);
|
||||
|
||||
// Open this file's player
|
||||
const pbtn = document.getElementById('pbtn-' + fileIdx);
|
||||
if (pbtn && pbtn.getAttribute('aria-expanded') !== 'true') togglePlayer(fileIdx, filename);
|
||||
activePlayerIdx = fileIdx;
|
||||
|
||||
const audio = document.getElementById('aud-' + fileIdx);
|
||||
if (!audio) return;
|
||||
audio.preload = 'auto';
|
||||
const seekTo = Math.max(0, start - getPreroll());
|
||||
const doSeek = () => { audio.currentTime = seekTo; audio.play().catch(() => {}); };
|
||||
if (audio.readyState >= 1) doSeek();
|
||||
else audio.addEventListener('loadedmetadata', doSeek, { once: true });
|
||||
|
||||
setCutFields(fileIdx, start, end);
|
||||
announce(`Day section ${si + 1} of ${dayActiveSections.length}: ${fmtT(start)}–${fmtT(end)} in ${filename}`);
|
||||
seekToSection(fileIdx, filename, start, end, null);
|
||||
announce(`Day section ${si + 1} of ${dayActiveSections.length}: ${fmtDur(start)}–${fmtDur(end)} in ${filename}`);
|
||||
}
|
||||
|
||||
function applyFilters() {
|
||||
@@ -1600,10 +1567,7 @@ async function pollStatus() {
|
||||
const active = new Set(s.active || []);
|
||||
recMap.forEach((filename, idx) => {
|
||||
const badge = document.getElementById('rec-'+idx);
|
||||
if (!badge) return;
|
||||
const on = active.has(filename);
|
||||
badge.hidden = !on;
|
||||
badge.setAttribute('aria-hidden', on ? 'false' : 'true');
|
||||
if (badge) badge.hidden = !active.has(filename);
|
||||
});
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user