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>
31 lines
1.1 KiB
JavaScript
31 lines
1.1 KiB
JavaScript
// Every URL the UI talks to, in one place. A leaf module.
|
|
|
|
const q = encodeURIComponent;
|
|
|
|
async function getJson(url) {
|
|
return (await fetch(url)).json();
|
|
}
|
|
|
|
export const getConfig = () => getJson('/api/config');
|
|
export const getFiles = () => getJson('/api/files');
|
|
export const getStatus = () => getJson('/api/status');
|
|
export const getStorage = () => getJson('/api/storage');
|
|
|
|
export const analyze = (file, {margin, minGap, minDur}) =>
|
|
getJson(`/api/analyze?file=${q(file)}&margin=${q(margin)}`
|
|
+ `&min_gap=${q(minGap)}&min_duration=${q(minDur)}`);
|
|
|
|
export async function deleteFile(file) {
|
|
const r = await fetch('/api/files/' + q(file), {method: 'DELETE'});
|
|
if (!r.ok) {
|
|
const d = await r.json().catch(() => ({}));
|
|
throw new Error(d.error || r.statusText);
|
|
}
|
|
}
|
|
|
|
export const clipUrl = (file, start, end) =>
|
|
`/api/clip?file=${q(file)}&start=${start.toFixed(1)}&end=${end.toFixed(1)}`;
|
|
export const cutUrl = (file, start, end) => `/api/cut?file=${q(file)}&start=${start}&end=${end}`;
|
|
export const streamUrl = file => '/stream/' + q(file);
|
|
export const downloadUrl = file => '/download/' + q(file);
|