Support scenic-only scenes and blackout fades
This commit is contained in:
+37
-32
@@ -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 = () => {
|
||||
<button onClick={handleLoadSuggestedAssets} disabled={!selectedScene}>
|
||||
Suggested media
|
||||
</button>
|
||||
<button onClick={handleLoadFavorites} disabled={!state}>
|
||||
Favorites
|
||||
<button onClick={handleLoadCuratedAssets} disabled={!state}>
|
||||
Curated
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1786,8 +1791,8 @@ export const App = () => {
|
||||
<button onClick={handleLoadSuggestedAssets} disabled={!selectedScene}>
|
||||
Suggested
|
||||
</button>
|
||||
<button onClick={handleLoadFavorites} disabled={!state}>
|
||||
Favorites
|
||||
<button onClick={handleLoadCuratedAssets} disabled={!state}>
|
||||
Curated
|
||||
</button>
|
||||
<button onClick={handleClearSelectedAssets} disabled={selectedAssetIds.length === 0}>
|
||||
Clear
|
||||
@@ -2316,7 +2321,7 @@ export const App = () => {
|
||||
presentation={programPresentation}
|
||||
blackout={cueState.blackout}
|
||||
transition={programOutputState?.transition ?? null}
|
||||
activationKey={`${programActivationKey}:${cueState.blackout ? "blackout" : "live"}`}
|
||||
activationKey={programActivationKey}
|
||||
qualityProfile="program"
|
||||
/>
|
||||
</div>
|
||||
@@ -2532,7 +2537,7 @@ export const App = () => {
|
||||
<div className="build-media-workarea">
|
||||
<div className="build-media-browser">
|
||||
<p className="bank-summary bank-summary--visible">
|
||||
Favorites: <strong>{favoriteCollection?.assetIds.length ?? 0}</strong> / Curated library: <strong>{curatedCollection?.assetIds.length ?? 0}</strong>
|
||||
Curated library: <strong>{curatedCollection?.assetIds.length ?? 0}</strong> / Approved bank: <strong>{filteredApprovedAssets.length}</strong>
|
||||
</p>
|
||||
{renderApprovedBank("build")}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import type { CueTransition } from "@goodgrief/shared-types";
|
||||
import { SceneViewport } from "../features/live/SceneViewport";
|
||||
import { readProgramOutputState, subscribeProgramOutput, type ProgramOutputState } from "../features/live/output-sync";
|
||||
import {
|
||||
createPresentationStructureHash,
|
||||
readProgramOutputState,
|
||||
subscribeProgramOutput,
|
||||
type ProgramOutputState
|
||||
} from "../features/live/output-sync";
|
||||
import "./output.css";
|
||||
|
||||
const enterFullscreen = async () => {
|
||||
@@ -126,7 +131,7 @@ export const ProgramOutputApp = () => {
|
||||
presentation={outputState?.presentation ?? null}
|
||||
blackout={outputState?.blackout ?? false}
|
||||
transition={transition}
|
||||
activationKey={`${outputState?.presentationHash ?? "program-empty"}:${outputState?.outputRevision ?? 0}:${outputState?.blackout ? "blackout" : "live"}`}
|
||||
activationKey={createPresentationStructureHash(outputState?.presentation ?? null)}
|
||||
/>
|
||||
<div className={`output-overlay ${overlayVisible ? "output-overlay--visible" : ""}`}>
|
||||
<div>
|
||||
|
||||
@@ -65,7 +65,7 @@ export const SceneViewport = ({
|
||||
surface.registerMany(defaultScenePlugins);
|
||||
surface.setQualityProfile(qualityProfileRef.current);
|
||||
surface.setBusy(busyRef.current);
|
||||
surface.setBlackout(blackoutRef.current);
|
||||
surface.setBlackout(blackoutRef.current, null, true);
|
||||
surfaceRef.current = surface;
|
||||
|
||||
const resize = () => {
|
||||
@@ -92,8 +92,8 @@ export const SceneViewport = ({
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
surfaceRef.current?.setBlackout(blackout);
|
||||
}, [blackout]);
|
||||
surfaceRef.current?.setBlackout(blackout, transition ?? defaultTransition);
|
||||
}, [blackout, transition]);
|
||||
|
||||
useEffect(() => {
|
||||
surfaceRef.current?.setQualityProfile(qualityProfile);
|
||||
|
||||
Reference in New Issue
Block a user