141 lines
3.9 KiB
TypeScript
141 lines
3.9 KiB
TypeScript
import type NodeCG from "@nodecg/types";
|
|
import type {
|
|
Ad,
|
|
AdState,
|
|
BreakSlide,
|
|
BundleConfig,
|
|
ChoreoConfig,
|
|
ObsStatus,
|
|
SchedulerState,
|
|
TickerConfig,
|
|
TickerMessage,
|
|
} from "../types";
|
|
import { getStrings } from "../i18n";
|
|
|
|
export interface Replicants {
|
|
tickerMessages: NodeCG.ServerReplicant<TickerMessage[]>;
|
|
tickerConfig: NodeCG.ServerReplicant<TickerConfig>;
|
|
ads: NodeCG.ServerReplicant<Ad[]>;
|
|
adState: NodeCG.ServerReplicant<AdState>;
|
|
choreoConfig: NodeCG.ServerReplicant<ChoreoConfig>;
|
|
schedulerState: NodeCG.ServerReplicant<SchedulerState>;
|
|
breakSlide: NodeCG.ServerReplicant<BreakSlide>;
|
|
obsStatus: NodeCG.ServerReplicant<ObsStatus>;
|
|
}
|
|
|
|
export function declareReplicants(nodecg: NodeCG.ServerAPI): Replicants {
|
|
// First-run seeds follow the configured locale, and `branding` in the
|
|
// bundle config can pre-fill the identity-flavored ones.
|
|
const config = nodecg.bundleConfig as unknown as BundleConfig;
|
|
const seed = getStrings(config?.locale).seed;
|
|
const branding = config?.branding ?? {};
|
|
|
|
// Only fill in defaults when nothing is persisted, so operator edits
|
|
// survive NodeCG restarts.
|
|
const rep = <T>(name: string, defaultValue: T): NodeCG.ServerReplicant<T> => {
|
|
const r = nodecg.Replicant(name) as unknown as NodeCG.ServerReplicant<T>;
|
|
if (r.value === undefined) r.value = defaultValue;
|
|
return r;
|
|
};
|
|
|
|
const ads = rep<Ad[]>("ads", [
|
|
{
|
|
id: "sample-qr",
|
|
rowText: seed.sampleAdRow,
|
|
columnText: seed.sampleAdColumn,
|
|
columnImageUrl: null,
|
|
qrUrl: branding.sampleQrUrl ?? "https://example.com",
|
|
enabled: true,
|
|
},
|
|
]);
|
|
|
|
// Migrate ads persisted by older versions: the { text } shape from before
|
|
// the rowText/columnText split, and rows without columnImageUrl.
|
|
if (
|
|
ads.value?.some(
|
|
(a) =>
|
|
(a as any).rowText === undefined ||
|
|
(a as any).columnImageUrl === undefined,
|
|
)
|
|
) {
|
|
ads.value = ads.value.map((a) => {
|
|
const legacy = a as any;
|
|
return {
|
|
id: a.id,
|
|
rowText:
|
|
legacy.rowText ??
|
|
(typeof legacy.text === "string" ? legacy.text : ""),
|
|
columnText: legacy.columnText ?? "",
|
|
columnImageUrl: legacy.columnImageUrl ?? null,
|
|
qrUrl: a.qrUrl,
|
|
enabled: a.enabled,
|
|
};
|
|
});
|
|
}
|
|
|
|
const slideDefaults: BreakSlide = {
|
|
heading: branding.breakHeading ?? seed.breakHeading,
|
|
nextActName: "",
|
|
nextActFrom: "",
|
|
logoVisible: true,
|
|
logoInverted: false,
|
|
logoName: null,
|
|
logoHeightPx: 240,
|
|
};
|
|
const breakSlide = rep<BreakSlide>("breakSlide", slideDefaults);
|
|
|
|
// Migrate slides persisted before the logo controls existed: fill in any
|
|
// missing fields from the defaults, keeping whatever the operator set.
|
|
if (
|
|
breakSlide.value &&
|
|
Object.keys(slideDefaults).some(
|
|
(k) => (breakSlide.value as any)[k] === undefined,
|
|
)
|
|
) {
|
|
breakSlide.value = { ...slideDefaults, ...breakSlide.value };
|
|
}
|
|
|
|
return {
|
|
ads,
|
|
breakSlide,
|
|
tickerMessages: rep<TickerMessage[]>("tickerMessages", []),
|
|
|
|
tickerConfig: rep<TickerConfig>("tickerConfig", {
|
|
loopBlurb: branding.tickerBlurb ?? seed.loopBlurb,
|
|
separator: "♦",
|
|
defaultTtlSeconds: 300,
|
|
speedPxPerSec: 120,
|
|
}),
|
|
|
|
adState: rep<AdState>("adState", {
|
|
visible: false,
|
|
corner: "tr",
|
|
adId: null,
|
|
}),
|
|
|
|
choreoConfig: rep<ChoreoConfig>("choreoConfig", {
|
|
normalAdIntervalMinutes: 15,
|
|
adDurationSeconds: 20,
|
|
breakGapSeconds: 45,
|
|
breakAdDurationSeconds: 15,
|
|
breakAdsPerBurst: 2,
|
|
}),
|
|
|
|
schedulerState: rep<SchedulerState>("schedulerState", {
|
|
mode: "normal",
|
|
phase: "live",
|
|
lastCorner: "bl",
|
|
lastAdIndex: -1,
|
|
burstRemaining: 0,
|
|
nextTransitionAt: null,
|
|
}),
|
|
|
|
obsStatus: rep<ObsStatus>("obsStatus", {
|
|
connected: false,
|
|
currentScene: null,
|
|
lastError: null,
|
|
attempts: 0,
|
|
}),
|
|
};
|
|
}
|