@hyperframes/studio 0.8.115 → 0.8.117
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-C-9N8nk0.js → hyperframes-player-5fcgSstJ.js} +1 -1
- package/dist/assets/{index-CAvZ0AA-.js → index-C5qKdpgl.js} +205 -203
- package/dist/assets/{index-BoUM6Xqb.js → index-XJSawwYe.js} +1 -1
- package/dist/assets/{index-CVuorIMv.js → index-tg2LXUeu.js} +1 -1
- package/dist/{chunk-TXZMMOMA.js → chunk-3F47EFCT.js} +100 -2
- package/dist/chunk-3F47EFCT.js.map +1 -0
- package/dist/{domEditingLayers-D7VYFJJD.js → domEditingLayers-OL7GLSS4.js} +2 -2
- package/dist/index.d.ts +132 -41
- package/dist/index.html +1 -1
- package/dist/index.js +1184 -1084
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/captions/hooks/useCaptionSync.ts +10 -6
- package/src/components/ProjectUnreachableBanner.tsx +3 -3
- package/src/components/editor/DomEditOverlay.tsx +1 -0
- package/src/components/editor/domEditOverlayGestures.ts +1 -0
- package/src/components/editor/domEditOverlayStartGesture.ts +2 -2
- package/src/components/editor/domGroupMoveBy.test.tsx +252 -0
- package/src/components/editor/domGroupMoveBy.ts +95 -0
- package/src/components/editor/dragUndoPaint.test.ts +5 -5
- package/src/components/editor/gestureHoldsPreviewReload.test.tsx +33 -0
- package/src/components/editor/manualEditingAvailability.ts +8 -0
- package/src/components/editor/manualOffsetDrag.ts +8 -9
- package/src/components/editor/motionPathSelection.test.ts +6 -31
- package/src/components/editor/plainTranslate.ts +5 -3
- package/src/components/editor/probeSourceElement.ts +2 -1
- package/src/components/editor/propertyPanelFont.tsx +3 -2
- package/src/components/editor/propertyPanelHelpers.ts +2 -2
- package/src/components/editor/propertyPanelTransformCommit.ts +2 -2
- package/src/components/editor/rotationDraft.ts +2 -1
- package/src/components/editor/useColorGradingController.ts +2 -1
- package/src/components/editor/useDomEditOverlayGestures.ts +2 -2
- package/src/components/editor/useFxCarveNodes.ts +2 -1
- package/src/components/editor/useFxLevelling.ts +2 -1
- package/src/components/feedback/projectProvenance.ts +2 -1
- package/src/components/nle/NLEContext.test.ts +1 -0
- package/src/components/nle/NLEContext.tsx +2 -1
- package/src/components/nle/NLEPreview.tsx +12 -10
- package/src/components/nle/PreviewPoster.tsx +2 -1
- package/src/components/renders/useFfmpegStatus.ts +2 -1
- package/src/components/renders/useRenderQueue.ts +5 -4
- package/src/components/sidebar/AssetsTab.tsx +2 -1
- package/src/components/sidebar/GlobalAssetsView.tsx +2 -1
- package/src/contexts/DomEditContext.tsx +4 -0
- package/src/hooks/domSelectionTestHarness.ts +23 -0
- package/src/hooks/domStyleCommit.propertyNames.test.ts +73 -0
- package/src/hooks/domStyleCommit.ts +8 -1
- package/src/hooks/draggedGsapPosition.ts +10 -0
- package/src/hooks/gsapDragCommit.test.ts +75 -164
- package/src/hooks/gsapDragCommit.ts +2 -105
- package/src/hooks/gsapDragPositionCommit.ts +30 -393
- package/src/hooks/gsapEditOutcome.ts +22 -2
- package/src/hooks/gsapGroupPreflight.ts +34 -0
- package/src/hooks/gsapMutationClient.ts +4 -3
- package/src/hooks/gsapParsedTween.test-helpers.ts +75 -0
- package/src/hooks/gsapParsedTween.test.ts +62 -0
- package/src/hooks/gsapParsedTween.ts +243 -0
- package/src/hooks/gsapPositionDetection.test.ts +78 -0
- package/src/hooks/gsapPositionDetection.ts +52 -33
- package/src/hooks/gsapResizeDropPoint.test.ts +4 -3
- package/src/hooks/gsapResizeElementSize.test.tsx +1 -0
- package/src/hooks/gsapResizeGeometrySweep.test.ts +13 -25
- package/src/hooks/gsapResizeIntercept.test.ts +63 -11
- package/src/hooks/gsapResizeIntercept.ts +79 -169
- package/src/hooks/gsapResizeSweep.test.ts +16 -33
- package/src/hooks/gsapRuntimeBridge.sharedTween.test.ts +6 -8
- package/src/hooks/gsapRuntimeBridge.test.ts +41 -6
- package/src/hooks/gsapRuntimeBridge.ts +105 -131
- package/src/hooks/gsapRuntimeKeyframes.ts +16 -3
- package/src/hooks/gsapScriptCommitHelpers.ts +2 -1
- package/src/hooks/gsapScriptCommitTypes.ts +9 -1
- package/src/hooks/gsapShared.ts +7 -0
- package/src/hooks/gsapShared.writeTarget.test.ts +26 -58
- package/src/hooks/gsapValueAtPlayhead.browser.test.ts +83 -0
- package/src/hooks/gsapValueAtPlayhead.realGsap.test.ts +179 -0
- package/src/hooks/gsapValueAtPlayhead.test.ts +552 -0
- package/src/hooks/gsapValueAtPlayhead.ts +292 -0
- package/src/hooks/gsapWholePropertyOffsetCommit.test.ts +43 -0
- package/src/hooks/gsapWholePropertyOffsetCommit.ts +2 -3
- package/src/hooks/keyframeCacheAstLoad.ts +2 -1
- package/src/hooks/newTweenTarget.test.ts +41 -76
- package/src/hooks/plainRotation.ts +5 -1
- package/src/hooks/timelineTimingSync.test.ts +1 -0
- package/src/hooks/timelineTimingSync.ts +2 -1
- package/src/hooks/useAskAgentModal.ts +2 -1
- package/src/hooks/useBlockCatalog.ts +2 -1
- package/src/hooks/useCaptionDetection.ts +2 -1
- package/src/hooks/useCompositionContentLoader.ts +2 -1
- package/src/hooks/useDomEditCommitsHelpers.ts +3 -2
- package/src/hooks/useDomEditPersist.test.tsx +335 -0
- package/src/hooks/useDomEditSession.test.tsx +30 -1
- package/src/hooks/useDomEditSession.ts +10 -0
- package/src/hooks/useDomEditTextCommits.test.tsx +23 -0
- package/src/hooks/useDomEditTextCommits.ts +8 -2
- package/src/hooks/useDomGeometryCommit.test.tsx +5 -1
- package/src/hooks/useDomGeometryCommit.ts +7 -9
- package/src/hooks/useEditHistoryActions.paint.test.tsx +82 -20
- package/src/hooks/useEditHistoryActions.test.tsx +15 -0
- package/src/hooks/useEditHistoryActions.ts +16 -5
- package/src/hooks/useElementLifecycleOps.ts +2 -1
- package/src/hooks/useEnableKeyframes.test.ts +32 -1
- package/src/hooks/useEnableKeyframes.ts +2 -1
- package/src/hooks/useExternalFileChangeCoordinator.test.tsx +19 -0
- package/src/hooks/useExternalFileChangeCoordinator.ts +4 -2
- package/src/hooks/useFileManager.projectOwnership.test.tsx +6 -1
- package/src/hooks/useFileManager.ts +14 -10
- package/src/hooks/useFileTree.ts +3 -2
- package/src/hooks/useFrameCapture.ts +5 -1
- package/src/hooks/useFreezeFrame.test.ts +39 -3
- package/src/hooks/useFreezeFrame.ts +7 -3
- package/src/hooks/useGroupCommits.ts +2 -1
- package/src/hooks/useGsapAwareEditing.groupPlan.test.tsx +160 -0
- package/src/hooks/useGsapAwareEditing.test.tsx +42 -29
- package/src/hooks/useGsapAwareEditing.testHelpers.tsx +42 -0
- package/src/hooks/useGsapAwareEditing.ts +17 -8
- package/src/hooks/useGsapAwareGroupMove.ts +18 -18
- package/src/hooks/useGsapInteractionFailureTelemetry.ts +10 -4
- package/src/hooks/useGsapScriptCommits.test.tsx +72 -2
- package/src/hooks/useGsapScriptCommits.ts +21 -17
- package/src/hooks/useLintModal.ts +2 -1
- package/src/hooks/usePersistentEditHistory.test.ts +302 -3
- package/src/hooks/usePersistentEditHistory.ts +50 -17
- package/src/hooks/usePreviewPersistence.history.test.tsx +14 -3
- package/src/hooks/usePreviewPersistence.ts +13 -11
- package/src/hooks/useProjectFileWriter.ts +20 -19
- package/src/hooks/useRemoveBackground.ts +2 -1
- package/src/hooks/useSdkSession.ts +2 -1
- package/src/hooks/useServerConnection.ts +4 -3
- package/src/hooks/useStudioSelectionPublisher.ts +2 -1
- package/src/hooks/useThumbnailStill.test.ts +100 -0
- package/src/hooks/useThumbnailStill.ts +37 -0
- package/src/hooks/useTimelineDeleteOps.ts +2 -1
- package/src/index.ts +4 -0
- package/src/player/components/AudioWaveform.tsx +3 -2
- package/src/player/components/CompositionThumbnail.tsx +2 -1
- package/src/player/components/clipMenuNormalize.ts +2 -1
- package/src/player/components/clipPeakMap.ts +2 -1
- package/src/player/hooks/useTimelinePlayer.ts +5 -1
- package/src/player/lib/thumbnailScheduler.test.ts +16 -0
- package/src/player/lib/thumbnailScheduler.ts +10 -2
- package/src/player/sceneSwap.ts +2 -1
- package/src/telemetry/client.ts +2 -1
- package/src/utils/blockInstaller.ts +2 -1
- package/src/utils/elementGsap.test.ts +9 -0
- package/src/utils/elementGsap.ts +7 -0
- package/src/utils/gsapSoftReload.test.ts +39 -0
- package/src/utils/gsapSoftReload.ts +9 -14
- package/src/utils/gsapUndoRestore.browser.test.ts +94 -41
- package/src/utils/gsapUndoRestore.ts +3 -6
- package/src/utils/previewPrefetch.ts +2 -1
- package/src/utils/razorSplitTransaction.ts +12 -8
- package/src/utils/softReloadTargets.ts +5 -6
- package/src/utils/studioApiFetch.test.ts +75 -0
- package/src/utils/studioApiFetch.ts +13 -0
- package/src/utils/studioFileHistory.ts +5 -1
- package/src/utils/studioHelpers.ts +7 -1
- package/src/utils/studioPendingEdits.test.ts +23 -6
- package/src/utils/studioPendingEdits.ts +32 -8
- package/src/utils/studioTelemetry.ts +2 -1
- package/src/utils/timelineCompositionInsert.ts +3 -2
- package/src/webmcp/StudioAgentTools.tsx +2 -1
- package/dist/chunk-TXZMMOMA.js.map +0 -1
- /package/dist/{domEditingLayers-D7VYFJJD.js.map → domEditingLayers-OL7GLSS4.js.map} +0 -0
|
@@ -2,17 +2,13 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
2
2
|
import type { HistoryListItem, HistoryResult } from "@hyperframes/studio-server";
|
|
3
3
|
import { studioFileContentVersion, studioWriteHeaders } from "../utils/studioFileVersion";
|
|
4
4
|
import type { RestoreFiles } from "../utils/gsapUndoRestore";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
label: string;
|
|
8
|
-
coalesceKey?: string;
|
|
9
|
-
coalesceMs?: number;
|
|
10
|
-
files: Record<string, { before: string; after: string }>;
|
|
11
|
-
}
|
|
5
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
6
|
+
import type { RecordEditInput } from "../utils/studioFileHistory";
|
|
12
7
|
|
|
13
8
|
interface ApplyCallbacks {
|
|
14
9
|
readFile: (path: string) => Promise<string>;
|
|
15
10
|
serialize?: <T>(paths: readonly string[], task: () => Promise<T>) => Promise<T>;
|
|
11
|
+
claimedAfter?: number;
|
|
16
12
|
}
|
|
17
13
|
|
|
18
14
|
export interface UsePersistentEditHistoryOptions {
|
|
@@ -53,6 +49,8 @@ function createOwnHistory() {
|
|
|
53
49
|
const own = new Map<string, OwnFiles>();
|
|
54
50
|
let next: Record<"undo" | "redo", NextStep | null> | null = null;
|
|
55
51
|
let changes = 0;
|
|
52
|
+
let claimed = { count: 0, id: "" };
|
|
53
|
+
const undoneIds = new Set<string>();
|
|
56
54
|
const remember = (id: string, files: OwnFiles) => {
|
|
57
55
|
const known = own.get(id) ?? {};
|
|
58
56
|
for (const [path, { before, after }] of Object.entries(files)) {
|
|
@@ -73,7 +71,16 @@ function createOwnHistory() {
|
|
|
73
71
|
if (seen === changes) next = { undo: view.back, redo: view.forward };
|
|
74
72
|
},
|
|
75
73
|
changes: () => changes,
|
|
74
|
+
claimCount: () => claimed.count,
|
|
75
|
+
claimedAfter: (count: number) => (claimed.count > count && claimed.id ? claimed.id : null),
|
|
76
|
+
targetFor: (id: string | null, pressedAt: number) =>
|
|
77
|
+
!id || !undoneIds.has(id) ? id : claimed.count > pressedAt ? false : null,
|
|
78
|
+
noteClaim: (id: string) => {
|
|
79
|
+
claimed = { count: claimed.count + 1, id };
|
|
80
|
+
},
|
|
76
81
|
stepped: (entry: { id: string; undoes?: string }) => {
|
|
82
|
+
if (entry.undoes) undoneIds.add(entry.undoes);
|
|
83
|
+
if (undoneIds.size > OWN_ENTRIES_KEPT) undoneIds.delete(undoneIds.values().next().value!);
|
|
77
84
|
const undone = entry.undoes ? own.get(entry.undoes) : undefined;
|
|
78
85
|
if (!undone) return;
|
|
79
86
|
const swapped = Object.entries(undone).map(([path, f]) => [
|
|
@@ -102,6 +109,7 @@ function createOwnHistory() {
|
|
|
102
109
|
},
|
|
103
110
|
clear: () => {
|
|
104
111
|
own.clear();
|
|
112
|
+
undoneIds.clear();
|
|
105
113
|
next = null;
|
|
106
114
|
changes += 1;
|
|
107
115
|
},
|
|
@@ -113,12 +121,20 @@ function historyUrl(projectId: string, path = ""): string {
|
|
|
113
121
|
return `/api/projects/${encodeURIComponent(projectId)}/history${path}`;
|
|
114
122
|
}
|
|
115
123
|
|
|
124
|
+
async function cantUndo(paths: string[] | undefined): Promise<ApplyResult> {
|
|
125
|
+
return { ok: false, reason: "content-mismatch", paths: paths ?? [] };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function unionPaths(...lists: Array<readonly string[] | undefined>): string[] {
|
|
129
|
+
return [...new Set(lists.flatMap((list) => list ?? []))];
|
|
130
|
+
}
|
|
131
|
+
|
|
116
132
|
async function post(
|
|
117
133
|
url: string,
|
|
118
134
|
body: object,
|
|
119
135
|
headers: Record<string, string> = {},
|
|
120
136
|
): Promise<{ ok: true; body: unknown } | { ok: false; status: number; error: string }> {
|
|
121
|
-
const response = await
|
|
137
|
+
const response = await studioApiFetch(url, {
|
|
122
138
|
method: "POST",
|
|
123
139
|
headers: { "Content-Type": "application/json", ...headers },
|
|
124
140
|
body: JSON.stringify(body),
|
|
@@ -194,9 +210,12 @@ export function usePersistentEditHistory({ projectId }: UsePersistentEditHistory
|
|
|
194
210
|
const refresh = useCallback(async () => {
|
|
195
211
|
if (!projectId) return;
|
|
196
212
|
const seen = own.changes();
|
|
197
|
-
const response = await
|
|
198
|
-
const next = response?.ok
|
|
213
|
+
const response = await studioApiFetch(historyUrl(projectId)).catch(() => null);
|
|
214
|
+
const next = response?.ok
|
|
215
|
+
? ((await response.json().catch(() => null)) as HistoryView | null)
|
|
216
|
+
: EMPTY;
|
|
199
217
|
if (projectIdRef.current !== projectId) return;
|
|
218
|
+
if (!next) return console.error("The history's reply was unreadable.");
|
|
200
219
|
setView(next);
|
|
201
220
|
own.offered(seen, next);
|
|
202
221
|
}, [projectId, own]);
|
|
@@ -210,9 +229,9 @@ export function usePersistentEditHistory({ projectId }: UsePersistentEditHistory
|
|
|
210
229
|
}, [refresh, own]);
|
|
211
230
|
|
|
212
231
|
const recordEdit = useCallback(
|
|
213
|
-
async ({ label, coalesceKey, coalesceMs, files }: RecordEditInput) => {
|
|
232
|
+
async ({ label, coalesceKey, coalesceMs, files, created = [] }: RecordEditInput) => {
|
|
214
233
|
if (!projectId) return;
|
|
215
|
-
const paths = Object.keys(files);
|
|
234
|
+
const paths = [...Object.keys(files), ...created];
|
|
216
235
|
own.overtake();
|
|
217
236
|
const reply = await post(historyUrl(projectId, "/claim"), {
|
|
218
237
|
label,
|
|
@@ -221,7 +240,10 @@ export function usePersistentEditHistory({ projectId }: UsePersistentEditHistory
|
|
|
221
240
|
...(coalesceKey && { coalesceKey, idleMs: coalesceMs ?? DEFAULT_COALESCE_MS }),
|
|
222
241
|
});
|
|
223
242
|
const claimed = claimHeld(reply, label);
|
|
224
|
-
if (claimed)
|
|
243
|
+
if (claimed) {
|
|
244
|
+
own.remember(claimed, files);
|
|
245
|
+
own.noteClaim(claimed);
|
|
246
|
+
}
|
|
225
247
|
heldClaimRef.current = claimed && coalesceKey ? { paths, at: Date.now() } : null;
|
|
226
248
|
await refresh();
|
|
227
249
|
},
|
|
@@ -231,14 +253,20 @@ export function usePersistentEditHistory({ projectId }: UsePersistentEditHistory
|
|
|
231
253
|
const step = useCallback(
|
|
232
254
|
async (direction: "undo" | "redo", callbacks: ApplyCallbacks): Promise<ApplyResult> => {
|
|
233
255
|
if (!projectId) return { ok: false, reason: "empty" };
|
|
256
|
+
const candidate =
|
|
257
|
+
direction === "undo" && callbacks.claimedAfter !== undefined
|
|
258
|
+
? own.claimedAfter(callbacks.claimedAfter)
|
|
259
|
+
: null;
|
|
234
260
|
const next = direction === "undo" ? view.back : view.forward;
|
|
235
|
-
const
|
|
261
|
+
const stepPaths = candidate ? Object.keys(own.afterOf(candidate)) : next?.paths;
|
|
262
|
+
const paths = unionPaths(stepPaths, heldClaimRef.current?.paths);
|
|
263
|
+
const pressedAt = own.claimCount();
|
|
236
264
|
own.overtake();
|
|
237
|
-
const
|
|
265
|
+
const attempt = async (target: string | null): Promise<ApplyResult> => {
|
|
238
266
|
const previous = await readAll(paths, callbacks.readFile);
|
|
239
267
|
const posted = await post(
|
|
240
|
-
historyUrl(projectId, "/step"),
|
|
241
|
-
{ direction: direction === "undo" ? "back" : "forward" },
|
|
268
|
+
historyUrl(projectId, target ? "/undo" : "/step"),
|
|
269
|
+
target ? { entryId: target } : { direction: direction === "undo" ? "back" : "forward" },
|
|
242
270
|
studioWriteHeaders(),
|
|
243
271
|
);
|
|
244
272
|
heldClaimRef.current = null;
|
|
@@ -266,6 +294,10 @@ export function usePersistentEditHistory({ projectId }: UsePersistentEditHistory
|
|
|
266
294
|
),
|
|
267
295
|
};
|
|
268
296
|
};
|
|
297
|
+
const run = () => {
|
|
298
|
+
const target = own.targetFor(candidate, pressedAt);
|
|
299
|
+
return target === false ? cantUndo(stepPaths) : attempt(target);
|
|
300
|
+
};
|
|
269
301
|
return callbacks.serialize ? callbacks.serialize(paths, run) : run();
|
|
270
302
|
},
|
|
271
303
|
[projectId, view, refresh, own],
|
|
@@ -301,6 +333,7 @@ export function usePersistentEditHistory({ projectId }: UsePersistentEditHistory
|
|
|
301
333
|
undo,
|
|
302
334
|
redo,
|
|
303
335
|
predict: own.predict,
|
|
336
|
+
claims: own.claimCount,
|
|
304
337
|
noteOutsideChange,
|
|
305
338
|
};
|
|
306
339
|
}
|
|
@@ -79,12 +79,13 @@ describe("undo that re-runs the top-level script over an element of a nested com
|
|
|
79
79
|
};
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
async function undo(fileOk: boolean) {
|
|
82
|
+
async function undo(fileOk: boolean, projectId: string | null = "p1") {
|
|
83
83
|
const { iframe, nwid } = nestedPreview();
|
|
84
84
|
const reloadPreview = vi.fn();
|
|
85
85
|
const fetch = vi.fn(async () => ({ ok: fileOk, json: async () => ({ content: SUB }) }));
|
|
86
86
|
vi.stubGlobal("fetch", fetch);
|
|
87
|
-
usePlayerStore.getState().beginTimelineSession(
|
|
87
|
+
if (projectId) usePlayerStore.getState().beginTimelineSession(projectId);
|
|
88
|
+
else usePlayerStore.setState({ timelineProjectId: null });
|
|
88
89
|
let sync: ReturnType<typeof usePreviewPersistence>["syncHistoryPreviewAfterApply"] | null =
|
|
89
90
|
null;
|
|
90
91
|
function Harness() {
|
|
@@ -109,7 +110,9 @@ describe("undo that re-runs the top-level script over an element of a nested com
|
|
|
109
110
|
it("restores the element from its own composition file", async () => {
|
|
110
111
|
const { nwid, reloadPreview, fetch } = await undo(true);
|
|
111
112
|
|
|
112
|
-
expect(fetch).toHaveBeenCalledWith(expect.stringContaining("/files/compositions%2Fsub.html")
|
|
113
|
+
expect(fetch).toHaveBeenCalledWith(expect.stringContaining("/files/compositions%2Fsub.html"), {
|
|
114
|
+
credentials: "omit",
|
|
115
|
+
});
|
|
113
116
|
expect(reloadPreview).not.toHaveBeenCalled();
|
|
114
117
|
expect(nwid.getAttribute("style")).toBe("left: 40px;");
|
|
115
118
|
});
|
|
@@ -120,4 +123,12 @@ describe("undo that re-runs the top-level script over an element of a nested com
|
|
|
120
123
|
expect(reloadPreview).toHaveBeenCalledTimes(1);
|
|
121
124
|
expect(nwid.getAttribute("style")).toBe("left: 40px; width: 337px");
|
|
122
125
|
});
|
|
126
|
+
|
|
127
|
+
it("reloads the preview in full when no project is open to read that file from", async () => {
|
|
128
|
+
const { nwid, reloadPreview, fetch } = await undo(true, null);
|
|
129
|
+
|
|
130
|
+
expect(fetch).not.toHaveBeenCalled();
|
|
131
|
+
expect(reloadPreview).toHaveBeenCalledTimes(1);
|
|
132
|
+
expect(nwid.getAttribute("style")).toBe("left: 40px; width: 337px");
|
|
133
|
+
});
|
|
123
134
|
});
|
|
@@ -163,8 +163,9 @@ export function usePreviewPersistence({
|
|
|
163
163
|
}, [drainPendingDomEditSaves]);
|
|
164
164
|
|
|
165
165
|
const settlePendingEdits = useCallback(async (): Promise<void> => {
|
|
166
|
-
|
|
167
|
-
await
|
|
166
|
+
const queued = domEditSaveQueueRef.current?.waitForIdle();
|
|
167
|
+
await flushStudioPendingEdits({ onlyCurrent: true });
|
|
168
|
+
await queued;
|
|
168
169
|
}, []);
|
|
169
170
|
|
|
170
171
|
const resetDomEditSaveQueueBreaker = useCallback(() => {
|
|
@@ -209,14 +210,15 @@ export function usePreviewPersistence({
|
|
|
209
210
|
// multi-file, sub-comp, or a permanent soft-reload failure.
|
|
210
211
|
const projectId = usePlayerStore.getState().timelineProjectId;
|
|
211
212
|
const nestedFiles = await settleNestedReads(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
213
|
+
readUndoNestedFiles(
|
|
214
|
+
previewIframeRef.current,
|
|
215
|
+
activeCompPathRef.current,
|
|
216
|
+
restore.files,
|
|
217
|
+
(path) =>
|
|
218
|
+
projectId
|
|
219
|
+
? readProjectFileContent(projectId, path)
|
|
220
|
+
: Promise.reject(new Error("No project is open to read nested files from.")),
|
|
221
|
+
),
|
|
220
222
|
);
|
|
221
223
|
const strategy = applyUndoRestoreToPreview(
|
|
222
224
|
previewIframeRef.current,
|
|
@@ -258,7 +260,7 @@ export function usePreviewPersistence({
|
|
|
258
260
|
|
|
259
261
|
// ── Migrate legacy studio-motion.json ──
|
|
260
262
|
// Projects that used the old JSON-file approach may still have a populated
|
|
261
|
-
//
|
|
263
|
+
// studio-motion.json in the project's .hyperframes folder. The studio no longer reads it, but
|
|
262
264
|
// the legacy render-script injection in `preview.ts` / `vite.studioMotion.ts`
|
|
263
265
|
// could still fire alongside the new seek-reapply runtime. Empty the file so
|
|
264
266
|
// the legacy codepath no-ops.
|
|
@@ -6,11 +6,22 @@ import {
|
|
|
6
6
|
StudioSaveNetworkError,
|
|
7
7
|
} from "../utils/studioSaveDiagnostics";
|
|
8
8
|
import { studioExpectedFileVersion, studioWriteHeaders } from "../utils/studioFileVersion";
|
|
9
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
9
10
|
|
|
10
11
|
export interface UseProjectFileWriterOptions {
|
|
11
12
|
projectId: string | null;
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
async function fetchProjectFile(projectId: string | null, path: string, query: string) {
|
|
16
|
+
if (!projectId) throw new Error("No active project");
|
|
17
|
+
const response = await studioApiFetch(
|
|
18
|
+
`/api/projects/${encodeURIComponent(projectId)}/files/${encodeURIComponent(path)}${query}`,
|
|
19
|
+
);
|
|
20
|
+
if (!response.ok) throw new Error(`Failed to read ${path}`);
|
|
21
|
+
const data = (await response.json()) as { content?: string; version?: string };
|
|
22
|
+
return { content: data.content, version: data.version ?? response.headers.get("etag") };
|
|
23
|
+
}
|
|
24
|
+
|
|
14
25
|
// The etag-guarded project-file read/write pair every Studio save path
|
|
15
26
|
// shares. A host mounting hand editing outside Studio needs only this, not
|
|
16
27
|
// the full file manager (file tree, editor tabs, uploads, font assets).
|
|
@@ -30,29 +41,19 @@ export function useProjectFileWriter({ projectId }: UseProjectFileWriterOptions)
|
|
|
30
41
|
|
|
31
42
|
const readProjectFile = useCallback(
|
|
32
43
|
async (path: string): Promise<string> => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (!response.ok) throw new Error(`Failed to read ${path}`);
|
|
38
|
-
const data = (await response.json()) as { content?: string; version?: string };
|
|
39
|
-
if (typeof data.content !== "string") throw new Error(`Missing file contents for ${path}`);
|
|
40
|
-
fileVersions.set(path, data.version ?? response.headers.get("etag"));
|
|
41
|
-
return data.content;
|
|
44
|
+
const { content, version } = await fetchProjectFile(projectId, path, "");
|
|
45
|
+
if (typeof content !== "string") throw new Error(`Missing file contents for ${path}`);
|
|
46
|
+
fileVersions.set(path, version);
|
|
47
|
+
return content;
|
|
42
48
|
},
|
|
43
49
|
[fileVersions, projectId],
|
|
44
50
|
);
|
|
45
51
|
|
|
46
52
|
const readOptionalProjectFile = useCallback(
|
|
47
53
|
async (path: string): Promise<string> => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
);
|
|
52
|
-
if (!response.ok) throw new Error(`Failed to read ${path}`);
|
|
53
|
-
const data = (await response.json()) as { content?: string; version?: string };
|
|
54
|
-
fileVersions.set(path, data.version ?? response.headers.get("etag"));
|
|
55
|
-
return typeof data.content === "string" ? data.content : "";
|
|
54
|
+
const { content, version } = await fetchProjectFile(projectId, path, "?optional=1");
|
|
55
|
+
fileVersions.set(path, version);
|
|
56
|
+
return typeof content === "string" ? content : "";
|
|
56
57
|
},
|
|
57
58
|
[fileVersions, projectId],
|
|
58
59
|
);
|
|
@@ -63,7 +64,7 @@ export function useProjectFileWriter({ projectId }: UseProjectFileWriterOptions)
|
|
|
63
64
|
const writeProjectId = projectId;
|
|
64
65
|
let expectedVersion = await studioExpectedFileVersion(fileVersions, path, expectedContent);
|
|
65
66
|
if (expectedVersion === undefined) {
|
|
66
|
-
const preflight = await
|
|
67
|
+
const preflight = await studioApiFetch(
|
|
67
68
|
`/api/projects/${encodeURIComponent(writeProjectId)}/files/${encodeURIComponent(path)}`,
|
|
68
69
|
);
|
|
69
70
|
if (preflight.ok) {
|
|
@@ -86,7 +87,7 @@ export function useProjectFileWriter({ projectId }: UseProjectFileWriterOptions)
|
|
|
86
87
|
// the retry can produce a second filesystem receipt that must be suppressed independently.
|
|
87
88
|
let response: Response;
|
|
88
89
|
try {
|
|
89
|
-
response = await
|
|
90
|
+
response = await studioApiFetch(
|
|
90
91
|
`/api/projects/${encodeURIComponent(writeProjectId)}/files/${encodeURIComponent(path)}`,
|
|
91
92
|
{
|
|
92
93
|
method: "PUT",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef } from "react";
|
|
2
2
|
import { waitForMediaJob } from "../components/studioMediaJobs";
|
|
3
3
|
import type { BackgroundRemovalProgress } from "../components/editor/propertyPanelTypes";
|
|
4
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
4
5
|
|
|
5
6
|
interface RemoveBackgroundOptions {
|
|
6
7
|
createBackgroundPlate?: boolean;
|
|
@@ -31,7 +32,7 @@ export function useRemoveBackground(
|
|
|
31
32
|
return useCallback(
|
|
32
33
|
// fallow-ignore-next-line complexity
|
|
33
34
|
async (inputPath: string, options: RemoveBackgroundOptions) => {
|
|
34
|
-
const response = await
|
|
35
|
+
const response = await studioApiFetch(
|
|
35
36
|
`/api/projects/${encodeURIComponent(projectId)}/media/remove-background`,
|
|
36
37
|
{
|
|
37
38
|
method: "POST",
|
|
@@ -6,6 +6,7 @@ import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
|
6
6
|
import type { PublishSdkSession } from "../utils/sdkCutover";
|
|
7
7
|
import { addExternalFileReloadListener } from "./externalFileReloadBus";
|
|
8
8
|
import { whenPreviewBooted } from "../player/store/playerStore";
|
|
9
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Why an optional project-file read produced no usable content. `stage: "read"`
|
|
@@ -194,7 +195,7 @@ async function readProjectFileOptional(
|
|
|
194
195
|
let res: Response;
|
|
195
196
|
const fetchStarted = performance.now();
|
|
196
197
|
try {
|
|
197
|
-
res = await
|
|
198
|
+
res = await studioApiFetch(
|
|
198
199
|
`/api/projects/${encodeURIComponent(projectId)}/files/${encodeURIComponent(path)}?optional=1`,
|
|
199
200
|
);
|
|
200
201
|
} catch {
|
|
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
|
|
|
2
2
|
import { buildProjectHash, parseProjectIdFromHash } from "../utils/projectRouting";
|
|
3
3
|
import { useMountEffect } from "./useMountEffect";
|
|
4
4
|
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
5
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
5
6
|
|
|
6
7
|
interface ServerConnectionState {
|
|
7
8
|
projectId: string | null;
|
|
@@ -37,7 +38,7 @@ type ProjectHashVerdict = {
|
|
|
37
38
|
|
|
38
39
|
async function resolveHashProject(id: string): Promise<ProjectHashVerdict> {
|
|
39
40
|
try {
|
|
40
|
-
const res = await
|
|
41
|
+
const res = await studioApiFetch(`/api/projects/${encodeURIComponent(id)}`);
|
|
41
42
|
if (res.ok) return { outcome: "ok", status: res.status };
|
|
42
43
|
return { outcome: res.status === 404 ? "missing" : "unknown", status: res.status };
|
|
43
44
|
} catch {
|
|
@@ -72,7 +73,7 @@ function replaceProjectHash(projectId: string): void {
|
|
|
72
73
|
*/
|
|
73
74
|
async function firstProjectId(): Promise<string | null> {
|
|
74
75
|
try {
|
|
75
|
-
const res = await
|
|
76
|
+
const res = await studioApiFetch("/api/projects");
|
|
76
77
|
const data = (await res.json()) as { projects?: Array<{ id?: string }> };
|
|
77
78
|
return data.projects?.[0]?.id ?? null;
|
|
78
79
|
} catch {
|
|
@@ -107,7 +108,7 @@ export function useServerConnection(): ServerConnectionState {
|
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
function tryConnect() {
|
|
110
|
-
|
|
111
|
+
studioApiFetch("/api/projects")
|
|
111
112
|
.then((r) => r.json())
|
|
112
113
|
.then(async (data) => {
|
|
113
114
|
if (cancelled) return;
|
|
@@ -3,6 +3,7 @@ import type { DomEditSelection } from "../components/editor/domEditing";
|
|
|
3
3
|
import { usePlayerStore } from "../player";
|
|
4
4
|
import { buildStudioSelectionSnapshot } from "../utils/studioSelectionSnapshot";
|
|
5
5
|
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
6
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
6
7
|
|
|
7
8
|
interface UseStudioSelectionPublisherParams {
|
|
8
9
|
projectId: string | null;
|
|
@@ -26,7 +27,7 @@ function reportSelectionPublishError(error: unknown): void {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
function putSelection(projectId: string, selection: unknown, signal?: AbortSignal): Promise<void> {
|
|
29
|
-
return
|
|
30
|
+
return studioApiFetch(`/api/projects/${encodeURIComponent(projectId)}/selection`, {
|
|
30
31
|
method: "PUT",
|
|
31
32
|
headers: { "Content-Type": "application/json" },
|
|
32
33
|
body: JSON.stringify({ selection }),
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { thumbnailScheduler } from "../player/lib/thumbnailScheduler";
|
|
7
|
+
// By package name, as a host imports it: the test fails if the export goes.
|
|
8
|
+
import { useThumbnailStill, type ThumbnailStill } from "@hyperframes/studio";
|
|
9
|
+
|
|
10
|
+
Object.defineProperty(globalThis, "IS_REACT_ACT_ENVIRONMENT", { configurable: true, value: true });
|
|
11
|
+
|
|
12
|
+
class MockImage {
|
|
13
|
+
onload: (() => void) | null = null;
|
|
14
|
+
onerror: (() => void) | null = null;
|
|
15
|
+
naturalWidth = 320;
|
|
16
|
+
naturalHeight = 180;
|
|
17
|
+
set src(_value: string) {
|
|
18
|
+
queueMicrotask(() => this.onload?.());
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const URL_ONE = "http://localhost:3000/api/projects/demo/thumbnail/index.html?t=1.00&revision=1";
|
|
23
|
+
const originalImage = globalThis.Image;
|
|
24
|
+
const originalFetch = globalThis.fetch;
|
|
25
|
+
const originalCreate = URL.createObjectURL;
|
|
26
|
+
const originalRevoke = URL.revokeObjectURL;
|
|
27
|
+
let host: HTMLDivElement;
|
|
28
|
+
let root: Root | null = null;
|
|
29
|
+
let shown: ThumbnailStill | null = null;
|
|
30
|
+
|
|
31
|
+
function Still({ url }: { url: string | null }) {
|
|
32
|
+
shown = useThumbnailStill(url, "demo");
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const mount = (url: string | null) =>
|
|
37
|
+
act(async () => {
|
|
38
|
+
root ??= createRoot(host);
|
|
39
|
+
root.render(React.createElement(Still, { url }));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
globalThis.Image = MockImage as unknown as typeof Image;
|
|
44
|
+
globalThis.fetch = vi.fn(async () => new Response(new Blob(["still"]), { status: 200 }));
|
|
45
|
+
URL.createObjectURL = vi.fn(() => "blob:still");
|
|
46
|
+
URL.revokeObjectURL = vi.fn();
|
|
47
|
+
shown = null;
|
|
48
|
+
host = document.createElement("div");
|
|
49
|
+
document.body.append(host);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
act(() => root?.unmount());
|
|
54
|
+
root = null;
|
|
55
|
+
thumbnailScheduler.setPreviewReloading(false);
|
|
56
|
+
thumbnailScheduler.invalidateProject("demo");
|
|
57
|
+
globalThis.Image = originalImage;
|
|
58
|
+
globalThis.fetch = originalFetch;
|
|
59
|
+
URL.createObjectURL = originalCreate;
|
|
60
|
+
URL.revokeObjectURL = originalRevoke;
|
|
61
|
+
document.body.replaceChildren();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("useThumbnailStill", () => {
|
|
65
|
+
it("hands back the still's object URL once the scheduler has loaded it", async () => {
|
|
66
|
+
await mount(URL_ONE);
|
|
67
|
+
expect(globalThis.fetch).toHaveBeenCalledWith(URL_ONE, expect.anything());
|
|
68
|
+
await vi.waitFor(() => expect(shown).toEqual({ status: "ready", url: "blob:still" }));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("hands back the same object while nothing changes", async () => {
|
|
72
|
+
await mount(URL_ONE);
|
|
73
|
+
await vi.waitFor(() => expect(shown).toEqual({ status: "ready", url: "blob:still" }));
|
|
74
|
+
const first = shown;
|
|
75
|
+
await mount(URL_ONE);
|
|
76
|
+
expect(shown).toBe(first);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("asks for nothing without a url", async () => {
|
|
80
|
+
await mount(null);
|
|
81
|
+
expect(globalThis.fetch).not.toHaveBeenCalled();
|
|
82
|
+
expect(shown).toEqual({ status: "loading" });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("says failed when the still cannot load, so a host can fall back", async () => {
|
|
86
|
+
globalThis.fetch = vi.fn(async () => new Response("", { status: 500 }));
|
|
87
|
+
await mount(URL_ONE);
|
|
88
|
+
await vi.waitFor(() => expect(shown).toEqual({ status: "failed" }));
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("holds the fetch while a preview reloads, and runs it once the reload is over", async () => {
|
|
92
|
+
thumbnailScheduler.setPreviewReloading(true);
|
|
93
|
+
await mount(URL_ONE);
|
|
94
|
+
expect(globalThis.fetch).not.toHaveBeenCalled();
|
|
95
|
+
expect(shown).toEqual({ status: "loading" });
|
|
96
|
+
await act(async () => thumbnailScheduler.setPreviewReloading(false));
|
|
97
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
|
98
|
+
await vi.waitFor(() => expect(shown).toEqual({ status: "ready", url: "blob:still" }));
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { compositionThumbnailRequest } from "../player/components/CompositionThumbnail";
|
|
3
|
+
import type { ThumbnailPriority } from "../player/lib/thumbnailScheduler";
|
|
4
|
+
import { useThumbnailLease } from "./useThumbnailLease";
|
|
5
|
+
|
|
6
|
+
export interface UseThumbnailStillOptions {
|
|
7
|
+
sessionEpoch?: number;
|
|
8
|
+
priority?: ThumbnailPriority;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type ThumbnailStill =
|
|
12
|
+
| { status: "loading" }
|
|
13
|
+
| { status: "ready"; url: string }
|
|
14
|
+
| { status: "failed" };
|
|
15
|
+
|
|
16
|
+
const LOADING: ThumbnailStill = Object.freeze({ status: "loading" });
|
|
17
|
+
const FAILED: ThumbnailStill = Object.freeze({ status: "failed" });
|
|
18
|
+
|
|
19
|
+
/** A host picture's server still through the thumbnail scheduler, so it yields to a preview reload. */
|
|
20
|
+
export function useThumbnailStill(
|
|
21
|
+
url: string | null,
|
|
22
|
+
projectId: string,
|
|
23
|
+
{ sessionEpoch = 0, priority = "visible" }: UseThumbnailStillOptions = {},
|
|
24
|
+
): ThumbnailStill {
|
|
25
|
+
const request = useMemo(
|
|
26
|
+
() => (url ? compositionThumbnailRequest(url, projectId, { sessionEpoch, priority }) : null),
|
|
27
|
+
[priority, projectId, sessionEpoch, url],
|
|
28
|
+
);
|
|
29
|
+
const snapshot = useThumbnailLease(request);
|
|
30
|
+
const ready =
|
|
31
|
+
snapshot.status === "ready" && snapshot.value.kind === "image" ? snapshot.value.url : null;
|
|
32
|
+
return useMemo(
|
|
33
|
+
() =>
|
|
34
|
+
ready ? { status: "ready", url: ready } : snapshot.status === "error" ? FAILED : LOADING,
|
|
35
|
+
[ready, snapshot.status],
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -21,6 +21,7 @@ import type {
|
|
|
21
21
|
TimelineGroupCommitOptions,
|
|
22
22
|
TimelineGroupMoveChange,
|
|
23
23
|
} from "./useTimelineGroupEditing";
|
|
24
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
24
25
|
|
|
25
26
|
/** Apply already-resolved ripple changes to the surviving elements for the
|
|
26
27
|
* optimistic store update after a delete. Pure — no IO. */
|
|
@@ -130,7 +131,7 @@ export function useTimelineDeleteOps({
|
|
|
130
131
|
}
|
|
131
132
|
return patchTarget;
|
|
132
133
|
});
|
|
133
|
-
const removeResponse = await
|
|
134
|
+
const removeResponse = await studioApiFetch(
|
|
134
135
|
buildProjectApiPath(
|
|
135
136
|
pid,
|
|
136
137
|
`/file-mutations/remove-elements/${encodeURIComponent(targetPath)}`,
|
package/src/index.ts
CHANGED
|
@@ -76,6 +76,8 @@ export type { AudioWaveformProps } from "./player/components/AudioWaveform";
|
|
|
76
76
|
export { ImageThumbnail } from "./player/components/ImageThumbnail";
|
|
77
77
|
export type { ImageThumbnailProps } from "./player/components/ImageThumbnail";
|
|
78
78
|
export { useRenderClipContent } from "./hooks/useRenderClipContent";
|
|
79
|
+
export { useThumbnailStill } from "./hooks/useThumbnailStill";
|
|
80
|
+
export type { ThumbnailStill, UseThumbnailStillOptions } from "./hooks/useThumbnailStill";
|
|
79
81
|
export type { UseRenderClipContentOptions } from "./hooks/useRenderClipContent";
|
|
80
82
|
export type { ThumbnailPriority } from "./player/lib/thumbnailScheduler";
|
|
81
83
|
export type {
|
|
@@ -208,6 +210,8 @@ export type { DomEditZOrder } from "./components/editor/useDomEditZOrder";
|
|
|
208
210
|
export type { ZOrderAction } from "./components/editor/canvasContextMenuZOrder";
|
|
209
211
|
export type { DomEditCapabilities, DomEditSelection } from "./components/editor/domEditingTypes";
|
|
210
212
|
export { useDomStyleCommit } from "./hooks/useDomStyleCommit";
|
|
213
|
+
export { ColorField } from "./components/editor/propertyPanelColor";
|
|
214
|
+
export { GradientField } from "./components/editor/propertyPanelFill";
|
|
211
215
|
export type { UseDomStyleCommitOptions } from "./hooks/useDomStyleCommit";
|
|
212
216
|
export type { DomEditCommitDeclineReason, DomEditCommitOutcome } from "./hooks/domEditCommitRunner";
|
|
213
217
|
export { resolveDomEditSelection } from "./components/editor/domEditingLayers";
|
|
@@ -5,6 +5,7 @@ import { useThumbnailLease } from "../../hooks/useThumbnailLease";
|
|
|
5
5
|
import { createThumbnailKey, type ThumbnailPriority } from "../lib/thumbnailScheduler";
|
|
6
6
|
import { decimatePeaks, loudnessToOpacity } from "./audioWaveformPeaks";
|
|
7
7
|
import { ClipFadesContext, type ClipFadeShape } from "./TimelineClipFades";
|
|
8
|
+
import { studioApiFetch } from "../../utils/studioApiFetch";
|
|
8
9
|
|
|
9
10
|
export interface AudioWaveformProps {
|
|
10
11
|
audioUrl: string;
|
|
@@ -124,7 +125,7 @@ async function loadWaveform(
|
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
async function fetchWaveformPeaks(url: string, signal: AbortSignal): Promise<number[]> {
|
|
127
|
-
const response = await
|
|
128
|
+
const response = await studioApiFetch(url, { signal });
|
|
128
129
|
if (!response.ok) throw new Error(`Waveform request failed (${response.status})`);
|
|
129
130
|
const data: unknown = await response.json();
|
|
130
131
|
if (
|
|
@@ -140,7 +141,7 @@ async function fetchWaveformPeaks(url: string, signal: AbortSignal): Promise<num
|
|
|
140
141
|
}
|
|
141
142
|
|
|
142
143
|
async function decodeWaveformPeaks(url: string, signal: AbortSignal): Promise<number[]> {
|
|
143
|
-
const response = await
|
|
144
|
+
const response = await studioApiFetch(url, { signal });
|
|
144
145
|
if (!response.ok) throw new Error(`Audio request failed (${response.status})`);
|
|
145
146
|
const buffer = await response.arrayBuffer();
|
|
146
147
|
if (signal.aborted) throw new DOMException("Aborted", "AbortError");
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "../lib/thumbnailScheduler";
|
|
9
9
|
import { TIMELINE_VIEWPORT_BUDGETS } from "../lib/timelineViewportBudgets";
|
|
10
10
|
import { computeThumbnailStrip, probeImageAspect } from "./thumbnailUtils";
|
|
11
|
+
import { studioApiFetch } from "../../utils/studioApiFetch";
|
|
11
12
|
|
|
12
13
|
interface CompositionThumbnailProps {
|
|
13
14
|
previewUrl: string;
|
|
@@ -106,7 +107,7 @@ export function compositionThumbnailRequest(
|
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
async function loadCompositionImage(url: string, signal: AbortSignal) {
|
|
109
|
-
const response = await
|
|
110
|
+
const response = await studioApiFetch(url, { signal });
|
|
110
111
|
if (!response.ok) throw new Error(`Composition thumbnail failed (${response.status})`);
|
|
111
112
|
const blob = await response.blob();
|
|
112
113
|
if (signal.aborted) throw new DOMException("Aborted", "AbortError");
|
|
@@ -4,6 +4,7 @@ import { resolveMediaPreviewUrl } from "./thumbnailUtils";
|
|
|
4
4
|
import type { TimelineElement } from "../store/timelineElement";
|
|
5
5
|
import { parseAutomation, VOLUME_TARGET } from "@hyperframes/core/audio-automation";
|
|
6
6
|
import type { TimelineEditOutcome } from "../../hooks/timelineEditPermission";
|
|
7
|
+
import { studioApiFetch } from "../../utils/studioApiFetch";
|
|
7
8
|
|
|
8
9
|
export interface NormalizePlan {
|
|
9
10
|
targetLufs: number;
|
|
@@ -74,7 +75,7 @@ function isNormalizePlan(value: unknown): value is NormalizePlan {
|
|
|
74
75
|
export async function requestNormalizePlan(
|
|
75
76
|
projectId: string,
|
|
76
77
|
el: TimelineElement,
|
|
77
|
-
fetchImpl: typeof
|
|
78
|
+
fetchImpl: typeof studioApiFetch = studioApiFetch,
|
|
78
79
|
): Promise<NormalizePlan> {
|
|
79
80
|
const origin = window.location.origin;
|
|
80
81
|
const relative = resolvePreviewRelative(
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildProjectApiPath } from "../../utils/projectRouting";
|
|
2
|
+
import { studioApiFetch } from "../../utils/studioApiFetch";
|
|
2
3
|
import { resolvePreviewRelative } from "../../utils/previewRelativePath";
|
|
3
4
|
import type { TimelineElement } from "../store/timelineElement";
|
|
4
5
|
import { clipSourcePeak, isPeakMap, type ClipSourceWindow, type PeakMap } from "./clipPeakRuns";
|
|
@@ -10,7 +11,7 @@ const peakMapRequests = new Map<string, Promise<PeakMap | null>>();
|
|
|
10
11
|
export function loadPeakMap(url: string): Promise<PeakMap | null> {
|
|
11
12
|
const pending = peakMapRequests.get(url);
|
|
12
13
|
if (pending) return pending;
|
|
13
|
-
const request =
|
|
14
|
+
const request = studioApiFetch(url)
|
|
14
15
|
.then((res) => (res.ok ? res.json() : null))
|
|
15
16
|
.then((body: unknown) => (isPeakMap(body) ? body : null))
|
|
16
17
|
.catch(() => null);
|