Timers: LTC is listener-only; admin switch, device/channel, grouped settings
Simplify LTC to its one working source — the built-in audio listener — and make the admin UI reflect it. Module: - Remove the stage-display websocket source (stage-ws.js, stagePassword) and the /v1/timecode/status API probe — both dead on ProPresenter 21.4. - Remove the remote-reader ingest (POST /ltc, ingestToken, tools/ ltc-reader.js): the module owns LTC end to end now. The decoder selftest moves to modules/propresenter-timers/ltc.selftest.js so the engine stays covered. - composeLtc collapses to the listener: running/stopped/nosignal, or a clear note when disabled / no device / capture failed / starting. - Manifest: ltcEnabled is now a `switch`; ltcDevice and ltcChannel carry showWhen:"ltcEnabled"; ProPresenter and LTC fields carry `group` labels. Shell (admin) — backward-compatible additions, coordinate with the shell lane: - New `switch` field type (boolean shown as the enable-toggle control). - `group`: consecutive fields render under a labeled subsection. - `showWhen: "<key>"`: a field is shown only while that boolean/switch is on (and actually collapses — .field[hidden] override, since .field's display:flex outranks the [hidden] UA default). - applyConfigPatch coerces `switch` as boolean. Documented in MODULE-GUIDE. Verified: engine selftest (29 checks); listener decodes BlackHole loopback (running 30fps, ageMs); device picker lists inputs; POST /ltc now 404s; admin shows two labeled groups, the switch, and device/channel appearing only when the switch is on (computed display none↔flex confirmed). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
7e75146838
commit
1d46657e06
+57
-5
@@ -102,15 +102,35 @@ function fieldFor(key, spec, value, passwordSet, moduleId) {
|
||||
const type = spec?.type || 'string';
|
||||
const label = spec?.label || key;
|
||||
const field = document.createElement('label');
|
||||
field.className = 'field' + (type === 'boolean' ? ' check' : '');
|
||||
field.className = 'field' + (type === 'boolean' ? ' check' : '') + (type === 'switch' ? ' switch-field' : '');
|
||||
let read;
|
||||
// For boolean/switch fields, the control other fields may depend on
|
||||
// (showWhen) — returned so the caller can wire visibility.
|
||||
let control = null;
|
||||
|
||||
if (type === 'boolean') {
|
||||
if (type === 'switch') {
|
||||
// Same toggle as the module enable switch, inline with its label.
|
||||
const toggle = document.createElement('span');
|
||||
toggle.className = 'switch';
|
||||
const input = document.createElement('input');
|
||||
input.type = 'checkbox';
|
||||
input.checked = Boolean(value);
|
||||
const track = document.createElement('span');
|
||||
track.className = 'track';
|
||||
toggle.append(input, track);
|
||||
const text = document.createElement('span');
|
||||
text.className = 'switch-label';
|
||||
text.textContent = label;
|
||||
field.append(toggle, text);
|
||||
read = () => input.checked;
|
||||
control = input;
|
||||
} else if (type === 'boolean') {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'checkbox';
|
||||
input.checked = Boolean(value);
|
||||
field.append(input, document.createTextNode(label));
|
||||
read = () => input.checked;
|
||||
control = input;
|
||||
} else if (type === 'endpoint') {
|
||||
// One consistent "Host / IP : Port" control for every module's upstream.
|
||||
const span = document.createElement('span');
|
||||
@@ -185,7 +205,7 @@ function fieldFor(key, spec, value, passwordSet, moduleId) {
|
||||
field.append(span, input);
|
||||
read = () => (type === 'number' ? Number(input.value) : input.value);
|
||||
}
|
||||
return { field, read };
|
||||
return { field, read, control };
|
||||
}
|
||||
|
||||
function buildModuleCard(mod) {
|
||||
@@ -298,10 +318,42 @@ function buildModuleCard(mod) {
|
||||
const form = document.createElement('div');
|
||||
form.className = 'module-form';
|
||||
const readers = new Map();
|
||||
const controls = new Map(); // key → boolean/switch input (for showWhen)
|
||||
const dependents = []; // { field, when } to show/hide by a control's state
|
||||
// Fields render in schema order; a `group` starts a labeled subsection
|
||||
// and consecutive same-group fields share it.
|
||||
let groupName = null;
|
||||
let container = form;
|
||||
for (const [key, spec] of schemaKeys) {
|
||||
const { field, read } = fieldFor(key, spec, mod.config[key], mod.passwordSet[key], mod.id);
|
||||
const g = spec?.group || '';
|
||||
if (g !== groupName) {
|
||||
groupName = g;
|
||||
if (g) {
|
||||
const group = document.createElement('div');
|
||||
group.className = 'field-group';
|
||||
const title = document.createElement('div');
|
||||
title.className = 'field-group-title';
|
||||
title.textContent = g;
|
||||
group.appendChild(title);
|
||||
form.appendChild(group);
|
||||
container = group;
|
||||
} else {
|
||||
container = form;
|
||||
}
|
||||
}
|
||||
const { field, read, control } = fieldFor(key, spec, mod.config[key], mod.passwordSet[key], mod.id);
|
||||
readers.set(key, read);
|
||||
form.appendChild(field);
|
||||
if (control) controls.set(key, control);
|
||||
if (spec?.showWhen) dependents.push({ field, when: spec.showWhen });
|
||||
container.appendChild(field);
|
||||
}
|
||||
// A field with showWhen:"K" is visible only while K's toggle is on.
|
||||
for (const dep of dependents) {
|
||||
const ctl = controls.get(dep.when);
|
||||
if (!ctl) continue;
|
||||
const sync = () => { dep.field.hidden = !ctl.checked; };
|
||||
ctl.addEventListener('change', sync);
|
||||
sync();
|
||||
}
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'module-actions';
|
||||
|
||||
Reference in New Issue
Block a user