Performance and layout enhancements
This commit is contained in:
+116
-51
@@ -1,4 +1,4 @@
|
||||
import { startTransition, useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
|
||||
import { startTransition, useDeferredValue, useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
|
||||
import {
|
||||
armCue,
|
||||
createCueRuntimeState,
|
||||
@@ -37,7 +37,9 @@ import {
|
||||
deleteCue,
|
||||
fireCue,
|
||||
generateCue,
|
||||
loadState,
|
||||
loadAdminBootstrap,
|
||||
loadAdminLibrary,
|
||||
loadAdminLive,
|
||||
moderateAsset,
|
||||
moveCue,
|
||||
rescanLibrary,
|
||||
@@ -46,6 +48,7 @@ import {
|
||||
} from "../features/live/api";
|
||||
import {
|
||||
createPresentationHash,
|
||||
createPresentationStructureHash,
|
||||
writeProgramOutputState,
|
||||
type ProgramOutputState
|
||||
} from "../features/live/output-sync";
|
||||
@@ -199,6 +202,19 @@ const matchPresetForScene = (
|
||||
const getApprovedAssets = (payload: RepositoryState) =>
|
||||
payload.photoAssets.filter((asset) => asset.moderationStatus === "approved");
|
||||
|
||||
const getPendingModerationAssets = (photoAssets: PhotoAsset[], submissions: Submission[]) =>
|
||||
photoAssets.filter((asset) => {
|
||||
if (asset.moderationStatus !== "pending") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const submission = submissions.find((entry) => entry.id === asset.submissionId);
|
||||
return submission?.source !== "admin_upload";
|
||||
});
|
||||
|
||||
const getPendingModerationCount = (photoAssets: PhotoAsset[], submissions: Submission[]) =>
|
||||
getPendingModerationAssets(photoAssets, submissions).length;
|
||||
|
||||
const filterAvailableAssetIds = (payload: RepositoryState, assetIds: string[]) => {
|
||||
const available = new Set(getApprovedAssets(payload).map((asset) => asset.id));
|
||||
return assetIds.filter((assetId) => available.has(assetId));
|
||||
@@ -491,12 +507,15 @@ export const App = () => {
|
||||
const [activePresetId, setActivePresetId] = useState(effectPresetLibrary[0]?.id ?? "");
|
||||
const [cueDraft, setCueDraft] = useState<CueDraftState>(createCueDraft());
|
||||
const [mediaSearch, setMediaSearch] = useState("");
|
||||
const deferredMediaSearch = useDeferredValue(mediaSearch);
|
||||
const [uploadName, setUploadName] = useState("");
|
||||
const [uploadCaption, setUploadCaption] = useState("");
|
||||
const [uploadPromptAnswer, setUploadPromptAnswer] = useState("");
|
||||
const [uploadFile, setUploadFile] = useState<File | null>(null);
|
||||
const [uploadAddToSelection, setUploadAddToSelection] = useState(true);
|
||||
const [status, setStatus] = useState("Connecting to local show state...");
|
||||
const [pendingCount, setPendingCount] = useState(0);
|
||||
const [approvedCount, setApprovedCount] = useState(0);
|
||||
const uploadInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const mediaSearchInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const metadataHydrationKeyRef = useRef<string | null>(null);
|
||||
@@ -516,11 +535,13 @@ export const App = () => {
|
||||
};
|
||||
|
||||
const hydrate = (payload: RepositoryState, initialize: boolean) => {
|
||||
const pendingCount = payload.photoAssets.filter((asset) => asset.moderationStatus === "pending").length;
|
||||
const nextPendingCount = getPendingModerationCount(payload.photoAssets, payload.submissions);
|
||||
const nextApprovedCount = getApprovedAssets(payload).length;
|
||||
|
||||
startTransition(() => {
|
||||
setState(payload);
|
||||
setStatus(`Ready. ${pendingCount} pending / ${getApprovedAssets(payload).length} approved.`);
|
||||
setPendingCount(nextPendingCount);
|
||||
setApprovedCount(nextApprovedCount);
|
||||
|
||||
if (initialize) {
|
||||
const initial = createInitialLiveState(payload);
|
||||
@@ -535,6 +556,7 @@ export const App = () => {
|
||||
setActivePresetId(initial.activePresetId);
|
||||
setCueDraft(initial.cueDraft);
|
||||
publishProgramOutput(initial.programPresentation, false, initial.programTransition);
|
||||
setStatus("Ready. Local show state loaded.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -542,20 +564,59 @@ export const App = () => {
|
||||
...current,
|
||||
cueStack: payload.cues
|
||||
}));
|
||||
setSelectedAssetIds((current) => {
|
||||
const filtered = filterAvailableAssetIds(payload, current);
|
||||
return filtered.length > 0 ? filtered : getDefaultAssetIds(payload);
|
||||
});
|
||||
setSelectedAssetIds((current) => filterAvailableAssetIds(payload, current));
|
||||
});
|
||||
};
|
||||
|
||||
const refresh = async (initialize = false) => {
|
||||
const payload = await loadState();
|
||||
const refreshBootstrap = async (initialize = false) => {
|
||||
const payload = await loadAdminBootstrap();
|
||||
hydrate(payload, initialize);
|
||||
};
|
||||
|
||||
const refreshLiveState = async () => {
|
||||
const payload = await loadAdminLive();
|
||||
startTransition(() => {
|
||||
setPendingCount(payload.pendingCount);
|
||||
setApprovedCount(payload.approvedCount);
|
||||
setState((current) => (current ? { ...current, cues: payload.cues } : current));
|
||||
setCueState((current) => ({
|
||||
...current,
|
||||
cueStack: payload.cues
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
const refreshLibraryState = async () => {
|
||||
const payload = await loadAdminLibrary();
|
||||
const nextPendingCount = getPendingModerationCount(payload.photoAssets, payload.submissions);
|
||||
const nextApprovedCount = payload.photoAssets.filter((asset) => asset.moderationStatus === "approved").length;
|
||||
const availableIds = new Set(
|
||||
payload.photoAssets
|
||||
.filter((asset) => asset.moderationStatus === "approved")
|
||||
.map((asset) => asset.id)
|
||||
);
|
||||
const knownIds = new Set(payload.photoAssets.map((asset) => asset.id));
|
||||
|
||||
startTransition(() => {
|
||||
setPendingCount(nextPendingCount);
|
||||
setApprovedCount(nextApprovedCount);
|
||||
setState((current) =>
|
||||
current
|
||||
? {
|
||||
...current,
|
||||
photoAssets: payload.photoAssets,
|
||||
submissions: payload.submissions,
|
||||
collections: payload.collections
|
||||
}
|
||||
: current
|
||||
);
|
||||
setSelectedAssetIds((current) => current.filter((assetId) => availableIds.has(assetId)));
|
||||
setMetadataAssetId((current) => (current && knownIds.has(current) ? current : null));
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
void refresh(true).catch((error) => {
|
||||
void refreshBootstrap(true).catch((error) => {
|
||||
setStatus(error instanceof Error ? error.message : "Could not load state.");
|
||||
});
|
||||
}, []);
|
||||
@@ -566,34 +627,31 @@ export const App = () => {
|
||||
}
|
||||
|
||||
const interval = window.setInterval(() => {
|
||||
void refresh(false).catch(() => {
|
||||
void refreshLiveState().catch(() => {
|
||||
setStatus("Refresh failed. Local state may be stale.");
|
||||
});
|
||||
if (workspaceMode === "build" || showUtilityTab === "media" || showUtilityTab === "moderation") {
|
||||
void refreshLibraryState().catch(() => {
|
||||
setStatus("Library refresh failed. Media state may be stale.");
|
||||
});
|
||||
}
|
||||
}, 4000);
|
||||
|
||||
return () => window.clearInterval(interval);
|
||||
}, [state]);
|
||||
}, [showUtilityTab, state, workspaceMode]);
|
||||
|
||||
const pendingAssets = useMemo(
|
||||
() =>
|
||||
state?.photoAssets.filter((asset) => {
|
||||
if (asset.moderationStatus !== "pending") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const submission = state.submissions.find((entry) => entry.id === asset.submissionId);
|
||||
return submission?.source !== "admin_upload";
|
||||
}) ?? [],
|
||||
[state]
|
||||
() => (state ? getPendingModerationAssets(state.photoAssets, state.submissions) : []),
|
||||
[state?.photoAssets, state?.submissions]
|
||||
);
|
||||
const approvedAssets = useMemo(() => (state ? getApprovedAssets(state) : []), [state]);
|
||||
const approvedAssets = useMemo(() => (state ? getApprovedAssets(state) : []), [state?.photoAssets]);
|
||||
const availablePresets = useMemo(
|
||||
() => (state && state.effectPresets.length > 0 ? state.effectPresets : effectPresetLibrary),
|
||||
[state]
|
||||
[state?.effectPresets]
|
||||
);
|
||||
const selectedScene = useMemo(
|
||||
() => (state ? findSceneById(state, selectedSceneId) : undefined),
|
||||
[selectedSceneId, state]
|
||||
[selectedSceneId, state?.scenes]
|
||||
);
|
||||
const selectedScenePresets = useMemo(
|
||||
() => getScenePresets(selectedScene, availablePresets),
|
||||
@@ -604,7 +662,7 @@ export const App = () => {
|
||||
(state?.scenes ?? []).filter(
|
||||
(scene) => sceneBrowserFilter === "all" || scene.sceneFamily === sceneBrowserFilter
|
||||
),
|
||||
[sceneBrowserFilter, state]
|
||||
[sceneBrowserFilter, state?.scenes]
|
||||
);
|
||||
const sceneFamilyCounts = useMemo(() => {
|
||||
const counts: Record<SceneBrowserFilter, number> = {
|
||||
@@ -620,31 +678,31 @@ export const App = () => {
|
||||
}
|
||||
|
||||
return counts;
|
||||
}, [state]);
|
||||
}, [state?.scenes]);
|
||||
const previewCue = useMemo(
|
||||
() => (state ? findCueById(state, cueState.previewCueId) : undefined),
|
||||
[cueState.previewCueId, state]
|
||||
[cueState.previewCueId, state?.cues]
|
||||
);
|
||||
const currentCue = useMemo(
|
||||
() => (state ? findCueById(state, cueState.currentCueId) : undefined),
|
||||
[cueState.currentCueId, state]
|
||||
[cueState.currentCueId, state?.cues]
|
||||
);
|
||||
const safeCue = useMemo(
|
||||
() => state?.cues.find((cue) => cue.id === state.showConfig.safeSceneCueId),
|
||||
[state]
|
||||
[state?.cues, state?.showConfig.safeSceneCueId]
|
||||
);
|
||||
const cueStack = state?.cues ?? [];
|
||||
const favoriteCollection: Collection | undefined = useMemo(
|
||||
() => state?.collections.find((collection) => collection.kind === "favorites"),
|
||||
[state]
|
||||
[state?.collections]
|
||||
);
|
||||
const curatedCollection: Collection | undefined = useMemo(
|
||||
() => state?.collections.find((collection) => collection.id === "collection-curated-library"),
|
||||
[state]
|
||||
[state?.collections]
|
||||
);
|
||||
const submissionMap = useMemo(
|
||||
() => new Map((state?.submissions ?? []).map((submission) => [submission.id, submission] as const)),
|
||||
[state]
|
||||
[state?.submissions]
|
||||
);
|
||||
const selectedAssets = useMemo(() => {
|
||||
const assetMap = new Map(approvedAssets.map((asset) => [asset.id, asset] as const));
|
||||
@@ -670,21 +728,21 @@ export const App = () => {
|
||||
[metadataAsset, submissionMap]
|
||||
);
|
||||
const filteredPendingAssets = useMemo(() => {
|
||||
const query = mediaSearch.trim().toLowerCase();
|
||||
const query = deferredMediaSearch.trim().toLowerCase();
|
||||
if (!query) {
|
||||
return pendingAssets;
|
||||
}
|
||||
|
||||
return pendingAssets.filter((asset) => getAssetSearchText(asset, submissionMap.get(asset.submissionId)).includes(query));
|
||||
}, [mediaSearch, pendingAssets, submissionMap]);
|
||||
}, [deferredMediaSearch, pendingAssets, submissionMap]);
|
||||
const filteredApprovedAssets = useMemo(() => {
|
||||
const query = mediaSearch.trim().toLowerCase();
|
||||
const query = deferredMediaSearch.trim().toLowerCase();
|
||||
if (!query) {
|
||||
return approvedAssets;
|
||||
}
|
||||
|
||||
return approvedAssets.filter((asset) => getAssetSearchText(asset, submissionMap.get(asset.submissionId)).includes(query));
|
||||
}, [approvedAssets, mediaSearch, submissionMap]);
|
||||
}, [approvedAssets, deferredMediaSearch, submissionMap]);
|
||||
const activePreset: EffectPreset | undefined =
|
||||
selectedScenePresets.find((preset) => preset.id === activePresetId) ??
|
||||
availablePresets.find((preset) => preset.id === activePresetId);
|
||||
@@ -774,11 +832,11 @@ export const App = () => {
|
||||
}, [activePreset?.id, activePreset?.modeKey, previewCue, previewLabel, previewParams, previewUsesArmedCue, selectedAssets, selectedScene, state]);
|
||||
|
||||
const previewActivationKey = useMemo(
|
||||
() => createPresentationHash(previewPresentation, false, null),
|
||||
() => createPresentationStructureHash(previewPresentation),
|
||||
[previewPresentation]
|
||||
);
|
||||
const programPresentation = programOutputState?.presentation ?? null;
|
||||
const programActivationKey = programOutputState?.presentationHash ?? "program-empty";
|
||||
const programActivationKey = `${programOutputState?.outputRevision ?? 0}:${createPresentationStructureHash(programPresentation)}`;
|
||||
|
||||
useEffect(() => {
|
||||
if (metadataAsset?.id && metadataAssetId !== metadataAsset.id) {
|
||||
@@ -902,7 +960,7 @@ export const App = () => {
|
||||
const handleRescanLibrary = async () => {
|
||||
try {
|
||||
const payload = await rescanLibrary();
|
||||
hydrate(payload, true);
|
||||
hydrate(payload, false);
|
||||
setStatus(
|
||||
`Library rescanned. ${getApprovedAssets(payload).length} approved assets ready / ${payload.collections.find((collection) => collection.id === "collection-curated-library")?.assetIds.length ?? 0} curated.`
|
||||
);
|
||||
@@ -945,7 +1003,7 @@ export const App = () => {
|
||||
|
||||
try {
|
||||
const result = await createAdminUpload(form);
|
||||
await refresh(false);
|
||||
await refreshBootstrap(false);
|
||||
setMetadataAssetId(result.assetId);
|
||||
setMetadataDirty(false);
|
||||
|
||||
@@ -1001,7 +1059,7 @@ export const App = () => {
|
||||
setMetadataSaving(true);
|
||||
await updateSubmissionMetadata(metadataSubmission.id, payload);
|
||||
setMetadataDirty(false);
|
||||
await refresh(false);
|
||||
await refreshBootstrap(false);
|
||||
setStatus(`Metadata saved for ${metadataTitle}.`);
|
||||
} catch (error) {
|
||||
setStatus(error instanceof Error ? error.message : "Could not save image metadata.");
|
||||
@@ -1033,7 +1091,7 @@ export const App = () => {
|
||||
}
|
||||
setSelectedAssetIds((current) => current.filter((assetId) => assetId !== asset.id));
|
||||
setMetadataAssetId((current) => (current === asset.id ? null : current));
|
||||
await refresh(false);
|
||||
await refreshBootstrap(false);
|
||||
setStatus(
|
||||
submission?.source === "library_import"
|
||||
? `Removed ${assetLabel} from the approved bank.`
|
||||
@@ -1184,7 +1242,7 @@ export const App = () => {
|
||||
|
||||
const savedCue = cueDraft.id ? await updateCue(cueId, payload) : await createCue(payload);
|
||||
syncPreviewFromCue(savedCue);
|
||||
await refresh(false);
|
||||
await refreshBootstrap(false);
|
||||
setCueDraft(createCueDraft(savedCue, selectedScene));
|
||||
setStatus(cueDraft.id ? `Cue updated: ${savedCue.notes ?? savedCue.id}` : `Cue created: ${savedCue.notes ?? savedCue.id}`);
|
||||
};
|
||||
@@ -1205,7 +1263,7 @@ export const App = () => {
|
||||
|
||||
const createdCue = await createCue(payload);
|
||||
syncPreviewFromCue(createdCue);
|
||||
await refresh(false);
|
||||
await refreshBootstrap(false);
|
||||
setCueDraft(createCueDraft(createdCue, selectedScene));
|
||||
setStatus(`Cue inserted: ${createdCue.notes ?? createdCue.id}`);
|
||||
};
|
||||
@@ -1224,7 +1282,7 @@ export const App = () => {
|
||||
payload.notes = `${cueDraft.notes || selectedScene?.name || "Cue"} copy`;
|
||||
const duplicatedCue = await createCue(payload);
|
||||
syncPreviewFromCue(duplicatedCue);
|
||||
await refresh(false);
|
||||
await refreshBootstrap(false);
|
||||
setCueDraft(createCueDraft(duplicatedCue, selectedScene));
|
||||
setStatus(`Cue duplicated: ${duplicatedCue.notes ?? duplicatedCue.id}`);
|
||||
};
|
||||
@@ -1235,7 +1293,7 @@ export const App = () => {
|
||||
}
|
||||
|
||||
await deleteCue(cueDraft.id);
|
||||
await refresh(false);
|
||||
await refreshBootstrap(false);
|
||||
setCueDraft(createCueDraft(undefined, selectedScene));
|
||||
setStatus("Cue deleted.");
|
||||
};
|
||||
@@ -1246,7 +1304,7 @@ export const App = () => {
|
||||
}
|
||||
|
||||
await moveCue(cueDraft.id, { direction });
|
||||
await refresh(false);
|
||||
await refreshBootstrap(false);
|
||||
setStatus(`Cue moved ${direction}.`);
|
||||
};
|
||||
|
||||
@@ -1255,7 +1313,7 @@ export const App = () => {
|
||||
decision,
|
||||
reasonCode: decision === "rejected" ? "operator_review" : undefined
|
||||
});
|
||||
await refresh(false);
|
||||
await refreshBootstrap(false);
|
||||
};
|
||||
|
||||
const handleTakeCue = async () => {
|
||||
@@ -2251,7 +2309,12 @@ export const App = () => {
|
||||
{selectedAssetCountLabel} / {selectedScene?.renderMode ?? "none"}
|
||||
</span>
|
||||
</div>
|
||||
<SceneViewport presentation={previewPresentation} activationKey={previewActivationKey} />
|
||||
<SceneViewport
|
||||
presentation={previewPresentation}
|
||||
activationKey={previewActivationKey}
|
||||
qualityProfile="preview"
|
||||
busy={workspaceMode === "build"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="surface-card surface-card--program">
|
||||
@@ -2269,6 +2332,7 @@ export const App = () => {
|
||||
blackout={cueState.blackout}
|
||||
transition={programOutputState?.transition ?? null}
|
||||
activationKey={`${programActivationKey}:${cueState.blackout ? "blackout" : "live"}`}
|
||||
qualityProfile="program"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2290,6 +2354,7 @@ export const App = () => {
|
||||
</div>
|
||||
<div className="admin-status">
|
||||
<span className="admin-topbar__status-text">{status}</span>
|
||||
<span className="source-badge">{pendingCount} pending / {approvedCount} approved</span>
|
||||
<div className="workspace-toggle" role="tablist" aria-label="Workspace mode">
|
||||
<button
|
||||
className={workspaceMode === "show" ? "workspace-toggle__button workspace-toggle__button--active" : "workspace-toggle__button"}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import type { RenderSurface as RenderSurfaceType, SurfacePresentation } from "@goodgrief/render-engine";
|
||||
import type {
|
||||
RenderSurface as RenderSurfaceType,
|
||||
SurfacePresentation,
|
||||
SurfaceQualityProfile
|
||||
} from "@goodgrief/render-engine";
|
||||
import type { CueTransition } from "@goodgrief/shared-types";
|
||||
import "./viewport.css";
|
||||
|
||||
@@ -8,6 +12,8 @@ interface SceneViewportProps {
|
||||
blackout?: boolean;
|
||||
transition?: CueTransition | null;
|
||||
activationKey?: string;
|
||||
qualityProfile?: SurfaceQualityProfile;
|
||||
busy?: boolean;
|
||||
}
|
||||
|
||||
const defaultTransition: CueTransition = {
|
||||
@@ -15,7 +21,14 @@ const defaultTransition: CueTransition = {
|
||||
durationMs: 0
|
||||
};
|
||||
|
||||
export const SceneViewport = ({ presentation, blackout = false, transition, activationKey }: SceneViewportProps) => {
|
||||
export const SceneViewport = ({
|
||||
presentation,
|
||||
blackout = false,
|
||||
transition,
|
||||
activationKey,
|
||||
qualityProfile = "program",
|
||||
busy = false
|
||||
}: SceneViewportProps) => {
|
||||
const frameRef = useRef<HTMLDivElement | null>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const surfaceRef = useRef<RenderSurfaceType | null>(null);
|
||||
@@ -23,11 +36,15 @@ export const SceneViewport = ({ presentation, blackout = false, transition, acti
|
||||
const activationRef = useRef<string | undefined>(activationKey);
|
||||
const transitionRef = useRef<CueTransition | null | undefined>(transition);
|
||||
const blackoutRef = useRef(blackout);
|
||||
const qualityProfileRef = useRef<SurfaceQualityProfile>(qualityProfile);
|
||||
const busyRef = useRef(busy);
|
||||
|
||||
presentationRef.current = presentation;
|
||||
activationRef.current = activationKey;
|
||||
transitionRef.current = transition;
|
||||
blackoutRef.current = blackout;
|
||||
qualityProfileRef.current = qualityProfile;
|
||||
busyRef.current = busy;
|
||||
|
||||
useEffect(() => {
|
||||
const frame = frameRef.current;
|
||||
@@ -46,6 +63,8 @@ export const SceneViewport = ({ presentation, blackout = false, transition, acti
|
||||
|
||||
const surface = new RenderSurface(canvas);
|
||||
surface.registerMany(defaultScenePlugins);
|
||||
surface.setQualityProfile(qualityProfileRef.current);
|
||||
surface.setBusy(busyRef.current);
|
||||
surface.setBlackout(blackoutRef.current);
|
||||
surfaceRef.current = surface;
|
||||
|
||||
@@ -60,7 +79,7 @@ export const SceneViewport = ({ presentation, blackout = false, transition, acti
|
||||
|
||||
const initialPresentation = presentationRef.current;
|
||||
if (initialPresentation) {
|
||||
void surface.activate(initialPresentation, defaultTransition);
|
||||
void surface.activate(initialPresentation, defaultTransition, activationRef.current);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -76,15 +95,27 @@ export const SceneViewport = ({ presentation, blackout = false, transition, acti
|
||||
surfaceRef.current?.setBlackout(blackout);
|
||||
}, [blackout]);
|
||||
|
||||
useEffect(() => {
|
||||
surfaceRef.current?.setQualityProfile(qualityProfile);
|
||||
}, [qualityProfile]);
|
||||
|
||||
useEffect(() => {
|
||||
surfaceRef.current?.setBusy(busy);
|
||||
}, [busy]);
|
||||
|
||||
useEffect(() => {
|
||||
const surface = surfaceRef.current;
|
||||
if (!surface) {
|
||||
return;
|
||||
}
|
||||
|
||||
void surface.activate(presentationRef.current, transitionRef.current ?? defaultTransition);
|
||||
void surface.activate(presentationRef.current, transitionRef.current ?? defaultTransition, activationRef.current);
|
||||
}, [activationKey]);
|
||||
|
||||
useEffect(() => {
|
||||
surfaceRef.current?.updatePresentation(presentation, activationKey);
|
||||
}, [activationKey, presentation]);
|
||||
|
||||
return (
|
||||
<div ref={frameRef} className="surface-viewport">
|
||||
<canvas ref={canvasRef} className="surface-viewport__canvas" />
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
import type {
|
||||
Collection,
|
||||
Cue,
|
||||
CueGeneratePayload,
|
||||
CueMovePayload,
|
||||
CueUpsertPayload,
|
||||
ModerationActionPayload,
|
||||
PhotoAsset,
|
||||
RepositoryState,
|
||||
Submission,
|
||||
SubmissionUpdatePayload
|
||||
} from "@goodgrief/shared-types";
|
||||
|
||||
export interface AdminLivePayload {
|
||||
cues: Cue[];
|
||||
pendingCount: number;
|
||||
approvedCount: number;
|
||||
}
|
||||
|
||||
export interface AdminLibraryPayload {
|
||||
photoAssets: PhotoAsset[];
|
||||
submissions: Submission[];
|
||||
collections: Collection[];
|
||||
}
|
||||
|
||||
const postVoid = async (url: string, body?: unknown) => {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
@@ -42,13 +56,14 @@ const requestJson = async <T>(url: string, init?: RequestInit) => {
|
||||
return (await response.json()) as T;
|
||||
};
|
||||
|
||||
export const loadState = async (): Promise<RepositoryState> => {
|
||||
const response = await fetch("/api/state");
|
||||
if (!response.ok) {
|
||||
throw new Error("Could not load admin state.");
|
||||
}
|
||||
return (await response.json()) as RepositoryState;
|
||||
};
|
||||
export const loadAdminBootstrap = async (): Promise<RepositoryState> =>
|
||||
requestJson<RepositoryState>("/api/admin/bootstrap");
|
||||
|
||||
export const loadAdminLive = async (): Promise<AdminLivePayload> =>
|
||||
requestJson<AdminLivePayload>("/api/admin/live");
|
||||
|
||||
export const loadAdminLibrary = async (): Promise<AdminLibraryPayload> =>
|
||||
requestJson<AdminLibraryPayload>("/api/admin/library");
|
||||
|
||||
export const rescanLibrary = async (): Promise<RepositoryState> =>
|
||||
requestJson<RepositoryState>("/api/library/rescan", {
|
||||
|
||||
@@ -13,6 +13,18 @@ export interface ProgramOutputState {
|
||||
|
||||
const storageKey = "goodgrief:program-output";
|
||||
const channelName = "goodgrief-program-output";
|
||||
const liveMutableParamPaths = new Set([
|
||||
"composition.motion",
|
||||
"composition.cameraTravel",
|
||||
"composition.orbitAmount",
|
||||
"scenicTreatment.fieldType",
|
||||
"scenicTreatment.fieldIntensity",
|
||||
"scenicTreatment.fieldScale",
|
||||
"scenicTreatment.fieldSpeed",
|
||||
"scenicTreatment.hue",
|
||||
"scenicTreatment.saturation",
|
||||
"scenicTreatment.lightness"
|
||||
]);
|
||||
|
||||
const canUseWindow = () => typeof window !== "undefined";
|
||||
|
||||
@@ -28,6 +40,36 @@ const hashString = (input: string) => {
|
||||
return (hash >>> 0).toString(16);
|
||||
};
|
||||
|
||||
const getStructuralParams = (presentation: SurfacePresentation | null) => {
|
||||
if (!presentation?.params) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const flattened = flattenSceneParams(presentation.params);
|
||||
for (const path of liveMutableParamPaths) {
|
||||
delete flattened[path];
|
||||
}
|
||||
|
||||
return flattened;
|
||||
};
|
||||
|
||||
export const createPresentationStructureHash = (presentation: SurfacePresentation | null) =>
|
||||
hashString(
|
||||
JSON.stringify({
|
||||
presentation: presentation
|
||||
? {
|
||||
definitionId: presentation.definition.id,
|
||||
effectPresetId: presentation.effectPresetId ?? null,
|
||||
modeKey: presentation.modeKey ?? null,
|
||||
assetIds: presentation.assets.map((asset) => asset.id),
|
||||
textFragments: presentation.textFragments ?? [],
|
||||
anchorCaption: presentation.anchorCaption ?? null,
|
||||
params: getStructuralParams(presentation)
|
||||
}
|
||||
: null
|
||||
})
|
||||
);
|
||||
|
||||
export const createPresentationHash = (
|
||||
presentation: SurfacePresentation | null,
|
||||
blackout: boolean,
|
||||
|
||||
Reference in New Issue
Block a user