@hyperframes/studio 0.7.37 → 0.7.39
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-BBrKzTGd.js +459 -0
- package/dist/assets/index-0P10SwC_.css +1 -0
- package/dist/assets/{index-BXYJbIax.js → index-312a3Ceu.js} +1 -1
- package/dist/assets/index-B9YvRJz1.js +396 -0
- package/dist/assets/{index-Ct-X2wlf.js → index-CQHiecE7.js} +1 -1
- package/dist/{chunk-JND3XUJL.js → chunk-RCLGSZ7C.js} +1 -1
- package/dist/chunk-RCLGSZ7C.js.map +1 -0
- package/dist/{domEditingLayers-UIQZJCOA.js → domEditingLayers-S6YOLVRG.js} +2 -2
- package/dist/index.d.ts +48 -10
- package/dist/index.html +2 -2
- package/dist/index.js +6045 -3507
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +26 -24
- package/src/components/AskAgentModal.tsx +23 -6
- package/src/components/LintModal.tsx +40 -10
- package/src/components/MediaPreview.tsx +36 -1
- package/src/components/SaveQueuePausedBanner.tsx +7 -6
- package/src/components/StudioErrorBoundary.tsx +19 -31
- package/src/components/StudioFeedbackBar.tsx +11 -2
- package/src/components/StudioGlobalDragOverlay.tsx +1 -1
- package/src/components/StudioHeader.tsx +184 -106
- package/src/components/StudioLeftSidebar.tsx +24 -3
- package/src/components/StudioOverlays.tsx +34 -8
- package/src/components/StudioPreviewArea.tsx +8 -0
- package/src/components/StudioRightPanel.tsx +160 -9
- package/src/components/StudioSplash.tsx +6 -3
- package/src/components/StudioToast.tsx +12 -11
- package/src/components/TimelineToolbar.test.tsx +9 -3
- package/src/components/TimelineToolbar.tsx +63 -29
- package/src/components/editor/DomEditCropHandles.tsx +238 -0
- package/src/components/editor/DomEditOverlay.tsx +111 -108
- package/src/components/editor/DomEditRotateHandle.tsx +41 -0
- package/src/components/editor/PropertyPanel.test.ts +48 -0
- package/src/components/editor/PropertyPanel.tsx +27 -47
- package/src/components/editor/PropertyPanelEmptyState.tsx +33 -0
- package/src/components/editor/SnapToolbar.tsx +20 -1
- package/src/components/editor/clipPathHelpers.ts +113 -0
- package/src/components/editor/colorGradingScopePatch.test.ts +74 -0
- package/src/components/editor/colorGradingScopePatch.ts +57 -0
- package/src/components/editor/domEditOverlayCrop.test.ts +106 -0
- package/src/components/editor/domEditOverlayCrop.ts +115 -0
- package/src/components/editor/domEditOverlayGeometry.ts +14 -0
- package/src/components/editor/domEditOverlayShape.ts +39 -0
- package/src/components/editor/domEditing.test.ts +2 -0
- package/src/components/editor/domEditingTypes.ts +3 -0
- package/src/components/editor/manualEditingAvailability.test.ts +0 -12
- package/src/components/editor/manualEditingAvailability.ts +0 -6
- package/src/components/editor/marqueeCommit.ts +3 -2
- package/src/components/editor/propertyPanelColorGradingControls.tsx +371 -329
- package/src/components/editor/propertyPanelColorGradingSection.tsx +274 -55
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +275 -0
- package/src/components/editor/propertyPanelHelpers.ts +32 -125
- package/src/components/editor/propertyPanelMediaSection.tsx +253 -99
- package/src/components/editor/propertyPanelStyleSections.tsx +75 -1
- package/src/components/editor/propertyPanelTypes.ts +113 -0
- package/src/components/editor/snapTargetCollection.ts +2 -3
- package/src/components/editor/useDomEditCompositionRect.ts +68 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +13 -6
- package/src/components/editor/useDomEditOverlayRects.ts +5 -3
- package/src/components/nle/CompositionBreadcrumb.tsx +3 -2
- package/src/components/nle/NLELayout.tsx +54 -51
- package/src/components/nle/NLEPreview.tsx +41 -5
- package/src/components/nle/TimelineResizeDivider.tsx +97 -0
- package/src/components/nle/usePreviewBlockDrop.ts +31 -7
- package/src/components/renders/RenderQueue.tsx +125 -24
- package/src/components/renders/RenderQueueItem.tsx +132 -64
- package/src/components/renders/useRenderQueue.ts +136 -24
- package/src/components/sidebar/AssetsTab.test.ts +87 -0
- package/src/components/sidebar/AssetsTab.tsx +113 -15
- package/src/components/sidebar/BlocksTab.tsx +2 -1
- package/src/components/sidebar/GlobalAssetsView.tsx +91 -0
- package/src/components/storyboard/StoryboardFrameFocus.tsx +39 -16
- package/src/components/storyboard/StoryboardLoaded.tsx +19 -2
- package/src/components/storyboard/StoryboardView.tsx +6 -0
- package/src/components/studioColorGradingScope.ts +124 -0
- package/src/components/studioMediaJobs.ts +123 -0
- package/src/components/ui/Button.tsx +20 -11
- package/src/components/ui/HyperframesLoader.tsx +9 -2
- package/src/components/ui/SearchInput.tsx +53 -0
- package/src/components/ui/Tooltip.tsx +52 -6
- package/src/components/ui/VideoFrameThumbnail.tsx +26 -3
- package/src/components/ui/useDialogBehavior.ts +83 -0
- package/src/contexts/PanelLayoutContext.tsx +3 -0
- package/src/contexts/StudioContext.tsx +7 -0
- package/src/hooks/domSelectionTestHarness.ts +1 -0
- package/src/hooks/useCompositionContentLoader.ts +40 -0
- package/src/hooks/useCropMode.ts +91 -0
- package/src/hooks/useDomEditCommits.test.tsx +2 -0
- package/src/hooks/useEditorSave.ts +14 -0
- package/src/hooks/useFileManager.ts +39 -25
- package/src/hooks/useFrameCapture.ts +12 -1
- package/src/hooks/usePanelLayout.ts +1 -0
- package/src/hooks/usePreviewPersistence.ts +24 -15
- package/src/hooks/useStudioContextValue.ts +5 -0
- package/src/hooks/useToast.ts +66 -13
- package/src/player/components/ShortcutsPanel.tsx +9 -0
- package/src/player/store/playerStore.ts +14 -0
- package/src/styles/studio.css +61 -0
- package/src/utils/sdkResolverShadow.test.ts +53 -3
- package/src/utils/sdkResolverShadow.ts +41 -1
- package/src/utils/studioPendingEdits.test.ts +43 -0
- package/src/utils/studioPendingEdits.ts +45 -0
- package/src/utils/studioUiPreferences.ts +4 -0
- package/dist/assets/hyperframes-player-BGuumSiM.js +0 -459
- package/dist/assets/index-DmkOvZns.css +0 -1
- package/dist/assets/index-SYSbQyK4.js +0 -396
- package/dist/chunk-JND3XUJL.js.map +0 -1
- /package/dist/{domEditingLayers-UIQZJCOA.js.map → domEditingLayers-S6YOLVRG.js.map} +0 -0
|
@@ -9,52 +9,116 @@ import {
|
|
|
9
9
|
} from "react";
|
|
10
10
|
import {
|
|
11
11
|
HF_COLOR_GRADING_ATTR,
|
|
12
|
-
HF_COLOR_GRADING_COLOR_SPACE,
|
|
13
12
|
isHfColorGradingActive,
|
|
14
13
|
normalizeHfColorGrading,
|
|
15
14
|
serializeHfColorGrading,
|
|
16
|
-
type HfColorGradingAdjustKey,
|
|
17
15
|
type HfColorGradingTarget,
|
|
18
16
|
type NormalizedHfColorGrading,
|
|
19
17
|
} from "@hyperframes/core/color-grading";
|
|
20
18
|
import { Compare, Palette, RotateCcw } from "../../icons/SystemIcons";
|
|
19
|
+
import {
|
|
20
|
+
addStudioPendingEditFlushListener,
|
|
21
|
+
trackStudioPendingEdit,
|
|
22
|
+
} from "../../utils/studioPendingEdits";
|
|
21
23
|
import type { DomEditSelection } from "./domEditing";
|
|
22
24
|
import { ColorGradingControls } from "./propertyPanelColorGradingControls";
|
|
25
|
+
import { stripQueryAndHash } from "./propertyPanelHelpers";
|
|
23
26
|
import { Section } from "./propertyPanelPrimitives";
|
|
24
27
|
|
|
25
|
-
const DEFAULT_ADJUST: Record<HfColorGradingAdjustKey, number> = {
|
|
26
|
-
exposure: 0,
|
|
27
|
-
contrast: 0,
|
|
28
|
-
highlights: 0,
|
|
29
|
-
shadows: 0,
|
|
30
|
-
whites: 0,
|
|
31
|
-
blacks: 0,
|
|
32
|
-
temperature: 0,
|
|
33
|
-
tint: 0,
|
|
34
|
-
saturation: 0,
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const DEFAULT_COLOR_GRADING: NormalizedHfColorGrading = {
|
|
38
|
-
enabled: true,
|
|
39
|
-
preset: "neutral",
|
|
40
|
-
intensity: 1,
|
|
41
|
-
adjust: DEFAULT_ADJUST,
|
|
42
|
-
lut: null,
|
|
43
|
-
colorSpace: HF_COLOR_GRADING_COLOR_SPACE,
|
|
44
|
-
};
|
|
45
|
-
|
|
46
28
|
const COLOR_GRADING_DATA_KEY = HF_COLOR_GRADING_ATTR.replace(/^data-/, "");
|
|
29
|
+
const RUNTIME_STATUS_REFRESH_DELAYS = [50, 250, 1000, 2500] as const;
|
|
30
|
+
const MEDIA_METADATA_CACHE = new Map<string, MediaMetadata | null>();
|
|
47
31
|
|
|
48
32
|
interface RuntimeColorGradingStatus {
|
|
49
33
|
state: "missing" | "inactive" | "pending" | "active" | "unavailable";
|
|
50
34
|
message: string;
|
|
51
35
|
}
|
|
52
36
|
|
|
37
|
+
interface MediaMetadata {
|
|
38
|
+
kind: "video" | "image" | "audio" | "unknown";
|
|
39
|
+
color: {
|
|
40
|
+
dynamicRange: "hdr" | "sdr" | "unknown";
|
|
41
|
+
hdrTransfer: "pq" | "hlg" | "unknown" | null;
|
|
42
|
+
label: string;
|
|
43
|
+
isHdr: boolean;
|
|
44
|
+
codecName?: string;
|
|
45
|
+
profile?: string;
|
|
46
|
+
pixelFormat?: string;
|
|
47
|
+
colorSpace?: string;
|
|
48
|
+
colorTransfer?: string;
|
|
49
|
+
colorPrimaries?: string;
|
|
50
|
+
};
|
|
51
|
+
probeError?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface MediaMetadataResponse {
|
|
55
|
+
path: string;
|
|
56
|
+
metadata: MediaMetadata;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function stripPreviewAssetPath(src: string, projectId: string): string | null {
|
|
60
|
+
let pathname = src;
|
|
61
|
+
try {
|
|
62
|
+
pathname = new URL(src, window.location.href).pathname;
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const projectMarker = `/api/projects/${encodeURIComponent(projectId)}/preview/`;
|
|
67
|
+
const genericMarker = "/preview/";
|
|
68
|
+
const marker = pathname.includes(projectMarker) ? projectMarker : genericMarker;
|
|
69
|
+
const index = pathname.indexOf(marker);
|
|
70
|
+
if (index < 0) return null;
|
|
71
|
+
const assetPath = decodeURIComponent(pathname.slice(index + marker.length)).replace(/^\/+/, "");
|
|
72
|
+
if (!assetPath || assetPath.startsWith("comp/")) return null;
|
|
73
|
+
return assetPath;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// fallow-ignore-next-line complexity
|
|
77
|
+
function resolveProjectAssetPath(
|
|
78
|
+
sourceFile: string,
|
|
79
|
+
src: string,
|
|
80
|
+
projectId: string,
|
|
81
|
+
): string | null {
|
|
82
|
+
const trimmed = stripQueryAndHash(src.trim());
|
|
83
|
+
if (!trimmed || /^(?:data:|blob:)/i.test(trimmed)) return null;
|
|
84
|
+
if (/^https?:\/\//i.test(trimmed)) return stripPreviewAssetPath(trimmed, projectId);
|
|
85
|
+
if (trimmed.startsWith("/")) {
|
|
86
|
+
return stripPreviewAssetPath(trimmed, projectId);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const sourceDir = sourceFile.includes("/")
|
|
90
|
+
? sourceFile.slice(0, sourceFile.lastIndexOf("/"))
|
|
91
|
+
: "";
|
|
92
|
+
const parts = `${sourceDir}/${trimmed}`.split("/");
|
|
93
|
+
const normalized: string[] = [];
|
|
94
|
+
for (const part of parts) {
|
|
95
|
+
if (!part || part === ".") continue;
|
|
96
|
+
if (part === "..") {
|
|
97
|
+
normalized.pop();
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
normalized.push(part);
|
|
101
|
+
}
|
|
102
|
+
return normalized.join("/") || null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function selectedMediaAssetPath(element: DomEditSelection, projectId: string): string | null {
|
|
106
|
+
if (element.tagName !== "video" && element.tagName !== "img") return null;
|
|
107
|
+
const media = element.element as HTMLImageElement | HTMLVideoElement;
|
|
108
|
+
const src = media.getAttribute("src") || media.currentSrc || "";
|
|
109
|
+
return resolveProjectAssetPath(element.sourceFile || "index.html", src, projectId);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function defaultColorGrading(): NormalizedHfColorGrading {
|
|
113
|
+
const grading = normalizeHfColorGrading("neutral");
|
|
114
|
+
if (!grading) throw new Error("Missing neutral color grading preset");
|
|
115
|
+
return grading;
|
|
116
|
+
}
|
|
117
|
+
|
|
53
118
|
function readColorGradingFromElement(element: DomEditSelection): NormalizedHfColorGrading {
|
|
54
|
-
|
|
55
|
-
normalizeHfColorGrading(element.dataAttributes[COLOR_GRADING_DATA_KEY]) ??
|
|
56
|
-
|
|
57
|
-
return { ...grading, intensity: 1 };
|
|
119
|
+
return (
|
|
120
|
+
normalizeHfColorGrading(element.dataAttributes[COLOR_GRADING_DATA_KEY]) ?? defaultColorGrading()
|
|
121
|
+
);
|
|
58
122
|
}
|
|
59
123
|
|
|
60
124
|
function toBridgeColorGrading(grading: NormalizedHfColorGrading): unknown {
|
|
@@ -97,13 +161,45 @@ function StatusPill({ status }: { status: RuntimeColorGradingStatus }) {
|
|
|
97
161
|
? "bg-red-400"
|
|
98
162
|
: "bg-panel-text-5";
|
|
99
163
|
return (
|
|
100
|
-
<div
|
|
164
|
+
<div
|
|
165
|
+
className="flex min-w-0 items-center gap-1.5 rounded bg-panel-input px-2 py-1 text-[10px] font-medium text-panel-text-3"
|
|
166
|
+
title={status.message}
|
|
167
|
+
>
|
|
101
168
|
<span className={`h-1.5 w-1.5 flex-shrink-0 rounded-full ${dotClass}`} />
|
|
102
169
|
<span className="truncate">{status.message}</span>
|
|
103
170
|
</div>
|
|
104
171
|
);
|
|
105
172
|
}
|
|
106
173
|
|
|
174
|
+
function HdrMediaWarning({ metadata }: { metadata: MediaMetadata | null }) {
|
|
175
|
+
if (metadata?.color.dynamicRange !== "hdr") return null;
|
|
176
|
+
const details = [
|
|
177
|
+
metadata.color.codecName,
|
|
178
|
+
metadata.color.profile,
|
|
179
|
+
metadata.color.pixelFormat,
|
|
180
|
+
metadata.color.colorPrimaries,
|
|
181
|
+
metadata.color.colorTransfer,
|
|
182
|
+
]
|
|
183
|
+
.filter(Boolean)
|
|
184
|
+
.join(" · ");
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<div className="mb-3 rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-[11px] leading-4 text-amber-100">
|
|
188
|
+
<div className="mb-1 flex min-w-0 items-center justify-between gap-2">
|
|
189
|
+
<span className="font-semibold">{metadata.color.label} source</span>
|
|
190
|
+
<span className="rounded bg-amber-400/20 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide text-amber-100">
|
|
191
|
+
SDR preview
|
|
192
|
+
</span>
|
|
193
|
+
</div>
|
|
194
|
+
<p className="text-amber-100/80">
|
|
195
|
+
These controls use the current SDR shader preview path. Render may stay HDR-tagged, but this
|
|
196
|
+
is not true HDR color grading yet.
|
|
197
|
+
</p>
|
|
198
|
+
{details && <p className="mt-1 truncate text-[10px] text-amber-100/55">{details}</p>}
|
|
199
|
+
</div>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
107
203
|
function HoldBeforeButton({
|
|
108
204
|
active,
|
|
109
205
|
disabled,
|
|
@@ -170,29 +266,46 @@ function HoldBeforeButton({
|
|
|
170
266
|
}
|
|
171
267
|
|
|
172
268
|
export function ColorGradingSection({
|
|
269
|
+
projectId,
|
|
173
270
|
element,
|
|
174
271
|
assets,
|
|
175
272
|
previewIframeRef,
|
|
176
273
|
onImportAssets,
|
|
177
274
|
onSetAttributeLive,
|
|
275
|
+
onApplyScope,
|
|
178
276
|
}: {
|
|
277
|
+
projectId: string;
|
|
179
278
|
element: DomEditSelection;
|
|
180
279
|
assets: string[];
|
|
181
280
|
previewIframeRef?: RefObject<HTMLIFrameElement | null>;
|
|
182
281
|
onImportAssets?: (files: FileList, dir?: string) => Promise<string[]>;
|
|
183
282
|
onSetAttributeLive: (attr: string, value: string | null) => void | Promise<void>;
|
|
283
|
+
onApplyScope?: (
|
|
284
|
+
scope: "source-file" | "project",
|
|
285
|
+
value: string | null,
|
|
286
|
+
) => Promise<{ changedFiles: number; changedElements: number }>;
|
|
184
287
|
}) {
|
|
185
288
|
const [grading, setGrading] = useState(() => readColorGradingFromElement(element));
|
|
186
289
|
const [compareEnabled, setCompareEnabled] = useState(false);
|
|
290
|
+
const [applyScope, setApplyScope] = useState<"source-file" | "project">("source-file");
|
|
291
|
+
const [applyBusy, setApplyBusy] = useState(false);
|
|
187
292
|
const [runtimeStatus, setRuntimeStatus] = useState<RuntimeColorGradingStatus>(() => ({
|
|
188
293
|
state: "pending",
|
|
189
294
|
message: "Waiting for runtime",
|
|
190
295
|
}));
|
|
296
|
+
const selectedAssetPath = useMemo(
|
|
297
|
+
() => selectedMediaAssetPath(element, projectId),
|
|
298
|
+
[element, projectId],
|
|
299
|
+
);
|
|
300
|
+
const [mediaMetadata, setMediaMetadata] = useState<MediaMetadata | null>(null);
|
|
191
301
|
const persistTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
192
302
|
const pendingPersistValueRef = useRef<string | null | undefined>(undefined);
|
|
303
|
+
const statusTimersRef = useRef<number[]>([]);
|
|
193
304
|
const onSetAttributeLiveRef = useRef(onSetAttributeLive);
|
|
305
|
+
const latestGradingRef = useRef(grading);
|
|
194
306
|
const compareEnabledRef = useRef(compareEnabled);
|
|
195
307
|
onSetAttributeLiveRef.current = onSetAttributeLive;
|
|
308
|
+
latestGradingRef.current = grading;
|
|
196
309
|
compareEnabledRef.current = compareEnabled;
|
|
197
310
|
const target = useMemo(
|
|
198
311
|
(): HfColorGradingTarget => ({
|
|
@@ -208,33 +321,75 @@ export function ColorGradingSection({
|
|
|
208
321
|
setRuntimeStatus(readRuntimeColorGradingStatus(previewIframeRef?.current, target));
|
|
209
322
|
}, [previewIframeRef, target]);
|
|
210
323
|
|
|
324
|
+
useEffect(() => {
|
|
325
|
+
setMediaMetadata(null);
|
|
326
|
+
if (!selectedAssetPath) return;
|
|
327
|
+
const cacheKey = `${projectId}:${selectedAssetPath}`;
|
|
328
|
+
if (MEDIA_METADATA_CACHE.has(cacheKey)) {
|
|
329
|
+
setMediaMetadata(MEDIA_METADATA_CACHE.get(cacheKey) ?? null);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const controller = new AbortController();
|
|
333
|
+
fetch(
|
|
334
|
+
`/api/projects/${encodeURIComponent(projectId)}/media/metadata?path=${encodeURIComponent(
|
|
335
|
+
selectedAssetPath,
|
|
336
|
+
)}`,
|
|
337
|
+
{ signal: controller.signal },
|
|
338
|
+
)
|
|
339
|
+
.then((response) => (response.ok ? response.json() : null))
|
|
340
|
+
.then((data: MediaMetadataResponse | null) => {
|
|
341
|
+
if (controller.signal.aborted) return;
|
|
342
|
+
const metadata = data?.metadata ?? null;
|
|
343
|
+
MEDIA_METADATA_CACHE.set(cacheKey, metadata);
|
|
344
|
+
setMediaMetadata(metadata);
|
|
345
|
+
})
|
|
346
|
+
.catch(() => {
|
|
347
|
+
if (!controller.signal.aborted) MEDIA_METADATA_CACHE.set(cacheKey, null);
|
|
348
|
+
});
|
|
349
|
+
return () => controller.abort();
|
|
350
|
+
}, [projectId, selectedAssetPath]);
|
|
351
|
+
|
|
352
|
+
const clearStatusTimers = useCallback(() => {
|
|
353
|
+
for (const timer of statusTimersRef.current) clearTimeout(timer);
|
|
354
|
+
statusTimersRef.current = [];
|
|
355
|
+
}, []);
|
|
356
|
+
|
|
357
|
+
const scheduleRuntimeStatusRefresh = useCallback(() => {
|
|
358
|
+
clearStatusTimers();
|
|
359
|
+
statusTimersRef.current = RUNTIME_STATUS_REFRESH_DELAYS.map((delay) =>
|
|
360
|
+
window.setTimeout(refreshRuntimeStatus, delay),
|
|
361
|
+
);
|
|
362
|
+
}, [clearStatusTimers, refreshRuntimeStatus]);
|
|
363
|
+
|
|
211
364
|
useEffect(() => {
|
|
212
365
|
refreshRuntimeStatus();
|
|
213
366
|
}, [refreshRuntimeStatus]);
|
|
214
367
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
368
|
+
const persistColorGradingValue = useCallback((value: string | null) => {
|
|
369
|
+
return trackStudioPendingEdit(
|
|
370
|
+
onSetAttributeLiveRef.current(COLOR_GRADING_DATA_KEY, value ?? null),
|
|
371
|
+
);
|
|
372
|
+
}, []);
|
|
373
|
+
|
|
374
|
+
const flushPendingPersist = useCallback(() => {
|
|
375
|
+
if (persistTimerRef.current) {
|
|
376
|
+
clearTimeout(persistTimerRef.current);
|
|
377
|
+
persistTimerRef.current = null;
|
|
378
|
+
}
|
|
379
|
+
if (pendingPersistValueRef.current === undefined) return undefined;
|
|
380
|
+
const value = pendingPersistValueRef.current;
|
|
381
|
+
pendingPersistValueRef.current = undefined;
|
|
382
|
+
return persistColorGradingValue(value);
|
|
383
|
+
}, [persistColorGradingValue]);
|
|
384
|
+
|
|
385
|
+
useEffect(() => addStudioPendingEditFlushListener(flushPendingPersist), [flushPendingPersist]);
|
|
228
386
|
|
|
229
387
|
useEffect(() => {
|
|
230
388
|
return () => {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
void onSetAttributeLiveRef.current(COLOR_GRADING_DATA_KEY, pendingPersistValueRef.current);
|
|
234
|
-
pendingPersistValueRef.current = undefined;
|
|
235
|
-
}
|
|
389
|
+
clearStatusTimers();
|
|
390
|
+
void flushPendingPersist();
|
|
236
391
|
};
|
|
237
|
-
}, []);
|
|
392
|
+
}, [clearStatusTimers, flushPendingPersist]);
|
|
238
393
|
|
|
239
394
|
const postColorGrading = useCallback(
|
|
240
395
|
(nextGrading: NormalizedHfColorGrading) => {
|
|
@@ -272,6 +427,31 @@ export function ColorGradingSection({
|
|
|
272
427
|
[previewIframeRef, target],
|
|
273
428
|
);
|
|
274
429
|
|
|
430
|
+
useEffect(() => {
|
|
431
|
+
const iframe = previewIframeRef?.current;
|
|
432
|
+
if (!iframe) return;
|
|
433
|
+
const refreshAndReplay = () => {
|
|
434
|
+
const nextGrading = latestGradingRef.current;
|
|
435
|
+
const active = isHfColorGradingActive(nextGrading);
|
|
436
|
+
if (active) postColorGrading(nextGrading);
|
|
437
|
+
postCompare(compareEnabledRef.current && active);
|
|
438
|
+
scheduleRuntimeStatusRefresh();
|
|
439
|
+
};
|
|
440
|
+
const onMessage = (event: MessageEvent) => {
|
|
441
|
+
if (event.source !== iframe.contentWindow) return;
|
|
442
|
+
const data = event.data as { source?: unknown; type?: unknown } | null;
|
|
443
|
+
if (data?.source === "hf-preview" && data.type === "ready") refreshAndReplay();
|
|
444
|
+
};
|
|
445
|
+
iframe.addEventListener("load", refreshAndReplay);
|
|
446
|
+
window.addEventListener("message", onMessage);
|
|
447
|
+
const timer = window.setTimeout(refreshAndReplay, 80);
|
|
448
|
+
return () => {
|
|
449
|
+
iframe.removeEventListener("load", refreshAndReplay);
|
|
450
|
+
window.removeEventListener("message", onMessage);
|
|
451
|
+
window.clearTimeout(timer);
|
|
452
|
+
};
|
|
453
|
+
}, [postColorGrading, postCompare, previewIframeRef, scheduleRuntimeStatusRefresh]);
|
|
454
|
+
|
|
275
455
|
useEffect(
|
|
276
456
|
() => () => {
|
|
277
457
|
postCompare(false);
|
|
@@ -289,7 +469,7 @@ export function ColorGradingSection({
|
|
|
289
469
|
postCompare(active);
|
|
290
470
|
if (!active) setCompareEnabled(false);
|
|
291
471
|
}
|
|
292
|
-
|
|
472
|
+
scheduleRuntimeStatusRefresh();
|
|
293
473
|
if (persistTimerRef.current) clearTimeout(persistTimerRef.current);
|
|
294
474
|
pendingPersistValueRef.current = isHfColorGradingActive(nextGrading)
|
|
295
475
|
? serializeHfColorGrading(nextGrading)
|
|
@@ -297,10 +477,11 @@ export function ColorGradingSection({
|
|
|
297
477
|
persistTimerRef.current = setTimeout(() => {
|
|
298
478
|
const value = pendingPersistValueRef.current;
|
|
299
479
|
pendingPersistValueRef.current = undefined;
|
|
300
|
-
|
|
480
|
+
persistTimerRef.current = null;
|
|
481
|
+
void persistColorGradingValue(value ?? null);
|
|
301
482
|
}, 350);
|
|
302
483
|
},
|
|
303
|
-
[
|
|
484
|
+
[persistColorGradingValue, postColorGrading, postCompare, scheduleRuntimeStatusRefresh],
|
|
304
485
|
);
|
|
305
486
|
|
|
306
487
|
const commitCompare = useCallback(
|
|
@@ -309,14 +490,25 @@ export function ColorGradingSection({
|
|
|
309
490
|
setCompareEnabled(nextEnabled);
|
|
310
491
|
if (nextEnabled) postColorGrading(grading);
|
|
311
492
|
postCompare(nextEnabled);
|
|
312
|
-
|
|
493
|
+
scheduleRuntimeStatusRefresh();
|
|
313
494
|
},
|
|
314
|
-
[grading, postColorGrading, postCompare,
|
|
495
|
+
[grading, postColorGrading, postCompare, scheduleRuntimeStatusRefresh],
|
|
315
496
|
);
|
|
316
497
|
|
|
498
|
+
const applyToScope = useCallback(async () => {
|
|
499
|
+
if (!onApplyScope || applyBusy) return;
|
|
500
|
+
setApplyBusy(true);
|
|
501
|
+
try {
|
|
502
|
+
const value = isHfColorGradingActive(grading) ? serializeHfColorGrading(grading) : null;
|
|
503
|
+
await onApplyScope(applyScope, value);
|
|
504
|
+
} finally {
|
|
505
|
+
setApplyBusy(false);
|
|
506
|
+
}
|
|
507
|
+
}, [applyBusy, applyScope, grading, onApplyScope]);
|
|
508
|
+
|
|
317
509
|
return (
|
|
318
510
|
<Section
|
|
319
|
-
title="Color
|
|
511
|
+
title="Color grading"
|
|
320
512
|
icon={<Palette size={15} />}
|
|
321
513
|
accessory={
|
|
322
514
|
<div className="flex min-w-0 items-center gap-1.5">
|
|
@@ -330,22 +522,49 @@ export function ColorGradingSection({
|
|
|
330
522
|
type="button"
|
|
331
523
|
onClick={(event) => {
|
|
332
524
|
event.stopPropagation();
|
|
333
|
-
commitColorGrading(
|
|
525
|
+
commitColorGrading(defaultColorGrading());
|
|
334
526
|
}}
|
|
335
527
|
className="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded text-panel-text-4 transition-colors hover:bg-panel-hover hover:text-panel-text-1"
|
|
336
|
-
title="Reset grading"
|
|
528
|
+
title="Reset color grading"
|
|
337
529
|
>
|
|
338
530
|
<RotateCcw size={12} />
|
|
339
531
|
</button>
|
|
340
532
|
</div>
|
|
341
533
|
}
|
|
342
534
|
>
|
|
535
|
+
<HdrMediaWarning metadata={mediaMetadata} />
|
|
343
536
|
<ColorGradingControls
|
|
344
537
|
grading={grading}
|
|
345
538
|
assets={assets}
|
|
346
539
|
onImportAssets={onImportAssets}
|
|
347
540
|
onCommitColorGrading={commitColorGrading}
|
|
348
541
|
/>
|
|
542
|
+
{onApplyScope && (
|
|
543
|
+
<div className="mt-4 grid min-w-0 grid-cols-[minmax(0,1fr)_auto] gap-2">
|
|
544
|
+
<select
|
|
545
|
+
value={applyScope}
|
|
546
|
+
onChange={(event) => setApplyScope(event.currentTarget.value as typeof applyScope)}
|
|
547
|
+
disabled={applyBusy}
|
|
548
|
+
className="w-full min-w-0 rounded-md bg-panel-input px-3 py-2 text-[11px] font-medium text-panel-text-1 outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
549
|
+
title="Choose where to copy these color grading settings"
|
|
550
|
+
>
|
|
551
|
+
<option value="source-file">Current file media</option>
|
|
552
|
+
<option value="project">All project media</option>
|
|
553
|
+
</select>
|
|
554
|
+
<button
|
|
555
|
+
type="button"
|
|
556
|
+
disabled={applyBusy}
|
|
557
|
+
onClick={(event) => {
|
|
558
|
+
event.stopPropagation();
|
|
559
|
+
void applyToScope();
|
|
560
|
+
}}
|
|
561
|
+
className="h-8 rounded-md bg-panel-input px-3 text-[11px] font-medium text-panel-text-2 transition-colors hover:bg-panel-hover hover:text-panel-text-1 disabled:cursor-not-allowed disabled:opacity-50"
|
|
562
|
+
title="Copy these color grading settings to the selected scope"
|
|
563
|
+
>
|
|
564
|
+
{applyBusy ? "Applying" : "Apply"}
|
|
565
|
+
</button>
|
|
566
|
+
</div>
|
|
567
|
+
)}
|
|
349
568
|
</Section>
|
|
350
569
|
);
|
|
351
570
|
}
|