From 78979cfd17c28c24730322cff0866ea4c8e52a5e Mon Sep 17 00:00:00 2001 From: dvdrw Date: Thu, 16 Jul 2026 17:07:46 +0200 Subject: [PATCH] ux: move timing into its own panel --- README.md | 6 +- package.json | 7 ++ src/dashboard/ChoreoPanel.tsx | 69 +------------------- src/dashboard/TimingPanel.tsx | 82 ++++++++++++++++++++++++ src/dashboard/timing-panel-bootstrap.tsx | 9 +++ src/dashboard/timing-panel.html | 11 ++++ 6 files changed, 114 insertions(+), 70 deletions(-) create mode 100644 src/dashboard/TimingPanel.tsx create mode 100644 src/dashboard/timing-panel-bootstrap.tsx create mode 100644 src/dashboard/timing-panel.html diff --git a/README.md b/README.md index d5d9ad5..e9fc586 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/package.json b/package.json index 2222aad..fd57ee0 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/dashboard/ChoreoPanel.tsx b/src/dashboard/ChoreoPanel.tsx index b9bb49c..adc2bbc 100644 --- a/src/dashboard/ChoreoPanel.tsx +++ b/src/dashboard/ChoreoPanel.tsx @@ -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; -const choreoConfigReplicant = nodecg.Replicant( - "choreoConfig", -) as unknown as NodeCG.ServerReplicant; - const breakSlideReplicant = nodecg.Replicant( "breakSlide", ) as unknown as NodeCG.ServerReplicant; @@ -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 ( - - ); -} - export function ChoreoPanel() { const status = useReplicantValue(obsStatusReplicant); const sched = useReplicantValue(schedulerStateReplicant); - const [cfg, updateCfg] = useReplicantForm(choreoConfigReplicant); const [slide, updateSlide] = useReplicantForm(breakSlideReplicant); const logoFiles = useReplicantValue(logoAssetsReplicant); @@ -243,41 +211,6 @@ export function ChoreoPanel() {
{t.common.loading}
)} - -
-

{t.choreo.timingSection}

- {cfg ? ( -
- updateCfg({ normalAdIntervalMinutes: n })} - /> - updateCfg({ adDurationSeconds: n })} - /> - updateCfg({ breakGapSeconds: n })} - /> - updateCfg({ breakAdDurationSeconds: n })} - /> - updateCfg({ breakAdsPerBurst: n })} - /> -
- ) : ( -
{t.common.loading}
- )} -
); } diff --git a/src/dashboard/TimingPanel.tsx b/src/dashboard/TimingPanel.tsx new file mode 100644 index 0000000..22b5db2 --- /dev/null +++ b/src/dashboard/TimingPanel.tsx @@ -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; + +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 ( + + ); +} + +export function TimingPanel() { + const [cfg, updateCfg] = useReplicantForm(choreoConfigReplicant); + + if (!cfg) { + return
{t.common.loading}
; + } + + return ( +
+

+ {t.choreo.timingSection} +

+
+ updateCfg({ normalAdIntervalMinutes: n })} + /> + updateCfg({ adDurationSeconds: n })} + /> + updateCfg({ breakGapSeconds: n })} + /> + updateCfg({ breakAdDurationSeconds: n })} + /> + updateCfg({ breakAdsPerBurst: n })} + /> +
+
+ ); +} diff --git a/src/dashboard/timing-panel-bootstrap.tsx b/src/dashboard/timing-panel-bootstrap.tsx new file mode 100644 index 0000000..fb421fc --- /dev/null +++ b/src/dashboard/timing-panel-bootstrap.tsx @@ -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(); diff --git a/src/dashboard/timing-panel.html b/src/dashboard/timing-panel.html new file mode 100644 index 0000000..6c639a8 --- /dev/null +++ b/src/dashboard/timing-panel.html @@ -0,0 +1,11 @@ + + + + + + + +
+ + +