ux: move timing into its own panel

This commit is contained in:
2026-07-16 17:07:46 +02:00
parent 4bb64a8ca3
commit 78979cfd17
6 changed files with 114 additions and 70 deletions
+4 -2
View File
@@ -77,8 +77,10 @@ edited from the dashboard and persists in NodeCG's database. Set
- **Ads** — CRUD for ads (strip text, column text, column image from the
Assets dialog, QR URL, enabled), with a live QR preview per row.
- **Choreography / OBS** — connection status, mode/phase badges, countdown to
the next automatic transition, timing configuration, break-slide content,
and manual controls: show ad now, skip ad, break toggle.
the next automatic transition, break-slide content, and manual controls:
show ad now, skip ad, break toggle.
- **Timing** — the choreography timing settings: ad interval, ad duration,
break gap, break ad duration, ads per burst.
- **OBS setup check** — read-only verification of the live OBS against the
expected scenes/sources/transforms/transitions, with concrete fixes.
+7
View File
@@ -95,6 +95,13 @@
"file": "choreo-panel.html",
"headerColor": "#8B2635"
},
{
"name": "timing-panel",
"title": "Timing",
"width": 3,
"file": "timing-panel.html",
"headerColor": "#8B2635"
},
{
"name": "obs-check-panel",
"title": "OBS setup check",
+1 -68
View File
@@ -4,12 +4,11 @@ import type NodeCG from "@nodecg/types";
import type {
AssetFile,
BreakSlide,
ChoreoConfig,
ObsStatus,
SchedulerState,
} from "../types";
import { t } from "./i18n";
import { parseNum, useReplicantForm, useReplicantValue } from "./lib";
import { useReplicantForm, useReplicantValue } from "./lib";
const obsStatusReplicant = nodecg.Replicant(
"obsStatus",
@@ -19,10 +18,6 @@ const schedulerStateReplicant = nodecg.Replicant(
"schedulerState",
) as unknown as NodeCG.ServerReplicant<SchedulerState>;
const choreoConfigReplicant = nodecg.Replicant(
"choreoConfig",
) as unknown as NodeCG.ServerReplicant<ChoreoConfig>;
const breakSlideReplicant = nodecg.Replicant(
"breakSlide",
) as unknown as NodeCG.ServerReplicant<BreakSlide>;
@@ -42,36 +37,9 @@ const formatCountdown = (ms: number) => {
return `${m}:${String(s).padStart(2, "0")}`;
};
function NumField({
label,
value,
onValid,
}: {
label: string;
value: number;
onValid: (n: number) => void;
}) {
return (
<label className="text-xs text-gray-600">
{label}
<input
className={inputCls}
type="number"
min="1"
value={value}
onChange={(e) => {
const n = parseNum(e.target.value);
if (n !== null && n > 0) onValid(n);
}}
/>
</label>
);
}
export function ChoreoPanel() {
const status = useReplicantValue<ObsStatus>(obsStatusReplicant);
const sched = useReplicantValue<SchedulerState>(schedulerStateReplicant);
const [cfg, updateCfg] = useReplicantForm<ChoreoConfig>(choreoConfigReplicant);
const [slide, updateSlide] = useReplicantForm<BreakSlide>(breakSlideReplicant);
const logoFiles = useReplicantValue<AssetFile[]>(logoAssetsReplicant);
@@ -243,41 +211,6 @@ export function ChoreoPanel() {
<div className="text-sm text-gray-500">{t.common.loading}</div>
)}
</section>
<section className="flex flex-col gap-2">
<h2 className={sectionTitleCls}>{t.choreo.timingSection}</h2>
{cfg ? (
<div className="grid grid-cols-2 gap-2">
<NumField
label={t.choreo.adIntervalLabel}
value={cfg.normalAdIntervalMinutes}
onValid={(n) => updateCfg({ normalAdIntervalMinutes: n })}
/>
<NumField
label={t.choreo.adDurationLabel}
value={cfg.adDurationSeconds}
onValid={(n) => updateCfg({ adDurationSeconds: n })}
/>
<NumField
label={t.choreo.breakGapLabel}
value={cfg.breakGapSeconds}
onValid={(n) => updateCfg({ breakGapSeconds: n })}
/>
<NumField
label={t.choreo.breakAdDurationLabel}
value={cfg.breakAdDurationSeconds}
onValid={(n) => updateCfg({ breakAdDurationSeconds: n })}
/>
<NumField
label={t.choreo.breakAdsPerBurstLabel}
value={cfg.breakAdsPerBurst}
onValid={(n) => updateCfg({ breakAdsPerBurst: n })}
/>
</div>
) : (
<div className="text-sm text-gray-500">{t.common.loading}</div>
)}
</section>
</div>
);
}
+82
View File
@@ -0,0 +1,82 @@
import React from "react";
import type NodeCG from "@nodecg/types";
import type { ChoreoConfig } from "../types";
import { t } from "./i18n";
import { parseNum, useReplicantForm } from "./lib";
const choreoConfigReplicant = nodecg.Replicant(
"choreoConfig",
) as unknown as NodeCG.ServerReplicant<ChoreoConfig>;
const inputCls =
"w-full rounded border border-gray-300 px-2 py-1 text-sm focus:border-blue-500 focus:outline-none";
function NumField({
label,
value,
onValid,
}: {
label: string;
value: number;
onValid: (n: number) => void;
}) {
return (
<label className="text-xs text-gray-600">
{label}
<input
className={inputCls}
type="number"
min="1"
value={value}
onChange={(e) => {
const n = parseNum(e.target.value);
if (n !== null && n > 0) onValid(n);
}}
/>
</label>
);
}
export function TimingPanel() {
const [cfg, updateCfg] = useReplicantForm<ChoreoConfig>(choreoConfigReplicant);
if (!cfg) {
return <div className="p-3 text-sm text-gray-500">{t.common.loading}</div>;
}
return (
<div className="flex flex-col gap-1 p-3">
<h2 className="text-xs font-bold uppercase tracking-wide text-gray-500">
{t.choreo.timingSection}
</h2>
<div className="grid grid-cols-2 gap-2">
<NumField
label={t.choreo.adIntervalLabel}
value={cfg.normalAdIntervalMinutes}
onValid={(n) => updateCfg({ normalAdIntervalMinutes: n })}
/>
<NumField
label={t.choreo.adDurationLabel}
value={cfg.adDurationSeconds}
onValid={(n) => updateCfg({ adDurationSeconds: n })}
/>
<NumField
label={t.choreo.breakGapLabel}
value={cfg.breakGapSeconds}
onValid={(n) => updateCfg({ breakGapSeconds: n })}
/>
<NumField
label={t.choreo.breakAdDurationLabel}
value={cfg.breakAdDurationSeconds}
onValid={(n) => updateCfg({ breakAdDurationSeconds: n })}
/>
<NumField
label={t.choreo.breakAdsPerBurstLabel}
value={cfg.breakAdsPerBurst}
onValid={(n) => updateCfg({ breakAdsPerBurst: n })}
/>
</div>
</div>
);
}
+9
View File
@@ -0,0 +1,9 @@
import React from "react";
import { createRoot } from "react-dom/client";
import { TimingPanel } from "./TimingPanel";
import { locale } from "./i18n";
document.documentElement.lang = locale;
const root = createRoot(document.getElementById("root")!);
root.render(<TimingPanel />);
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link href="./panel.css" type="text/css" rel="stylesheet" />
</head>
<body>
<div id="root"></div>
<script type="module" src="./timing-panel-bootstrap.tsx"></script>
</body>
</html>