From 4c6982bf684e7b60c3b9c4e2fdf0717f7b3cd0ca Mon Sep 17 00:00:00 2001 From: vance Date: Fri, 10 Apr 2026 16:40:42 -0700 Subject: [PATCH] Support scenic-only scenes and blackout fades --- apps/admin/src/app/App.tsx | 69 +++---- apps/admin/src/app/ProgramOutputApp.tsx | 9 +- .../admin/src/features/live/SceneViewport.tsx | 6 +- packages/render-engine/src/index.ts | 180 +++++++++++++----- packages/shared-types/src/mock.ts | 10 - packages/shared-types/src/scenes.ts | 14 +- services/api/src/state-store.ts | 18 +- 7 files changed, 195 insertions(+), 111 deletions(-) diff --git a/apps/admin/src/app/App.tsx b/apps/admin/src/app/App.tsx index 3045dc0..3ec19e7 100644 --- a/apps/admin/src/app/App.tsx +++ b/apps/admin/src/app/App.tsx @@ -235,9 +235,9 @@ const getAssetSecondaryLabel = (submission: Submission | undefined) => const getDefaultAssetIds = (payload: RepositoryState) => { const approvedIds = new Set(getApprovedAssets(payload).map((asset) => asset.id)); - const favorites = payload.collections.find((collection) => collection.kind === "favorites"); - const favoriteIds = favorites?.assetIds.filter((assetId) => approvedIds.has(assetId)) ?? []; - return (favoriteIds.length > 0 ? favoriteIds : Array.from(approvedIds)).slice(0, 12); + const curated = payload.collections.find((collection) => collection.id === "collection-curated-library"); + const curatedIds = curated?.assetIds.filter((assetId) => approvedIds.has(assetId)) ?? []; + return (curatedIds.length > 0 ? curatedIds : Array.from(approvedIds)).slice(0, 12); }; const findSceneById = (payload: RepositoryState, sceneId: string) => @@ -263,8 +263,7 @@ const buildParamsForScene = ( const buildPresentationFromCue = ( payload: RepositoryState, - cue: Cue | undefined, - fallbackAssetIds: string[] + cue: Cue | undefined ): SurfacePresentation | null => { if (!cue) { return null; @@ -281,7 +280,7 @@ const buildPresentationFromCue = ( : findCollectionAssets(payload, cue.collectionId); const assetMap = new Map(payload.photoAssets.map((asset) => [asset.id, asset] as const)); - const assets = (cueAssetIds.length > 0 ? cueAssetIds : fallbackAssetIds) + const assets = cueAssetIds .map((assetId) => assetMap.get(assetId)) .filter((asset): asset is PhotoAsset => Boolean(asset)); @@ -322,13 +321,12 @@ const getSuggestedAssetsForScene = (payload: RepositoryState, sceneId: string, f const approved = getApprovedAssets(payload); const curatedCollection = payload.collections.find((collection) => collection.id === "collection-curated-library"); const curatedIds = new Set(curatedCollection?.assetIds ?? []); - const favorites = new Set(payload.collections.find((collection) => collection.kind === "favorites")?.assetIds ?? []); const recommendedLimit = Math.min(scene?.inputRules.maxAssets ?? 8, 12); const prioritized = approved .slice() .sort((left, right) => { - const leftScore = (curatedIds.has(left.id) ? 3 : 0) + (favorites.has(left.id) ? 2 : 0); - const rightScore = (curatedIds.has(right.id) ? 3 : 0) + (favorites.has(right.id) ? 2 : 0); + const leftScore = curatedIds.has(left.id) ? 3 : 0; + const rightScore = curatedIds.has(right.id) ? 3 : 0; return rightScore - leftScore; }) .map((asset) => asset.id) @@ -414,10 +412,10 @@ const createInitialLiveState = (payload: RepositoryState) => { return { cueState: armedState, - programPresentation: buildPresentationFromCue(payload, programCue, defaultAssetIds), + programPresentation: buildPresentationFromCue(payload, programCue), programTransition: programCue?.transitionIn ?? null, selectedSceneId: previewScene?.id ?? payload.scenes[0]?.id ?? "", - selectedAssetIds: previewCueAssetIds.length > 0 + selectedAssetIds: previewCue ? previewCueAssetIds : previewScene ? getSuggestedAssetsForScene(payload, previewScene.id, defaultAssetIds) @@ -630,10 +628,6 @@ export const App = () => { const canMoveSelectedCueUp = selectedCueIndex > 0 && !cueMoveInFlight; const canMoveSelectedCueDown = selectedCueIndex >= 0 && selectedCueIndex < cueStack.length - 1 && !cueMoveInFlight; const canSaveCue = Boolean(selectedScene && previewParams && !cueMutationInFlight && (!cueDraft.id || cueDraftDirty)); - const favoriteCollection: Collection | undefined = useMemo( - () => state?.collections.find((collection) => collection.kind === "favorites"), - [state?.collections] - ); const curatedCollection: Collection | undefined = useMemo( () => state?.collections.find((collection) => collection.id === "collection-curated-library"), [state?.collections] @@ -776,7 +770,7 @@ export const App = () => { [previewPresentation] ); const programPresentation = programOutputState?.presentation ?? null; - const programActivationKey = `${programOutputState?.outputRevision ?? 0}:${createPresentationStructureHash(programPresentation)}`; + const programActivationKey = createPresentationStructureHash(programPresentation); useEffect(() => { dispatchAdmin({ @@ -793,13 +787,14 @@ export const App = () => { const nextPreset = matchPresetForScene(scene, availablePresets); const preservedAssetIds = filterAvailableAssetIds(state, selectedAssetIds); + const shouldPreserveEmptySelection = selectedAssetIds.length === 0; dispatchAdmin({ type: "previewSceneSelected", scene, presetId: nextPreset?.id ?? availablePresets[0]?.id ?? effectPresetLibrary[0]?.id ?? "", params: buildParamsForScene(scene, undefined, nextPreset), assetIds: - preservedAssetIds.length > 0 + preservedAssetIds.length > 0 || shouldPreserveEmptySelection ? preservedAssetIds : getSuggestedAssetsForScene(state, scene.id, getDefaultAssetIds(state)) }); @@ -827,7 +822,7 @@ export const App = () => { scene, presetId: matchedPreset?.id ?? availablePresets[0]?.id ?? effectPresetLibrary[0]?.id ?? "", params: buildParamsForScene(scene, cue.parameterOverrides, matchedPreset), - assetIds: cueAssetIds.length > 0 ? cueAssetIds : getSuggestedAssetsForScene(state, scene.id, getDefaultAssetIds(state)), + assetIds: cueAssetIds, armPreview: options.armPreview }); }; @@ -851,10 +846,7 @@ export const App = () => { } const preset = matchPresetForScene(scene, availablePresets, draft.effectPresetId); - const nextAssetIds = - draft.assetIds && draft.assetIds.length > 0 - ? filterAvailableAssetIds(state, draft.assetIds) - : getSuggestedAssetsForScene(state, scene.id, getDefaultAssetIds(state)); + const nextAssetIds = draft.assetIds ? filterAvailableAssetIds(state, draft.assetIds) : []; dispatchAdmin({ type: "generatedCueDraftLoaded", @@ -1001,8 +993,16 @@ export const App = () => { }; const setBlackout = (blackout: boolean) => { + if (cueState.blackout === blackout) { + return; + } + + const programCue = programOutputState?.presentation?.cue ?? null; + const blackoutTransition = blackout + ? programCue?.transitionOut ?? defaultCueTransition + : programCue?.transitionIn ?? defaultCueTransition; dispatchAdmin({ type: "blackoutSet", blackout }); - publishProgramOutput(programOutputState?.presentation ?? null, blackout, programOutputState?.transition ?? null); + publishProgramOutput(programOutputState?.presentation ?? null, blackout, blackoutTransition); }; useEffect(() => { @@ -1377,17 +1377,22 @@ export const App = () => { }); }; - const handleLoadFavorites = () => { + const handleLoadCuratedAssets = () => { if (!state) { return; } - const nextAssetIds = getDefaultAssetIds(state); + const approvedIds = new Set(approvedAssets.map((asset) => asset.id)); + const curatedAssetIds = (curatedCollection?.assetIds.filter((assetId) => approvedIds.has(assetId)) ?? []).slice(0, 12); + const nextAssetIds = curatedAssetIds.length > 0 ? curatedAssetIds : getDefaultAssetIds(state); dispatchAdmin({ type: "selectedAssetsReplaced", assetIds: nextAssetIds, metadataAssetId: nextAssetIds[0] ?? null, - status: "Favorites bank loaded into preview." + status: + curatedAssetIds.length > 0 + ? "Curated library loaded into preview." + : "No curated images available. Loaded default approved media instead." }); }; @@ -1749,8 +1754,8 @@ export const App = () => { - @@ -1786,8 +1791,8 @@ export const App = () => { -