@hyperframes/studio 0.7.58 → 0.7.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{hyperframes-player-CtTDO63S.js → hyperframes-player-3XTTaVNf.js} +1 -1
- package/dist/assets/{index-C47jAC3Q.js → index-D6etaey-.js} +1 -1
- package/dist/assets/index-DXbu6IPT.css +1 -0
- package/dist/assets/{index-DeQPzqwH.js → index-Dh_WhagG.js} +1 -1
- package/dist/assets/index-cH6NfVV_.js +426 -0
- package/dist/index.d.ts +12 -3
- package/dist/index.html +2 -2
- package/dist/index.js +13755 -8627
- package/dist/index.js.map +1 -1
- package/dist/styles/tailwind-preset.d.ts +5 -0
- package/dist/styles/tailwind-preset.js +8 -1
- package/dist/styles/tailwind-preset.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +4 -4
- package/src/components/DesignPanelPromoteProvider.tsx +15 -2
- package/src/components/StudioRightPanel.tsx +42 -53
- package/src/components/editor/AnimationCard.test.tsx +134 -0
- package/src/components/editor/AnimationCard.tsx +19 -4
- package/src/components/editor/ArcPathControls.tsx +1 -0
- package/src/components/editor/GestureRecordControl.tsx +7 -1
- package/src/components/editor/GsapAnimationSection.tsx +109 -14
- package/src/components/editor/InspectorHeaderActions.tsx +42 -4
- package/src/components/editor/PropertyPanel.test.tsx +948 -0
- package/src/components/editor/PropertyPanel.tsx +144 -148
- package/src/components/editor/PropertyPanelEmptyState.test.tsx +74 -0
- package/src/components/editor/PropertyPanelEmptyState.tsx +179 -1
- package/src/components/editor/PropertyPanelFlat.tsx +580 -0
- package/src/components/editor/PropertyPanelFlatFooter.test.tsx +83 -0
- package/src/components/editor/PropertyPanelFlatFooter.tsx +73 -0
- package/src/components/editor/PropertyPanelFlatHeader.test.tsx +79 -0
- package/src/components/editor/PropertyPanelFlatHeader.tsx +112 -0
- package/src/components/editor/gsapAnimationCallbacks.ts +27 -0
- package/src/components/editor/gsapLivePreview.ts +34 -0
- package/src/components/editor/manualEditingAvailability.test.ts +10 -0
- package/src/components/editor/manualEditingAvailability.ts +9 -0
- package/src/components/editor/propertyPanel3dTransform.tsx +5 -0
- package/src/components/editor/propertyPanelColor.test.tsx +28 -0
- package/src/components/editor/propertyPanelColor.tsx +31 -1
- package/src/components/editor/propertyPanelColorGradingControls.tsx +11 -2
- package/src/components/editor/propertyPanelColorGradingSection.tsx +45 -365
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +19 -5
- package/src/components/editor/propertyPanelFill.tsx +22 -5
- package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +622 -0
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +570 -0
- package/src/components/editor/propertyPanelFlatLayoutSection.test.tsx +293 -0
- package/src/components/editor/propertyPanelFlatLayoutSection.tsx +383 -0
- package/src/components/editor/propertyPanelFlatMaskInsetRows.tsx +102 -0
- package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +436 -0
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +286 -0
- package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +272 -0
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +315 -0
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +1174 -0
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +563 -0
- package/src/components/editor/propertyPanelFlatSelectRow.tsx +107 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.test.ts +23 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.ts +27 -0
- package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +598 -0
- package/src/components/editor/propertyPanelFlatStyleSections.tsx +516 -0
- package/src/components/editor/propertyPanelFlatTextSection.test.tsx +465 -0
- package/src/components/editor/propertyPanelFlatTextSection.tsx +407 -0
- package/src/components/editor/propertyPanelFlatTimingDerivation.test.ts +71 -0
- package/src/components/editor/propertyPanelFlatTimingDerivation.ts +59 -0
- package/src/components/editor/propertyPanelFlatToggle.test.tsx +63 -0
- package/src/components/editor/propertyPanelFlatToggle.tsx +53 -0
- package/src/components/editor/propertyPanelFont.test.tsx +30 -0
- package/src/components/editor/propertyPanelFont.tsx +136 -95
- package/src/components/editor/propertyPanelHelpers.ts +73 -0
- package/src/components/editor/propertyPanelInputCoverage.test.tsx +587 -0
- package/src/components/editor/propertyPanelMediaSection.tsx +10 -0
- package/src/components/editor/propertyPanelPrimitives.tsx +60 -29
- package/src/components/editor/propertyPanelSections.test.tsx +161 -0
- package/src/components/editor/propertyPanelSections.tsx +154 -90
- package/src/components/editor/propertyPanelStyleSections.tsx +12 -1
- package/src/components/editor/propertyPanelTimingSection.tsx +1 -1
- package/src/components/editor/propertyPanelTypes.ts +14 -1
- package/src/components/editor/propertyPanelValueTier.test.ts +32 -0
- package/src/components/editor/propertyPanelValueTier.ts +28 -0
- package/src/components/editor/useColorGradingController.test.ts +418 -0
- package/src/components/editor/useColorGradingController.ts +596 -0
- package/src/components/panels/VariablesPanel.tsx +4 -0
- package/src/components/sidebar/CompositionsTab.test.ts +28 -2
- package/src/components/sidebar/CompositionsTab.tsx +3 -1
- package/src/components/storyboard/FramePoster.tsx +24 -4
- package/src/components/storyboard/StoryboardFrameFocus.tsx +4 -0
- package/src/components/storyboard/StoryboardFrameTile.tsx +30 -1
- package/src/components/storyboard/StoryboardGrid.tsx +23 -1
- package/src/components/storyboard/StoryboardLoaded.tsx +58 -2
- package/src/components/storyboard/StoryboardView.tsx +9 -2
- package/src/components/storyboard/frameComments.test.ts +95 -0
- package/src/components/storyboard/frameComments.ts +115 -0
- package/src/components/storyboard/useFrameComments.ts +82 -0
- package/src/contexts/DesignPanelInputContext.test.tsx +81 -0
- package/src/contexts/DesignPanelInputContext.tsx +48 -0
- package/src/contexts/DomEditContext.tsx +4 -0
- package/src/contexts/FileManagerContext.tsx +3 -0
- package/src/contexts/PanelLayoutContext.tsx +3 -0
- package/src/contexts/VariablePromoteContext.test.tsx +67 -0
- package/src/contexts/VariablePromoteContext.tsx +10 -5
- package/src/hooks/domEditCommitRunner.ts +11 -0
- package/src/hooks/gsapScriptCommitTypes.ts +6 -4
- package/src/hooks/timelineEditingHelpers.ts +2 -2
- package/src/hooks/timelineTrackVisibility.test.ts +52 -0
- package/src/hooks/timelineTrackVisibility.ts +27 -10
- package/src/hooks/useAppHotkeys.ts +9 -0
- package/src/hooks/useDomEditAttributeCommits.ts +123 -1
- package/src/hooks/useDomEditCommits.test.tsx +41 -1
- package/src/hooks/useDomEditCommits.ts +31 -177
- package/src/hooks/useDomEditCommitsHelpers.ts +158 -0
- package/src/hooks/useDomEditSession.ts +18 -6
- package/src/hooks/useDomEditTextCommits.ts +14 -9
- package/src/hooks/useDomEditWiring.ts +10 -9
- package/src/hooks/useEditorSave.ts +1 -1
- package/src/hooks/useElementLifecycleOps.ts +8 -3
- package/src/hooks/useFileManager.projectOwnership.test.tsx +109 -0
- package/src/hooks/useFileManager.ts +151 -65
- package/src/hooks/useGsapAnimationOps.ts +7 -6
- package/src/hooks/useGsapKeyframeOps.ts +6 -5
- package/src/hooks/useGsapPropertyDebounce.ts +43 -28
- package/src/hooks/useGsapPropertyDebounceFlush.test.ts +35 -0
- package/src/hooks/useGsapScriptCommits.test.tsx +48 -2
- package/src/hooks/useGsapScriptCommits.ts +150 -50
- package/src/hooks/useGsapSelectionHandlers.test.tsx +116 -0
- package/src/hooks/useGsapSelectionHandlers.ts +94 -31
- package/src/hooks/useInspectorSplitResize.ts +51 -0
- package/src/hooks/usePanelLayout.test.ts +118 -0
- package/src/hooks/usePanelLayout.ts +24 -1
- package/src/hooks/usePersistentEditHistory.projectOwnership.test.tsx +60 -0
- package/src/hooks/usePersistentEditHistory.test.ts +49 -1
- package/src/hooks/usePersistentEditHistory.ts +71 -31
- package/src/hooks/usePreviewPersistence.ts +4 -1
- package/src/hooks/useProjectCompositionVariables.ts +10 -10
- package/src/hooks/useProjectSignaturePoll.test.tsx +135 -0
- package/src/hooks/useProjectSignaturePoll.ts +68 -0
- package/src/hooks/useRazorSplit.history.test.tsx +12 -4
- package/src/hooks/useRazorSplit.test.tsx +88 -0
- package/src/hooks/useRazorSplit.testHelpers.ts +3 -2
- package/src/hooks/useRazorSplit.ts +50 -16
- package/src/hooks/useSdkSession.lifecycle.test.tsx +137 -0
- package/src/hooks/useSdkSession.ts +155 -14
- package/src/hooks/useSlideshowPersist.ts +7 -1
- package/src/hooks/useStoryboard.ts +23 -4
- package/src/hooks/useTimelineEditing.test.tsx +4 -1
- package/src/hooks/useTimelineEditing.ts +18 -10
- package/src/hooks/useTimelineEditingTypes.ts +5 -1
- package/src/hooks/useTimelineGroupEditing.ts +17 -3
- package/src/hooks/useVariablesPersist.ts +9 -7
- package/src/icons/SystemIcons.tsx +2 -0
- package/src/styles/studio.css +22 -0
- package/src/styles/tailwind-preset.shared.js +7 -0
- package/src/utils/designInputTracking.test.ts +97 -0
- package/src/utils/designInputTracking.ts +80 -0
- package/src/utils/domEditSaveQueue.test.ts +19 -0
- package/src/utils/domEditSaveQueue.ts +2 -1
- package/src/utils/sdkCutover.gate.test.ts +7 -7
- package/src/utils/sdkCutover.test.ts +511 -62
- package/src/utils/sdkCutover.ts +181 -219
- package/src/utils/sdkEditTransaction.ts +293 -0
- package/src/utils/sdkResolverAttempts.ts +94 -0
- package/src/utils/sdkResolverShadow.test.ts +148 -0
- package/src/utils/sdkResolverShadow.ts +139 -106
- package/src/utils/setSlideshowManifest.test.ts +0 -9
- package/src/utils/setSlideshowManifest.ts +20 -27
- package/src/utils/studioFileHistory.test.ts +41 -0
- package/src/utils/studioFileHistory.ts +34 -29
- package/src/utils/studioFileMutationCoordinator.ts +45 -0
- package/src/utils/studioFileVersion.test.ts +36 -0
- package/src/utils/studioFileVersion.ts +23 -0
- package/src/utils/studioHelpers.test.ts +45 -0
- package/src/utils/studioHelpers.ts +45 -14
- package/src/utils/studioSaveDiagnostics.test.ts +18 -0
- package/src/utils/studioSaveDiagnostics.ts +38 -0
- package/dist/assets/index-B_UvTX3E.js +0 -423
- package/dist/assets/index-uahwWkgw.css +0 -1
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState, type RefObject } from "react";
|
|
2
|
+
import {
|
|
3
|
+
HF_COLOR_GRADING_ATTR,
|
|
4
|
+
isHfColorGradingActive,
|
|
5
|
+
normalizeHfColorGrading,
|
|
6
|
+
serializeHfColorGrading,
|
|
7
|
+
type HfColorGradingTarget,
|
|
8
|
+
type NormalizedHfColorGrading,
|
|
9
|
+
} from "@hyperframes/core/color-grading";
|
|
10
|
+
import {
|
|
11
|
+
addStudioPendingEditFlushListener,
|
|
12
|
+
trackStudioPendingEdit,
|
|
13
|
+
} from "../../utils/studioPendingEdits";
|
|
14
|
+
import type { DomEditSelection } from "./domEditing";
|
|
15
|
+
import { selectionIdentityKey, stripQueryAndHash } from "./propertyPanelHelpers";
|
|
16
|
+
import { bumpDomEditCommitVersion } from "../../hooks/domEditCommitRunner";
|
|
17
|
+
import {
|
|
18
|
+
acceptStudioRuntimeMessage,
|
|
19
|
+
postRuntimeControlMessage,
|
|
20
|
+
} from "../../player/lib/runtimeProtocol";
|
|
21
|
+
|
|
22
|
+
const COLOR_GRADING_DATA_KEY = HF_COLOR_GRADING_ATTR.replace(/^data-/, "");
|
|
23
|
+
const RUNTIME_STATUS_REFRESH_DELAYS = [50, 250, 1000, 2500] as const;
|
|
24
|
+
const MEDIA_METADATA_CACHE = new Map<string, MediaMetadata | null>();
|
|
25
|
+
|
|
26
|
+
export interface RuntimeColorGradingStatus {
|
|
27
|
+
state: "missing" | "inactive" | "pending" | "active" | "unavailable";
|
|
28
|
+
message: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface MediaMetadata {
|
|
32
|
+
kind: "video" | "image" | "audio" | "unknown";
|
|
33
|
+
color: {
|
|
34
|
+
dynamicRange: "hdr" | "sdr" | "unknown";
|
|
35
|
+
hdrTransfer: "pq" | "hlg" | "unknown" | null;
|
|
36
|
+
label: string;
|
|
37
|
+
isHdr: boolean;
|
|
38
|
+
codecName?: string;
|
|
39
|
+
profile?: string;
|
|
40
|
+
pixelFormat?: string;
|
|
41
|
+
colorSpace?: string;
|
|
42
|
+
colorTransfer?: string;
|
|
43
|
+
colorPrimaries?: string;
|
|
44
|
+
};
|
|
45
|
+
probeError?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface MediaMetadataResponse {
|
|
49
|
+
path: string;
|
|
50
|
+
metadata: MediaMetadata;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function stripPreviewAssetPath(src: string, projectId: string): string | null {
|
|
54
|
+
let pathname = src;
|
|
55
|
+
try {
|
|
56
|
+
pathname = new URL(src, window.location.href).pathname;
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const projectMarker = `/api/projects/${encodeURIComponent(projectId)}/preview/`;
|
|
61
|
+
const genericMarker = "/preview/";
|
|
62
|
+
const marker = pathname.includes(projectMarker) ? projectMarker : genericMarker;
|
|
63
|
+
const index = pathname.indexOf(marker);
|
|
64
|
+
if (index < 0) return null;
|
|
65
|
+
const assetPath = decodeURIComponent(pathname.slice(index + marker.length)).replace(/^\/+/, "");
|
|
66
|
+
if (!assetPath || assetPath.startsWith("comp/")) return null;
|
|
67
|
+
return assetPath;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// fallow-ignore-next-line complexity
|
|
71
|
+
function resolveProjectAssetPath(
|
|
72
|
+
sourceFile: string,
|
|
73
|
+
src: string,
|
|
74
|
+
projectId: string,
|
|
75
|
+
): string | null {
|
|
76
|
+
const trimmed = stripQueryAndHash(src.trim());
|
|
77
|
+
if (!trimmed || /^(?:data:|blob:)/i.test(trimmed)) return null;
|
|
78
|
+
if (/^https?:\/\//i.test(trimmed)) return stripPreviewAssetPath(trimmed, projectId);
|
|
79
|
+
if (trimmed.startsWith("/")) {
|
|
80
|
+
return stripPreviewAssetPath(trimmed, projectId);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const sourceDir = sourceFile.includes("/")
|
|
84
|
+
? sourceFile.slice(0, sourceFile.lastIndexOf("/"))
|
|
85
|
+
: "";
|
|
86
|
+
const parts = `${sourceDir}/${trimmed}`.split("/");
|
|
87
|
+
const normalized: string[] = [];
|
|
88
|
+
for (const part of parts) {
|
|
89
|
+
if (!part || part === ".") continue;
|
|
90
|
+
if (part === "..") {
|
|
91
|
+
normalized.pop();
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
normalized.push(part);
|
|
95
|
+
}
|
|
96
|
+
return normalized.join("/") || null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function selectedMediaAssetPath(element: DomEditSelection, projectId: string): string | null {
|
|
100
|
+
if (element.tagName !== "video" && element.tagName !== "img") return null;
|
|
101
|
+
const media = element.element as HTMLImageElement | HTMLVideoElement;
|
|
102
|
+
const src = media.getAttribute("src") || media.currentSrc || "";
|
|
103
|
+
return resolveProjectAssetPath(element.sourceFile || "index.html", src, projectId);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function defaultColorGrading(): NormalizedHfColorGrading {
|
|
107
|
+
const grading = normalizeHfColorGrading("neutral");
|
|
108
|
+
if (!grading) throw new Error("Missing neutral color grading preset");
|
|
109
|
+
return grading;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function readColorGradingFromElement(element: DomEditSelection): NormalizedHfColorGrading {
|
|
113
|
+
return (
|
|
114
|
+
normalizeHfColorGrading(element.dataAttributes[COLOR_GRADING_DATA_KEY]) ?? defaultColorGrading()
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function toBridgeColorGrading(grading: NormalizedHfColorGrading): unknown {
|
|
119
|
+
if (!isHfColorGradingActive(grading)) return null;
|
|
120
|
+
const { enabled: _enabled, ...bridgeGrading } = grading;
|
|
121
|
+
return bridgeGrading;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function readRuntimeColorGradingStatus(
|
|
125
|
+
iframe: HTMLIFrameElement | null | undefined,
|
|
126
|
+
target: HfColorGradingTarget,
|
|
127
|
+
): RuntimeColorGradingStatus {
|
|
128
|
+
try {
|
|
129
|
+
const win = iframe?.contentWindow as
|
|
130
|
+
| (Window & {
|
|
131
|
+
__hf?: {
|
|
132
|
+
colorGrading?: {
|
|
133
|
+
getStatus?: (
|
|
134
|
+
target: HfColorGradingTarget | string | null | undefined,
|
|
135
|
+
) => RuntimeColorGradingStatus;
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
})
|
|
139
|
+
| null
|
|
140
|
+
| undefined;
|
|
141
|
+
const status = win?.__hf?.colorGrading?.getStatus?.(target);
|
|
142
|
+
return status ?? { state: "pending", message: "Waiting for runtime" };
|
|
143
|
+
} catch {
|
|
144
|
+
return { state: "unavailable", message: "Preview unavailable" };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface ColorGradingControllerState {
|
|
149
|
+
grading: NormalizedHfColorGrading;
|
|
150
|
+
compareEnabled: boolean;
|
|
151
|
+
applyScope: "source-file" | "project";
|
|
152
|
+
applyBusy: boolean;
|
|
153
|
+
runtimeStatus: RuntimeColorGradingStatus;
|
|
154
|
+
mediaMetadata: MediaMetadata | null;
|
|
155
|
+
commitColorGrading: (next: NormalizedHfColorGrading) => void;
|
|
156
|
+
commitCompare: (enabled: boolean) => void;
|
|
157
|
+
setApplyScope: (scope: "source-file" | "project") => void;
|
|
158
|
+
applyToScope: () => Promise<void>;
|
|
159
|
+
resetGrading: () => void;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function useColorGradingController({
|
|
163
|
+
projectId,
|
|
164
|
+
element,
|
|
165
|
+
previewIframeRef,
|
|
166
|
+
onSetAttributeLive,
|
|
167
|
+
onApplyScope,
|
|
168
|
+
}: {
|
|
169
|
+
projectId: string;
|
|
170
|
+
element: DomEditSelection;
|
|
171
|
+
previewIframeRef?: RefObject<HTMLIFrameElement | null>;
|
|
172
|
+
onSetAttributeLive: (
|
|
173
|
+
attr: string,
|
|
174
|
+
value: string | null,
|
|
175
|
+
onSettled?: (ok: boolean) => void,
|
|
176
|
+
) => void | Promise<void>;
|
|
177
|
+
onApplyScope?: (
|
|
178
|
+
scope: "source-file" | "project",
|
|
179
|
+
value: string | null,
|
|
180
|
+
) => Promise<{ changedFiles: number; changedElements: number }>;
|
|
181
|
+
}): ColorGradingControllerState {
|
|
182
|
+
const [grading, setGrading] = useState(() => readColorGradingFromElement(element));
|
|
183
|
+
const [compareEnabled, setCompareEnabled] = useState(false);
|
|
184
|
+
const [applyScope, setApplyScope] = useState<"source-file" | "project">("source-file");
|
|
185
|
+
const [applyBusy, setApplyBusy] = useState(false);
|
|
186
|
+
const [runtimeStatus, setRuntimeStatus] = useState<RuntimeColorGradingStatus>(() => ({
|
|
187
|
+
state: "pending",
|
|
188
|
+
message: "Waiting for runtime",
|
|
189
|
+
}));
|
|
190
|
+
const selectedAssetPath = useMemo(
|
|
191
|
+
() => selectedMediaAssetPath(element, projectId),
|
|
192
|
+
[element, projectId],
|
|
193
|
+
);
|
|
194
|
+
const [mediaMetadata, setMediaMetadata] = useState<MediaMetadata | null>(null);
|
|
195
|
+
const persistTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
196
|
+
const pendingPersistValueRef = useRef<string | null | undefined>(undefined);
|
|
197
|
+
const pendingPersistGradingRef = useRef<NormalizedHfColorGrading | null>(null);
|
|
198
|
+
// Identity the pending edit was made FOR, snapshotted at schedule time —
|
|
199
|
+
// read by a global flush instead of identityKeyRef.current, which may no
|
|
200
|
+
// longer describe this edit's element by the time the flush runs.
|
|
201
|
+
const pendingPersistIdentityRef = useRef<string | null>(null);
|
|
202
|
+
// The last grading value actually confirmed saved — distinct from `grading`
|
|
203
|
+
// (the optimistic value shown immediately on commit). A rejected persist
|
|
204
|
+
// reverts to this instead of leaving the UI permanently showing a value
|
|
205
|
+
// that was never written.
|
|
206
|
+
const confirmedGradingRef = useRef(grading);
|
|
207
|
+
// Monotonic per-commit version — guards against TWO edits on the SAME
|
|
208
|
+
// element racing (not just a selection change). If edit A's persist is
|
|
209
|
+
// still in flight when edit B commits, A's eventual settle must not stamp
|
|
210
|
+
// confirmedGradingRef with its now-superseded value or revert `grading`
|
|
211
|
+
// out from under B's newer optimistic state.
|
|
212
|
+
const gradingVersionRef = useRef(0);
|
|
213
|
+
const statusTimersRef = useRef<number[]>([]);
|
|
214
|
+
const latestGradingRef = useRef(grading);
|
|
215
|
+
const compareEnabledRef = useRef(compareEnabled);
|
|
216
|
+
latestGradingRef.current = grading;
|
|
217
|
+
compareEnabledRef.current = compareEnabled;
|
|
218
|
+
|
|
219
|
+
// Reset all per-element STATE when the selection changes to a different
|
|
220
|
+
// element — unlike the legacy ColorGradingSection (remounted via a
|
|
221
|
+
// `key={selectionIdentityKey(element)}` from its parent), this hook is
|
|
222
|
+
// called unconditionally on every render, so nothing naturally remounts it.
|
|
223
|
+
// Without this, switching selection reuses the previous element's grading/
|
|
224
|
+
// compare/mediaMetadata state. Adjusting state during render (comparing
|
|
225
|
+
// against a ref) is React's documented pattern for STATE updates
|
|
226
|
+
// specifically — it resolves in the same render pass instead of flashing
|
|
227
|
+
// stale state for one frame, and is safe to repeat if React discards and
|
|
228
|
+
// re-runs this render, since every value here is a pure function of
|
|
229
|
+
// `element`. It must NOT be used for side effects or for consuming
|
|
230
|
+
// shared mutable state (like the pending-persist timer/value) — a
|
|
231
|
+
// discarded render would have already consumed them with no corresponding
|
|
232
|
+
// effect ever running to compensate. That part happens below, in an
|
|
233
|
+
// effect's cleanup, which is guaranteed to run only for a render that
|
|
234
|
+
// actually committed.
|
|
235
|
+
const identityKey = selectionIdentityKey(element);
|
|
236
|
+
const identityKeyRef = useRef(identityKey);
|
|
237
|
+
if (identityKeyRef.current !== identityKey) {
|
|
238
|
+
identityKeyRef.current = identityKey;
|
|
239
|
+
const freshGrading = readColorGradingFromElement(element);
|
|
240
|
+
latestGradingRef.current = freshGrading;
|
|
241
|
+
confirmedGradingRef.current = freshGrading;
|
|
242
|
+
setGrading(freshGrading);
|
|
243
|
+
setCompareEnabled(false);
|
|
244
|
+
compareEnabledRef.current = false;
|
|
245
|
+
setApplyScope("source-file");
|
|
246
|
+
setApplyBusy(false);
|
|
247
|
+
setRuntimeStatus({ state: "pending", message: "Waiting for runtime" });
|
|
248
|
+
setMediaMetadata(null);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Flushes — never discards — a still-pending edit when selection moves to
|
|
252
|
+
// a different element, and cancels the debounce timer. Implemented as an
|
|
253
|
+
// effect CLEANUP (not the render-phase block above, and not a queued ref
|
|
254
|
+
// consumed by a separate effect): a cleanup only ever runs for the
|
|
255
|
+
// specific effect instance that actually committed for `identityKey`, so
|
|
256
|
+
// there's no window where a discarded/interrupted render could have
|
|
257
|
+
// already consumed pendingPersistValueRef without this ever running to
|
|
258
|
+
// compensate. The cleanup's closure captures onSetAttributeLive/target
|
|
259
|
+
// bound to the OUTGOING identity, since it runs before the next effect
|
|
260
|
+
// instance (for the NEW identity) is established.
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
return () => {
|
|
263
|
+
// Stale runtime-status timers scheduled for the OUTGOING element must
|
|
264
|
+
// not fire after this: readRuntimeColorGradingStatus closes over
|
|
265
|
+
// `target`, so an old timer firing post-switch would stamp the NEW
|
|
266
|
+
// element's runtimeStatus with the OLD element's answer.
|
|
267
|
+
for (const timer of statusTimersRef.current) clearTimeout(timer);
|
|
268
|
+
statusTimersRef.current = [];
|
|
269
|
+
if (persistTimerRef.current) {
|
|
270
|
+
clearTimeout(persistTimerRef.current);
|
|
271
|
+
persistTimerRef.current = null;
|
|
272
|
+
}
|
|
273
|
+
if (pendingPersistValueRef.current === undefined) return;
|
|
274
|
+
const value = pendingPersistValueRef.current;
|
|
275
|
+
pendingPersistValueRef.current = undefined;
|
|
276
|
+
pendingPersistGradingRef.current = null;
|
|
277
|
+
pendingPersistIdentityRef.current = null;
|
|
278
|
+
trackStudioPendingEdit(onSetAttributeLive(COLOR_GRADING_DATA_KEY, value));
|
|
279
|
+
};
|
|
280
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- identityKey is the intended trigger; see comment above
|
|
281
|
+
}, [identityKey]);
|
|
282
|
+
|
|
283
|
+
const target = useMemo(
|
|
284
|
+
(): HfColorGradingTarget => ({
|
|
285
|
+
id: element.id ?? null,
|
|
286
|
+
hfId: element.hfId ?? null,
|
|
287
|
+
selector: element.selector ?? null,
|
|
288
|
+
selectorIndex: element.selectorIndex ?? null,
|
|
289
|
+
}),
|
|
290
|
+
[element.hfId, element.id, element.selector, element.selectorIndex],
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const refreshRuntimeStatus = useCallback(() => {
|
|
294
|
+
setRuntimeStatus(readRuntimeColorGradingStatus(previewIframeRef?.current, target));
|
|
295
|
+
}, [previewIframeRef, target]);
|
|
296
|
+
|
|
297
|
+
useEffect(() => {
|
|
298
|
+
setMediaMetadata(null);
|
|
299
|
+
if (!selectedAssetPath) return;
|
|
300
|
+
const cacheKey = `${projectId}:${selectedAssetPath}`;
|
|
301
|
+
if (MEDIA_METADATA_CACHE.has(cacheKey)) {
|
|
302
|
+
setMediaMetadata(MEDIA_METADATA_CACHE.get(cacheKey) ?? null);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const controller = new AbortController();
|
|
306
|
+
fetch(
|
|
307
|
+
`/api/projects/${encodeURIComponent(projectId)}/media/metadata?path=${encodeURIComponent(
|
|
308
|
+
selectedAssetPath,
|
|
309
|
+
)}`,
|
|
310
|
+
{ signal: controller.signal },
|
|
311
|
+
)
|
|
312
|
+
.then(async (response) => {
|
|
313
|
+
if (!response.ok) return { ok: false as const };
|
|
314
|
+
const data: MediaMetadataResponse | null = await response.json();
|
|
315
|
+
return { ok: true as const, metadata: data?.metadata ?? null };
|
|
316
|
+
})
|
|
317
|
+
.then((result) => {
|
|
318
|
+
if (controller.signal.aborted) return;
|
|
319
|
+
// Only cache a definitive answer from a successful response — a non-OK
|
|
320
|
+
// status is a transient/server failure, not a stable "no metadata"
|
|
321
|
+
// result, and caching it would suppress the HDR banner for this asset
|
|
322
|
+
// for the page's whole lifetime. Leave the key absent so the next
|
|
323
|
+
// selection retries.
|
|
324
|
+
if (!result.ok) {
|
|
325
|
+
setMediaMetadata(null);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
MEDIA_METADATA_CACHE.set(cacheKey, result.metadata);
|
|
329
|
+
setMediaMetadata(result.metadata);
|
|
330
|
+
})
|
|
331
|
+
.catch(() => {
|
|
332
|
+
// Same reasoning as the non-OK branch above: don't cache a network-
|
|
333
|
+
// level fetch failure either.
|
|
334
|
+
if (!controller.signal.aborted) setMediaMetadata(null);
|
|
335
|
+
});
|
|
336
|
+
return () => controller.abort();
|
|
337
|
+
}, [projectId, selectedAssetPath]);
|
|
338
|
+
|
|
339
|
+
const clearStatusTimers = useCallback(() => {
|
|
340
|
+
for (const timer of statusTimersRef.current) clearTimeout(timer);
|
|
341
|
+
statusTimersRef.current = [];
|
|
342
|
+
}, []);
|
|
343
|
+
|
|
344
|
+
const scheduleRuntimeStatusRefresh = useCallback(() => {
|
|
345
|
+
clearStatusTimers();
|
|
346
|
+
statusTimersRef.current = RUNTIME_STATUS_REFRESH_DELAYS.map((delay) =>
|
|
347
|
+
window.setTimeout(refreshRuntimeStatus, delay),
|
|
348
|
+
);
|
|
349
|
+
}, [clearStatusTimers, refreshRuntimeStatus]);
|
|
350
|
+
|
|
351
|
+
useEffect(() => {
|
|
352
|
+
refreshRuntimeStatus();
|
|
353
|
+
}, [refreshRuntimeStatus]);
|
|
354
|
+
|
|
355
|
+
const persistColorGradingValue = useCallback(
|
|
356
|
+
(
|
|
357
|
+
value: string | null,
|
|
358
|
+
attemptedGrading: NormalizedHfColorGrading,
|
|
359
|
+
attemptIdentityKey: string,
|
|
360
|
+
isLatestAttempt: () => boolean,
|
|
361
|
+
setAttributeLive: (
|
|
362
|
+
attr: string,
|
|
363
|
+
value: string | null,
|
|
364
|
+
onSettled?: (ok: boolean) => void,
|
|
365
|
+
) => void | Promise<void>,
|
|
366
|
+
) => {
|
|
367
|
+
// Two guards, not one: identity (selection moved to a DIFFERENT
|
|
368
|
+
// element — that element already got its own confirmedGradingRef
|
|
369
|
+
// baseline from the reset block) and version (a NEWER edit landed on
|
|
370
|
+
// the SAME element — e.g. the user dragged Exposure, then Contrast,
|
|
371
|
+
// before Exposure's persist settled; Exposure settling afterward must
|
|
372
|
+
// not stamp confirmedGradingRef with its now-superseded value or
|
|
373
|
+
// revert `grading` out from under Contrast's newer optimistic state).
|
|
374
|
+
const applySettled = (ok: boolean) => {
|
|
375
|
+
if (identityKeyRef.current !== attemptIdentityKey) return;
|
|
376
|
+
if (!isLatestAttempt()) return;
|
|
377
|
+
if (ok) {
|
|
378
|
+
confirmedGradingRef.current = attemptedGrading;
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
// Persist failed — the optimistic grading was never actually saved.
|
|
382
|
+
// Revert to the last confirmed-good value instead of leaving the
|
|
383
|
+
// control showing an unsaved state as if it succeeded.
|
|
384
|
+
const reverted = confirmedGradingRef.current;
|
|
385
|
+
latestGradingRef.current = reverted;
|
|
386
|
+
setGrading(reverted);
|
|
387
|
+
setRuntimeStatus({ state: "unavailable", message: "Save failed — reverted" });
|
|
388
|
+
};
|
|
389
|
+
// `onSettled` is the real signal — the underlying commit runner
|
|
390
|
+
// (runDomEditCommit) intentionally swallows persist failures so a
|
|
391
|
+
// caller `await`-ing this promise never sees a rejection; a rejection
|
|
392
|
+
// handler here alone would be dead code against the actual Studio
|
|
393
|
+
// callback. The `.catch` below is a fallback for any OTHER
|
|
394
|
+
// implementation of onSetAttributeLive that rejects instead.
|
|
395
|
+
// `setAttributeLive` is passed in by the caller rather than read from a
|
|
396
|
+
// ref: a debounced persist for element A must call the callback that
|
|
397
|
+
// was live when A's edit was SCHEDULED, not whatever the ref holds by
|
|
398
|
+
// the time the timer fires — a "latest" ref would let a stale timer
|
|
399
|
+
// wrongly target a since-selected element B's callback.
|
|
400
|
+
const result = setAttributeLive(COLOR_GRADING_DATA_KEY, value ?? null, applySettled);
|
|
401
|
+
return trackStudioPendingEdit(
|
|
402
|
+
Promise.resolve(result).then(
|
|
403
|
+
() => undefined,
|
|
404
|
+
() => applySettled(false),
|
|
405
|
+
),
|
|
406
|
+
);
|
|
407
|
+
},
|
|
408
|
+
[],
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
const flushPendingPersist = useCallback(() => {
|
|
412
|
+
if (persistTimerRef.current) {
|
|
413
|
+
clearTimeout(persistTimerRef.current);
|
|
414
|
+
persistTimerRef.current = null;
|
|
415
|
+
}
|
|
416
|
+
if (pendingPersistValueRef.current === undefined) return undefined;
|
|
417
|
+
const value = pendingPersistValueRef.current;
|
|
418
|
+
const attemptedGrading = pendingPersistGradingRef.current ?? latestGradingRef.current;
|
|
419
|
+
// Snapshotted at schedule time, not identityKeyRef.current read fresh
|
|
420
|
+
// here — an async flush could otherwise tag the attempt with whatever
|
|
421
|
+
// identity is "current" by then, not the one this edit was made for.
|
|
422
|
+
const attemptIdentityKey = pendingPersistIdentityRef.current ?? identityKeyRef.current;
|
|
423
|
+
pendingPersistValueRef.current = undefined;
|
|
424
|
+
pendingPersistGradingRef.current = null;
|
|
425
|
+
pendingPersistIdentityRef.current = null;
|
|
426
|
+
// A flush cancels the pending debounce timer above, so this becomes the
|
|
427
|
+
// one-and-only in-flight attempt for this element — bump the version so
|
|
428
|
+
// it still registers as "the latest attempt" against the same guard a
|
|
429
|
+
// regular debounced commit uses, instead of unconditionally claiming
|
|
430
|
+
// that title. Without this, a newer edit landing after the flush starts
|
|
431
|
+
// but before it settles would have its own eventual settle silently
|
|
432
|
+
// lose the version race to this unconditionally-"latest" flush.
|
|
433
|
+
const isLatestAttempt = bumpDomEditCommitVersion(gradingVersionRef);
|
|
434
|
+
return persistColorGradingValue(
|
|
435
|
+
value,
|
|
436
|
+
attemptedGrading,
|
|
437
|
+
attemptIdentityKey,
|
|
438
|
+
isLatestAttempt,
|
|
439
|
+
onSetAttributeLive,
|
|
440
|
+
);
|
|
441
|
+
}, [onSetAttributeLive, persistColorGradingValue]);
|
|
442
|
+
|
|
443
|
+
useEffect(() => addStudioPendingEditFlushListener(flushPendingPersist), [flushPendingPersist]);
|
|
444
|
+
|
|
445
|
+
useEffect(() => {
|
|
446
|
+
return () => {
|
|
447
|
+
clearStatusTimers();
|
|
448
|
+
void flushPendingPersist();
|
|
449
|
+
};
|
|
450
|
+
}, [clearStatusTimers, flushPendingPersist]);
|
|
451
|
+
|
|
452
|
+
const postColorGrading = useCallback(
|
|
453
|
+
(nextGrading: NormalizedHfColorGrading) => {
|
|
454
|
+
postRuntimeControlMessage(previewIframeRef?.current?.contentWindow, "set-color-grading", {
|
|
455
|
+
target,
|
|
456
|
+
grading: toBridgeColorGrading(nextGrading),
|
|
457
|
+
});
|
|
458
|
+
},
|
|
459
|
+
[previewIframeRef, target],
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
const postCompare = useCallback(
|
|
463
|
+
(enabled: boolean) => {
|
|
464
|
+
postRuntimeControlMessage(
|
|
465
|
+
previewIframeRef?.current?.contentWindow,
|
|
466
|
+
"set-color-grading-compare",
|
|
467
|
+
{
|
|
468
|
+
target,
|
|
469
|
+
compare: { enabled, position: 1, lineWidth: 0 },
|
|
470
|
+
},
|
|
471
|
+
);
|
|
472
|
+
},
|
|
473
|
+
[previewIframeRef, target],
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
useEffect(() => {
|
|
477
|
+
const iframe = previewIframeRef?.current;
|
|
478
|
+
if (!iframe) return;
|
|
479
|
+
const refreshAndReplay = () => {
|
|
480
|
+
const nextGrading = latestGradingRef.current;
|
|
481
|
+
const active = isHfColorGradingActive(nextGrading);
|
|
482
|
+
if (active) postColorGrading(nextGrading);
|
|
483
|
+
postCompare(compareEnabledRef.current && active);
|
|
484
|
+
scheduleRuntimeStatusRefresh();
|
|
485
|
+
};
|
|
486
|
+
const onMessage = (event: MessageEvent) => {
|
|
487
|
+
if (event.source !== iframe.contentWindow) return;
|
|
488
|
+
const data = event.data as { source?: unknown; type?: unknown } | null;
|
|
489
|
+
if (data?.source !== "hf-preview" || data.type !== "ready") return;
|
|
490
|
+
if (!acceptStudioRuntimeMessage(data)) return;
|
|
491
|
+
refreshAndReplay();
|
|
492
|
+
};
|
|
493
|
+
iframe.addEventListener("load", refreshAndReplay);
|
|
494
|
+
window.addEventListener("message", onMessage);
|
|
495
|
+
const timer = window.setTimeout(refreshAndReplay, 80);
|
|
496
|
+
return () => {
|
|
497
|
+
iframe.removeEventListener("load", refreshAndReplay);
|
|
498
|
+
window.removeEventListener("message", onMessage);
|
|
499
|
+
window.clearTimeout(timer);
|
|
500
|
+
};
|
|
501
|
+
}, [postColorGrading, postCompare, previewIframeRef, scheduleRuntimeStatusRefresh]);
|
|
502
|
+
|
|
503
|
+
useEffect(
|
|
504
|
+
() => () => {
|
|
505
|
+
postCompare(false);
|
|
506
|
+
},
|
|
507
|
+
[postCompare],
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
const commitColorGrading = useCallback(
|
|
511
|
+
(nextGrading: NormalizedHfColorGrading) => {
|
|
512
|
+
setGrading(nextGrading);
|
|
513
|
+
setRuntimeStatus({ state: "pending", message: "Updating shader" });
|
|
514
|
+
postColorGrading(nextGrading);
|
|
515
|
+
const active = isHfColorGradingActive(nextGrading);
|
|
516
|
+
if (compareEnabledRef.current) {
|
|
517
|
+
postCompare(active);
|
|
518
|
+
if (!active) setCompareEnabled(false);
|
|
519
|
+
}
|
|
520
|
+
scheduleRuntimeStatusRefresh();
|
|
521
|
+
if (persistTimerRef.current) clearTimeout(persistTimerRef.current);
|
|
522
|
+
pendingPersistValueRef.current = isHfColorGradingActive(nextGrading)
|
|
523
|
+
? serializeHfColorGrading(nextGrading)
|
|
524
|
+
: null;
|
|
525
|
+
pendingPersistGradingRef.current = nextGrading;
|
|
526
|
+
pendingPersistIdentityRef.current = identityKeyRef.current;
|
|
527
|
+
// Captured now (edit time), not read fresh inside the timer — the
|
|
528
|
+
// timer fires 350ms later and may run after selection has already
|
|
529
|
+
// moved on, at which point identityKeyRef.current would no longer
|
|
530
|
+
// describe the element this edit was actually made for.
|
|
531
|
+
const attemptIdentityKey = identityKeyRef.current;
|
|
532
|
+
// Bumps a monotonic version and hands back a checker bound to THIS
|
|
533
|
+
// specific commit — reused from the same primitive the DOM-attribute
|
|
534
|
+
// commit runner uses for the identical same-target-rapid-edits race.
|
|
535
|
+
const isLatestAttempt = bumpDomEditCommitVersion(gradingVersionRef);
|
|
536
|
+
persistTimerRef.current = setTimeout(() => {
|
|
537
|
+
const value = pendingPersistValueRef.current;
|
|
538
|
+
const attemptedGrading = pendingPersistGradingRef.current ?? nextGrading;
|
|
539
|
+
pendingPersistValueRef.current = undefined;
|
|
540
|
+
pendingPersistGradingRef.current = null;
|
|
541
|
+
pendingPersistIdentityRef.current = null;
|
|
542
|
+
persistTimerRef.current = null;
|
|
543
|
+
void persistColorGradingValue(
|
|
544
|
+
value ?? null,
|
|
545
|
+
attemptedGrading,
|
|
546
|
+
attemptIdentityKey,
|
|
547
|
+
isLatestAttempt,
|
|
548
|
+
onSetAttributeLive,
|
|
549
|
+
);
|
|
550
|
+
}, 350);
|
|
551
|
+
},
|
|
552
|
+
[
|
|
553
|
+
onSetAttributeLive,
|
|
554
|
+
persistColorGradingValue,
|
|
555
|
+
postColorGrading,
|
|
556
|
+
postCompare,
|
|
557
|
+
scheduleRuntimeStatusRefresh,
|
|
558
|
+
],
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
const commitCompare = useCallback(
|
|
562
|
+
(enabled: boolean) => {
|
|
563
|
+
const nextEnabled = enabled && isHfColorGradingActive(grading);
|
|
564
|
+
setCompareEnabled(nextEnabled);
|
|
565
|
+
if (nextEnabled) postColorGrading(grading);
|
|
566
|
+
postCompare(nextEnabled);
|
|
567
|
+
scheduleRuntimeStatusRefresh();
|
|
568
|
+
},
|
|
569
|
+
[grading, postColorGrading, postCompare, scheduleRuntimeStatusRefresh],
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
const applyToScope = useCallback(async () => {
|
|
573
|
+
if (!onApplyScope || applyBusy) return;
|
|
574
|
+
setApplyBusy(true);
|
|
575
|
+
try {
|
|
576
|
+
const value = isHfColorGradingActive(grading) ? serializeHfColorGrading(grading) : null;
|
|
577
|
+
await onApplyScope(applyScope, value);
|
|
578
|
+
} finally {
|
|
579
|
+
setApplyBusy(false);
|
|
580
|
+
}
|
|
581
|
+
}, [applyBusy, applyScope, grading, onApplyScope]);
|
|
582
|
+
|
|
583
|
+
return {
|
|
584
|
+
grading,
|
|
585
|
+
compareEnabled,
|
|
586
|
+
applyScope,
|
|
587
|
+
applyBusy,
|
|
588
|
+
runtimeStatus,
|
|
589
|
+
mediaMetadata,
|
|
590
|
+
commitColorGrading,
|
|
591
|
+
commitCompare,
|
|
592
|
+
setApplyScope,
|
|
593
|
+
applyToScope,
|
|
594
|
+
resetGrading: () => commitColorGrading(defaultColorGrading()),
|
|
595
|
+
};
|
|
596
|
+
}
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
VariableValidationIssue,
|
|
7
7
|
} from "@hyperframes/sdk";
|
|
8
8
|
import type { EditHistoryKind } from "../../utils/editHistory";
|
|
9
|
+
import type { PublishSdkSession } from "../../utils/sdkCutover";
|
|
9
10
|
import { useStudioPlaybackContext, useStudioShellContext } from "../../contexts/StudioContext";
|
|
10
11
|
import { useDomEditContext } from "../../contexts/DomEditContext";
|
|
11
12
|
import { useFileManagerContext } from "../../contexts/FileManagerContext";
|
|
@@ -32,6 +33,7 @@ function shellSingleQuote(value: string): string {
|
|
|
32
33
|
|
|
33
34
|
interface VariablesPanelProps {
|
|
34
35
|
sdkSession: Composition | null;
|
|
36
|
+
publishSdkSession: PublishSdkSession;
|
|
35
37
|
reloadPreview: () => void;
|
|
36
38
|
domEditSaveTimestampRef: MutableRefObject<number>;
|
|
37
39
|
recordEdit: (entry: {
|
|
@@ -247,6 +249,7 @@ const EMPTY_STATE = (
|
|
|
247
249
|
// fallow-ignore-next-line complexity
|
|
248
250
|
export const VariablesPanel = memo(function VariablesPanel({
|
|
249
251
|
sdkSession,
|
|
252
|
+
publishSdkSession,
|
|
250
253
|
reloadPreview,
|
|
251
254
|
domEditSaveTimestampRef,
|
|
252
255
|
recordEdit,
|
|
@@ -285,6 +288,7 @@ export const VariablesPanel = memo(function VariablesPanel({
|
|
|
285
288
|
recordEdit,
|
|
286
289
|
reloadPreview,
|
|
287
290
|
domEditSaveTimestampRef,
|
|
291
|
+
publishSdkSession,
|
|
288
292
|
});
|
|
289
293
|
|
|
290
294
|
const declarations = useMemo(
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
resolveCompositionPreviewScale,
|
|
4
|
+
resolveThumbnailSeekTime,
|
|
5
|
+
syncIframePlayback,
|
|
6
|
+
} from "./CompositionsTab";
|
|
3
7
|
|
|
4
8
|
describe("resolveCompositionPreviewScale", () => {
|
|
5
9
|
it("scales a 16:9 stage to fit the composition card", () => {
|
|
@@ -50,3 +54,25 @@ describe("resolveThumbnailSeekTime", () => {
|
|
|
50
54
|
expect(resolveThumbnailSeekTime(Number.NaN)).toBe(3);
|
|
51
55
|
});
|
|
52
56
|
});
|
|
57
|
+
|
|
58
|
+
describe("syncIframePlayback", () => {
|
|
59
|
+
it("mutes a composition-card preview before playing it", () => {
|
|
60
|
+
const calls: string[] = [];
|
|
61
|
+
const postMessage = vi.fn(() => calls.push("mute"));
|
|
62
|
+
const player = {
|
|
63
|
+
play: vi.fn(() => calls.push("play")),
|
|
64
|
+
};
|
|
65
|
+
const iframe = {
|
|
66
|
+
contentWindow: { __player: player, postMessage },
|
|
67
|
+
getRootNode: () => ({}),
|
|
68
|
+
} as unknown as HTMLIFrameElement;
|
|
69
|
+
|
|
70
|
+
expect(syncIframePlayback(iframe, true)).toBe(true);
|
|
71
|
+
expect(postMessage).toHaveBeenCalledWith(
|
|
72
|
+
expect.objectContaining({ action: "set-muted", muted: true }),
|
|
73
|
+
"*",
|
|
74
|
+
);
|
|
75
|
+
expect(calls).toEqual(["mute", "play"]);
|
|
76
|
+
expect(player.play).toHaveBeenCalledOnce();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { memo, useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { setPreviewMediaMuted } from "../../player/lib/timelineIframeHelpers";
|
|
2
3
|
|
|
3
4
|
interface CompositionsTabProps {
|
|
4
5
|
projectId: string;
|
|
@@ -87,12 +88,13 @@ function resolveIframeDuration(iframe: HTMLIFrameElement | null): number | null
|
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
function syncIframePlayback(iframe: HTMLIFrameElement | null, shouldPlay: boolean): boolean {
|
|
91
|
+
export function syncIframePlayback(iframe: HTMLIFrameElement | null, shouldPlay: boolean): boolean {
|
|
91
92
|
try {
|
|
92
93
|
const player = (iframe?.contentWindow as PreviewWindow | null)?.__player;
|
|
93
94
|
if (!player) return false;
|
|
94
95
|
|
|
95
96
|
if (shouldPlay) {
|
|
97
|
+
setPreviewMediaMuted(iframe, true);
|
|
96
98
|
player.play?.();
|
|
97
99
|
return true;
|
|
98
100
|
}
|