Files
adminandClaude Opus 5.5 2a19515c27 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>
2026-10-18 01:05:15 +02:00

50 lines
2.0 KiB
JavaScript

// The file listing and what the UI knows about each file. A leaf apart from
// util.js.
import {fileStartEpoch, fmtHM} from './util.js';
// Full listing from /api/files. Reassigned on every load; importers see the
// new array through the live binding.
export let allFiles = [];
// _idx is per filename and stable for the whole session: assigned the first
// time a name is seen and never reused. DOM ids (row-, pbtn-, aud-, …) are
// built from it, so they survive reloads — that is what lets a re-rendered day
// keep focus where it was. It used to be the position in the listing, which
// shifted on every rollover and left stored indexes pointing one row off.
const idxByName = new Map();
const stableIdx = name => {
if (!idxByName.has(name)) idxByName.set(name, idxByName.size);
return idxByName.get(name);
};
export function setFiles(list) {
allFiles = list.map(f => ({...f, _idx: stableIdx(f.name)}));
}
export function removeFile(name) {
allFiles = allFiles.filter(f => f.name !== name);
}
export const fileByName = name => allFiles.find(f => f.name === name) ?? null;
export const fileByIdx = idx => allFiles.find(f => f._idx === idx) ?? null;
// Queue entries carry the filename, never a row index; resolve at use time.
// Null when the file is gone from the current listing.
export const fileIdxByName = name => fileByName(name)?._idx ?? null;
export const isAnalysable = f => f.ext === 'wav' || f.ext === 'flac';
// idx -> [{start, end, score}], populated by analyse()
export const sectionMap = new Map();
// What a recording is called wherever it is spoken: its wall-clock span from
// the filename clock, "22:00 to 23:00". The filename itself ("20260523_220000")
// reads as a string of numbers. Non-standard names fall back to the name.
export function fileSpan(f) {
const start = fileStartEpoch(f.date);
if (start == null) return f.name;
if (f.recording) return `${fmtHM(start)} to now`;
if (f.duration == null) return fmtHM(start);
return `${fmtHM(start)} to ${fmtHM(Math.round((start + f.duration) / 60) * 60)}`;
}