feat: split the web UI into modules and rework it for screen readers
webui.html becomes webui/ (index.html, style.css, ES modules in js/, no
build step); web.py serves it under / and /ui/<path> with the same
containment check as recordings.
How it feels with a screen reader:
- Days read "Today, Saturday 17 October"; recordings are named by their
wall-clock span ("22:00 to 23:00"), which is the row header and what
every button says. The filename moves to the last column.
- Focus is never dropped: busy buttons stay focused (aria-disabled),
finished analyses focus their first section, delete moves to the next
row, closing the clip bar (x or Escape) returns to where you were.
- Reloads rebuild only the days whose content changed; DOM ids are stable
per filename, so a rollover no longer resets the reading position.
- Only user-initiated actions are announced; highlights completion is
spoken (the progress area was stuck aria-busy before).
- Media Session previous/next track step sections like J/K (AirPods,
lock screen, media keys).
- Row layout: Time | Length | Loudness | Player | Options | File. The
player opens inside its cell; Download/Cut/Delete are always there,
and Cut toggles its start/end fields in place.
- Settings tucked behind a <details>; pre-roll and auto-advance are
remembered per browser; no CSS uppercase on headers or badges.
Fixes the REC badge showing on every row: its inline-flex display beat
the UA [hidden] rule. One global [hidden]{display:none !important} now
covers every case.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
// The settings panel: analysis params, pre-roll and the auto-advance mode.
|
||||
|
||||
import {$, remember} from './util.js';
|
||||
|
||||
// The three analysis controls, read straight from the DOM. Deliberately no
|
||||
// hardcoded fallbacks: the inputs are seeded from /api/config at startup, so
|
||||
// web.py's MARGIN_DB / MIN_GAP_SECONDS / MIN_DURATION_SECONDS stay the single
|
||||
// source of truth. An empty input sends an empty param, which _qs_float()
|
||||
// falls back to the server default for.
|
||||
export const analysisParams = () => ({
|
||||
margin: $('margin-input').value,
|
||||
minGap: $('min-gap-input').value,
|
||||
minDur: $('min-duration-input').value,
|
||||
});
|
||||
// Same values as one string; a highlights panel built with other values is
|
||||
// stale and gets recomputed on the next expand
|
||||
export const hlParams = () => Object.values(analysisParams()).join('|');
|
||||
|
||||
export const getPreroll = () => {
|
||||
const v = parseFloat($('preroll-input').value);
|
||||
return isNaN(v) || v < 0 ? 0 : v;
|
||||
};
|
||||
|
||||
export const advanceMode = () =>
|
||||
document.querySelector('input[name="clip-adv"]:checked')?.value || 'off';
|
||||
|
||||
// Does a server-side cached analysis match the current control values?
|
||||
// Auto-loading on a mismatch would silently recompute every file.
|
||||
export function cachedParamsMatch(ca) {
|
||||
const p = analysisParams();
|
||||
return ca != null
|
||||
&& Number(ca.margin) === parseFloat(p.margin)
|
||||
&& Number(ca.min_gap) === parseFloat(p.minGap)
|
||||
&& Number(ca.min_duration) === parseFloat(p.minDur);
|
||||
}
|
||||
|
||||
// Seed the analysis params from /api/config, and restore the per-browser
|
||||
// conveniences. Margin/gap/min-duration are not remembered client-side: a
|
||||
// stale browser value would quietly disagree with the server's defaults.
|
||||
export function initSettings(cfg) {
|
||||
if (cfg.margin != null) $('margin-input').value = cfg.margin;
|
||||
if (cfg.min_gap != null) $('min-gap-input').value = cfg.min_gap;
|
||||
if (cfg.min_duration != null) $('min-duration-input').value = cfg.min_duration;
|
||||
|
||||
const pre = remember.get('preroll');
|
||||
if (pre != null) $('preroll-input').value = pre;
|
||||
$('preroll-input').addEventListener('change', e => remember.set('preroll', e.target.value));
|
||||
|
||||
const adv = remember.get('advance');
|
||||
const radio = adv && document.querySelector(`input[name="clip-adv"][value="${adv}"]`);
|
||||
if (radio) radio.checked = true;
|
||||
document.querySelectorAll('input[name="clip-adv"]').forEach(r =>
|
||||
r.addEventListener('change', () => remember.set('advance', r.value)));
|
||||
|
||||
const panel = $('settings');
|
||||
panel.open = remember.get('settingsOpen') === '1';
|
||||
panel.addEventListener('toggle', () => remember.set('settingsOpen', panel.open ? '1' : '0'));
|
||||
}
|
||||
Reference in New Issue
Block a user