@hyperframes/studio 0.8.75 → 0.8.76
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-BkDxgSKG.js +464 -0
- package/dist/assets/{index-BiFKcNjs.js → index-COX0scwJ.js} +1 -1
- package/dist/assets/{index-BlUYw1MB.js → index-DeHir1up.js} +156 -156
- package/dist/assets/{index-HjexPsny.js → index-FmKUmYhC.js} +1 -1
- package/dist/{chunk-7BP32G62.js → chunk-IYDE66BY.js} +6 -1
- package/dist/chunk-IYDE66BY.js.map +1 -0
- package/dist/{domEditingLayers-UKMVYXT6.js → domEditingLayers-2XBFXXSW.js} +4 -2
- package/dist/index.d.ts +34 -69
- package/dist/index.html +1 -1
- package/dist/index.js +4147 -4253
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/components/StudioRightPanels.types.ts +0 -2
- package/src/components/editor/DomEditOverlay.test.ts +5 -0
- package/src/components/editor/LayersPanel.tsx +3 -14
- package/src/components/editor/PreviewOverlayProvider.test.tsx +6 -1
- package/src/components/editor/domEditing.ts +1 -0
- package/src/components/editor/domEditingLayers.test.ts +27 -0
- package/src/components/editor/domEditingLayers.ts +9 -0
- package/src/components/editor/offCanvasIndicatorRefresh.test.tsx +57 -35
- package/src/components/editor/overlayFrameLoop.test.ts +16 -0
- package/src/components/editor/overlayFrameLoop.ts +11 -0
- package/src/components/editor/usePreviewCompositionRect.test.tsx +6 -1
- package/src/components/nle/NLEPreview.test.ts +94 -17
- package/src/components/nle/NLEPreview.tsx +14 -0
- package/src/components/nle/PreviewPoster.tsx +99 -0
- package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +30 -49
- package/src/components/panels/VariablesPanel.tsx +0 -2
- package/src/components/sidebar/CompositionsTab.drag.test.tsx +5 -1
- package/src/components/sidebar/CompositionsTab.rootBadge.test.tsx +14 -0
- package/src/components/sidebar/CompositionsTab.test.ts +4 -6
- package/src/components/sidebar/CompositionsTab.tsx +28 -21
- package/src/components/studioColorGradingScope.ts +0 -3
- package/src/hooks/domEditDeleteMembers.ts +0 -2
- package/src/hooks/gsapScriptCommitTypes.ts +0 -2
- package/src/hooks/keyframeCacheAstLoad.test.ts +31 -1
- package/src/hooks/keyframeCacheAstLoad.ts +2 -1
- package/src/hooks/timelineAudioGroupCreate.ts +0 -1
- package/src/hooks/timelineEditingHelpers.ts +0 -3
- package/src/hooks/timelineTimingSync.ts +0 -1
- package/src/hooks/timelineTrackVisibility.ts +0 -1
- package/src/hooks/useBlockHandlers.ts +0 -2
- package/src/hooks/useClipboard.test.ts +69 -10
- package/src/hooks/useClipboard.ts +28 -19
- package/src/hooks/useDomEditCommits.test.tsx +0 -1
- package/src/hooks/useDomEditCommits.ts +0 -4
- package/src/hooks/useEditHistoryActions.test.tsx +31 -14
- package/src/hooks/useEditHistoryActions.ts +12 -7
- package/src/hooks/useEditorSave.ts +0 -3
- package/src/hooks/useElementLifecycleOps.ts +0 -1
- package/src/hooks/useEnableKeyframes.test.ts +9 -1
- package/src/hooks/useFileManager.ts +0 -2
- package/src/hooks/useGroupCommits.ts +0 -1
- package/src/hooks/useGsapScriptCommits.ts +0 -1
- package/src/hooks/useLintModal.test.tsx +34 -1
- package/src/hooks/useLintModal.ts +32 -30
- package/src/hooks/usePersistentEditHistory.test.ts +221 -315
- package/src/hooks/usePersistentEditHistory.ts +155 -342
- package/src/hooks/usePopulateKeyframeCacheForFile.test.tsx +2 -0
- package/src/hooks/usePreviewPersistence.ts +0 -2
- package/src/hooks/useProjectCompositionVariables.ts +1 -3
- package/src/hooks/useRazorSplit.history.test.tsx +46 -67
- package/src/hooks/useRazorSplit.test.ts +17 -47
- package/src/hooks/useRenderClipContent.test.ts +29 -0
- package/src/hooks/useRenderClipContent.ts +6 -3
- package/src/hooks/useSdkSession.lifecycle.test.tsx +34 -4
- package/src/hooks/useSdkSession.ts +8 -2
- package/src/hooks/useSlideshowPersist.ts +0 -2
- package/src/hooks/useStudioContextValue.ts +6 -1
- package/src/hooks/useTimelineAssetDropOps.ts +0 -1
- package/src/hooks/useTimelineDeleteOps.ts +0 -1
- package/src/hooks/useTimelineEditing.test.tsx +0 -1
- package/src/hooks/useTimelineEditingTypes.ts +0 -2
- package/src/hostMount.test.tsx +53 -22
- package/src/main.tsx +3 -0
- package/src/player/components/CompositionThumbnail.tsx +14 -0
- package/src/player/components/Player.test.ts +49 -0
- package/src/player/components/Player.tsx +21 -7
- package/src/player/components/timelineClipDragCommit.test.ts +7 -39
- package/src/player/hooks/timelineDomBinding.test.ts +50 -0
- package/src/player/hooks/timelineSyncHydration.test.ts +25 -1
- package/src/player/hooks/timelineSyncHydration.ts +3 -3
- package/src/player/hooks/useShadowPreviewReload.ts +1 -0
- package/src/player/hooks/useTimelinePlayer.play.test.ts +120 -0
- package/src/player/hooks/useTimelinePlayer.ts +1 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +2 -0
- package/src/player/lib/timelineElementHelpers.ts +45 -9
- package/src/player/store/playerStore.test.ts +35 -1
- package/src/player/store/playerStore.ts +29 -0
- package/src/player/store/readinessSlice.ts +34 -6
- package/src/utils/blockInstaller.ts +0 -3
- package/src/utils/previewPrefetch.test.ts +22 -0
- package/src/utils/previewPrefetch.ts +14 -0
- package/src/utils/razorSplitTransaction.test.ts +0 -1
- package/src/utils/razorSplitTransaction.ts +1 -1
- package/src/utils/sdkEditTransaction.ts +0 -3
- package/src/utils/studioFileHistory.test.ts +0 -9
- package/src/utils/studioFileHistory.ts +1 -5
- package/src/utils/timelineCompositionInsert.ts +0 -1
- package/dist/assets/hyperframes-player-WbBVbWuO.js +0 -464
- package/dist/chunk-7BP32G62.js.map +0 -1
- package/src/hooks/usePersistentEditHistory.projectOwnership.test.tsx +0 -60
- package/src/utils/editHistory.test.ts +0 -534
- package/src/utils/editHistory.ts +0 -236
- package/src/utils/editHistoryStorage.test.ts +0 -37
- package/src/utils/editHistoryStorage.ts +0 -99
- /package/dist/{domEditingLayers-UKMVYXT6.js.map → domEditingLayers-2XBFXXSW.js.map} +0 -0
|
@@ -428,14 +428,50 @@ function nodeMatchesManifestClip(node: Element, clip: ClipManifestClip): boolean
|
|
|
428
428
|
});
|
|
429
429
|
}
|
|
430
430
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
431
|
+
/** Whether `node` sits in the composition the clip was read from, the chain the runtime records outermost first
|
|
432
|
+
* (`resolveNearestCompositionContext` in core's runtime/timeline.ts). A clip without that scope accepts any node. */
|
|
433
|
+
function nodeInClipScope(node: Element, clip: ClipManifestClip): boolean {
|
|
434
|
+
const scope = clip.compositionAncestors;
|
|
435
|
+
if (!scope) return true;
|
|
436
|
+
const ids: string[] = [];
|
|
437
|
+
for (let cursor = node.parentElement; cursor; cursor = cursor.parentElement) {
|
|
438
|
+
const id = cursor.getAttribute("data-composition-id");
|
|
439
|
+
if (id) ids.unshift(id);
|
|
440
|
+
}
|
|
441
|
+
return ids.length === scope.length && ids.every((id, index) => id === scope[index]);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** The first match in the clip's composition across `selectors`; a lone match stands only when none is in scope, as a
|
|
445
|
+
* healed host can stale the clip's chain for a pass. An id can repeat in a sub-composition earlier in the document. */
|
|
446
|
+
function findInClipScope(
|
|
447
|
+
doc: Document,
|
|
448
|
+
clip: ClipManifestClip,
|
|
449
|
+
selectors: string[],
|
|
450
|
+
): Element | null {
|
|
451
|
+
let lone: Element | null = null;
|
|
452
|
+
for (const selector of selectors) {
|
|
453
|
+
const nodes = Array.from(doc.querySelectorAll(selector));
|
|
454
|
+
const scoped = nodes.find((node) => nodeInClipScope(node, clip));
|
|
455
|
+
if (scoped) return scoped;
|
|
456
|
+
if (nodes.length === 1) lone ??= nodes[0];
|
|
457
|
+
}
|
|
458
|
+
return lone;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export function findClipElementById(doc: Document, clip: ClipManifestClip): Element | null {
|
|
462
|
+
if (!clip.id) return null;
|
|
463
|
+
const first = doc.getElementById(clip.id);
|
|
464
|
+
if (!first || nodeInClipScope(first, clip)) return first;
|
|
465
|
+
return findInClipScope(doc, clip, [`[id="${CSS.escape(clip.id)}"]`]);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function findTimelineDomNode(doc: Document, clip: ClipManifestClip): Element | null {
|
|
469
|
+
if (!clip.id) return null;
|
|
470
|
+
const first = doc.getElementById(clip.id);
|
|
471
|
+
if (first && nodeInClipScope(first, clip)) return first;
|
|
472
|
+
const id = CSS.escape(clip.id);
|
|
473
|
+
const byOtherKeys = [`[data-hf-id="${id}"]`, `[data-composition-id="${id}"]`, `.${id}`];
|
|
474
|
+
return findInClipScope(doc, clip, first ? [`[id="${id}"]`, ...byOtherKeys] : byOtherKeys);
|
|
439
475
|
}
|
|
440
476
|
|
|
441
477
|
export function findTimelineDomNodeForClip(
|
|
@@ -445,7 +481,7 @@ export function findTimelineDomNodeForClip(
|
|
|
445
481
|
usedNodes = new Set<Element>(),
|
|
446
482
|
getCandidates = () => getTimelineDomNodes(doc),
|
|
447
483
|
): Element | null {
|
|
448
|
-
const byIdentity =
|
|
484
|
+
const byIdentity = findTimelineDomNode(doc, clip);
|
|
449
485
|
if (byIdentity && !usedNodes.has(byIdentity) && nodeMatchesClipTag(byIdentity, clip))
|
|
450
486
|
return byIdentity;
|
|
451
487
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
2
|
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
3
|
-
import { usePlayerStore, liveTime, type TimelineElement } from "./playerStore";
|
|
3
|
+
import { usePlayerStore, liveTime, whenPreviewBooted, type TimelineElement } from "./playerStore";
|
|
4
4
|
import { thumbnailRevisionOf } from "./thumbnailSlice";
|
|
5
5
|
|
|
6
6
|
/** The playback/selection state `reset()` restores (persistent prefs asserted separately). */
|
|
@@ -343,9 +343,20 @@ describe("usePlayerStore", () => {
|
|
|
343
343
|
const video = doc.createElement("video");
|
|
344
344
|
Object.defineProperty(video, "readyState", { value: 0, configurable: true });
|
|
345
345
|
doc.body.appendChild(video);
|
|
346
|
+
usePlayerStore.getState().markPreviewLoadStep(doc);
|
|
346
347
|
return { doc, video };
|
|
347
348
|
}
|
|
348
349
|
|
|
350
|
+
it("waits for the document's load step before marking it ready", async () => {
|
|
351
|
+
const doc = document.implementation.createHTMLDocument("composition");
|
|
352
|
+
usePlayerStore.getState().requestTimelineReady(doc);
|
|
353
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
354
|
+
expect(usePlayerStore.getState().timelineReady).toBe(false);
|
|
355
|
+
|
|
356
|
+
usePlayerStore.getState().markPreviewLoadStep(doc);
|
|
357
|
+
await vi.waitFor(() => expect(usePlayerStore.getState().timelineReady).toBe(true));
|
|
358
|
+
});
|
|
359
|
+
|
|
349
360
|
it("resolves timelineReady once the doc's media settles", async () => {
|
|
350
361
|
const { doc, video } = stalledVideoDoc();
|
|
351
362
|
usePlayerStore.getState().requestTimelineReady(doc);
|
|
@@ -748,3 +759,26 @@ describe("liveTime", () => {
|
|
|
748
759
|
expect(result).toBe(false);
|
|
749
760
|
});
|
|
750
761
|
});
|
|
762
|
+
|
|
763
|
+
describe("whenPreviewBooted", () => {
|
|
764
|
+
it("resolves false for a project the user left before its preview booted", async () => {
|
|
765
|
+
usePlayerStore.setState({ timelineProjectId: "project-a", previewBooted: false });
|
|
766
|
+
const waitA = whenPreviewBooted("project-a");
|
|
767
|
+
|
|
768
|
+
usePlayerStore.getState().beginTimelineSession("project-b");
|
|
769
|
+
usePlayerStore.getState().markPreviewBooted();
|
|
770
|
+
|
|
771
|
+
await expect(waitA).resolves.toBe(false);
|
|
772
|
+
await expect(whenPreviewBooted("project-b")).resolves.toBe(true);
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
it("waits through the open of its own project, then resolves on that project's boot", async () => {
|
|
776
|
+
usePlayerStore.setState({ timelineProjectId: "project-a", previewBooted: true });
|
|
777
|
+
const waitB = whenPreviewBooted("project-b");
|
|
778
|
+
|
|
779
|
+
usePlayerStore.getState().beginTimelineSession("project-b");
|
|
780
|
+
usePlayerStore.getState().markPreviewBooted();
|
|
781
|
+
|
|
782
|
+
await expect(waitB).resolves.toBe(true);
|
|
783
|
+
});
|
|
784
|
+
});
|
|
@@ -551,3 +551,32 @@ export const usePlayerStore = create<PlayerState>((set, get) => ({
|
|
|
551
551
|
}));
|
|
552
552
|
|
|
553
553
|
attachPlayerStoreDevHandle(usePlayerStore);
|
|
554
|
+
|
|
555
|
+
export function isPreviewBooted(projectId: string): boolean {
|
|
556
|
+
const { previewBooted, timelineProjectId } = usePlayerStore.getState();
|
|
557
|
+
return previewBooted && timelineProjectId === projectId;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** True once projectId's live preview has booted, false once another project replaces it.
|
|
561
|
+
* Open-time work the first frame does not need (server parses, lint) waits on it. */
|
|
562
|
+
export function whenPreviewBooted(projectId: string): Promise<boolean> {
|
|
563
|
+
const openedFrom = usePlayerStore.getState().timelineProjectId;
|
|
564
|
+
let seen = false;
|
|
565
|
+
const settle = (state: PlayerState): boolean | null => {
|
|
566
|
+
if (state.timelineProjectId === projectId) {
|
|
567
|
+
seen = true;
|
|
568
|
+
return state.previewBooted ? true : null;
|
|
569
|
+
}
|
|
570
|
+
return seen || state.timelineProjectId !== openedFrom ? false : null;
|
|
571
|
+
};
|
|
572
|
+
return new Promise((resolve) => {
|
|
573
|
+
const now = settle(usePlayerStore.getState());
|
|
574
|
+
if (now !== null) return resolve(now);
|
|
575
|
+
const stop = usePlayerStore.subscribe((state) => {
|
|
576
|
+
const result = settle(state);
|
|
577
|
+
if (result === null) return;
|
|
578
|
+
stop();
|
|
579
|
+
resolve(result);
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
}
|
|
@@ -8,26 +8,45 @@ import { settleCompositionReadiness } from "@hyperframes/core/composition-readin
|
|
|
8
8
|
|
|
9
9
|
export interface PlaybackReadinessSlice {
|
|
10
10
|
timelineReady: boolean;
|
|
11
|
+
/** Latched when the project's first preview shows its first frame (or fails), kept through edit
|
|
12
|
+
* reloads, so work that must not compete with the boot waits for it once. */
|
|
13
|
+
previewBooted: boolean;
|
|
11
14
|
setTimelineReady: (ready: boolean) => void;
|
|
15
|
+
markPreviewBooted: () => void;
|
|
12
16
|
/** Sets timelineReady once doc's readiness inputs settle, or immediately
|
|
13
|
-
* if doc is null. A wait a later call supersedes never wins the race.
|
|
17
|
+
* if doc is null. A wait a later call supersedes never wins the race.
|
|
18
|
+
* Waits for doc's load step first: it pauses and rewinds the preview, so a
|
|
19
|
+
* Play enabled before it would be undone. */
|
|
14
20
|
requestTimelineReady: (doc: Document | null) => void;
|
|
21
|
+
/** Called by the preview's load step for the document it ran on. */
|
|
22
|
+
markPreviewLoadStep: (doc: Document) => void;
|
|
15
23
|
}
|
|
16
24
|
|
|
17
25
|
let timelineReadyGeneration = 0;
|
|
26
|
+
const loadStepDocs = new WeakSet<Document>();
|
|
27
|
+
let awaitingLoadStep: { doc: Document; generation: number } | null = null;
|
|
18
28
|
|
|
19
29
|
/** For a full timeline reset: bumps the generation so any requestTimelineReady
|
|
20
30
|
* wait in flight can never resolve into what replaced it. */
|
|
21
|
-
export function resetPlaybackReadinessState(): Pick<
|
|
31
|
+
export function resetPlaybackReadinessState(): Pick<
|
|
32
|
+
PlaybackReadinessSlice,
|
|
33
|
+
"timelineReady" | "previewBooted"
|
|
34
|
+
> {
|
|
22
35
|
timelineReadyGeneration++;
|
|
23
|
-
return { timelineReady: false };
|
|
36
|
+
return { timelineReady: false, previewBooted: false };
|
|
24
37
|
}
|
|
25
38
|
|
|
26
39
|
export function createPlaybackReadinessSlice(
|
|
27
40
|
set: StoreApi<PlaybackReadinessSlice>["setState"],
|
|
28
41
|
): PlaybackReadinessSlice {
|
|
42
|
+
const settle = (doc: Document, generation: number) =>
|
|
43
|
+
settleCompositionReadiness(doc, () => {
|
|
44
|
+
if (generation === timelineReadyGeneration) set({ timelineReady: true });
|
|
45
|
+
});
|
|
29
46
|
return {
|
|
30
47
|
timelineReady: false,
|
|
48
|
+
previewBooted: false,
|
|
49
|
+
markPreviewBooted: () => set({ previewBooted: true }),
|
|
31
50
|
setTimelineReady: (ready) => {
|
|
32
51
|
timelineReadyGeneration++;
|
|
33
52
|
set({ timelineReady: ready });
|
|
@@ -35,9 +54,18 @@ export function createPlaybackReadinessSlice(
|
|
|
35
54
|
requestTimelineReady: (doc) => {
|
|
36
55
|
const generation = ++timelineReadyGeneration;
|
|
37
56
|
if (!doc) return set({ timelineReady: true });
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
57
|
+
if (!loadStepDocs.has(doc)) {
|
|
58
|
+
awaitingLoadStep = { doc, generation };
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
settle(doc, generation);
|
|
62
|
+
},
|
|
63
|
+
markPreviewLoadStep: (doc) => {
|
|
64
|
+
loadStepDocs.add(doc);
|
|
65
|
+
if (awaitingLoadStep?.doc !== doc) return;
|
|
66
|
+
const { generation } = awaitingLoadStep;
|
|
67
|
+
awaitingLoadStep = null;
|
|
68
|
+
settle(doc, generation);
|
|
41
69
|
},
|
|
42
70
|
};
|
|
43
71
|
}
|
|
@@ -9,7 +9,6 @@ import { collectHtmlIds } from "./studioHelpers";
|
|
|
9
9
|
import { generateId } from "./generateId";
|
|
10
10
|
import { formatTimelineAttributeNumber } from "../player/components/timelineEditing";
|
|
11
11
|
import { saveProjectFilesWithHistory } from "./studioFileHistory";
|
|
12
|
-
import type { EditHistoryKind } from "./editHistory";
|
|
13
12
|
import { extendRootDurationInSource } from "./rootDuration";
|
|
14
13
|
import { deriveTimelineStoreKeyForDomId } from "../player/lib/timelineElementHelpers";
|
|
15
14
|
|
|
@@ -41,7 +40,6 @@ interface AddBlockOptions {
|
|
|
41
40
|
writeProjectFile: (path: string, content: string) => Promise<void>;
|
|
42
41
|
recordEdit: (entry: {
|
|
43
42
|
label: string;
|
|
44
|
-
kind: EditHistoryKind;
|
|
45
43
|
coalesceKey?: string;
|
|
46
44
|
files: Record<string, { before: string; after: string }>;
|
|
47
45
|
}) => Promise<void>;
|
|
@@ -234,7 +232,6 @@ export async function addBlockToProject(
|
|
|
234
232
|
await saveProjectFilesWithHistory({
|
|
235
233
|
projectId,
|
|
236
234
|
label: `Add ${isBlock ? "block" : "component"}: ${block.title}`,
|
|
237
|
-
kind: "timeline",
|
|
238
235
|
files: { [targetPath]: patchedContent },
|
|
239
236
|
readFile: async () => originalContent,
|
|
240
237
|
writeFile: writeProjectFile,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { prefetchPreviewForHash } from "./previewPrefetch";
|
|
4
|
+
|
|
5
|
+
describe("prefetchPreviewForHash", () => {
|
|
6
|
+
afterEach(() => vi.unstubAllGlobals());
|
|
7
|
+
|
|
8
|
+
it("requests the opening project's preview once, not on every hash update", () => {
|
|
9
|
+
const fetchStub = vi.fn(() => Promise.resolve(new Response("")));
|
|
10
|
+
vi.stubGlobal("fetch", fetchStub);
|
|
11
|
+
|
|
12
|
+
prefetchPreviewForHash("#project/demo");
|
|
13
|
+
prefetchPreviewForHash("#project/demo?t=2&tab=design");
|
|
14
|
+
prefetchPreviewForHash("#project/other");
|
|
15
|
+
prefetchPreviewForHash("#settings");
|
|
16
|
+
|
|
17
|
+
expect(fetchStub.mock.calls.map(([url]) => new URL(String(url)).pathname)).toEqual([
|
|
18
|
+
"/api/projects/demo/preview",
|
|
19
|
+
"/api/projects/other/preview",
|
|
20
|
+
]);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { applyPreviewVariablesToUrl } from "../hooks/previewVariablesStore";
|
|
2
|
+
import { buildProjectApiPath, parseProjectIdFromHash } from "./projectRouting";
|
|
3
|
+
|
|
4
|
+
let prefetched: string | null = null;
|
|
5
|
+
|
|
6
|
+
/** Requests the URL's project preview while Studio boots, so the server builds it in parallel. */
|
|
7
|
+
export function prefetchPreviewForHash(hash: string): void {
|
|
8
|
+
const projectId = parseProjectIdFromHash(hash);
|
|
9
|
+
if (!projectId || projectId === prefetched) return;
|
|
10
|
+
prefetched = projectId;
|
|
11
|
+
const url = new URL(buildProjectApiPath(projectId, "/preview"), window.location.origin);
|
|
12
|
+
applyPreviewVariablesToUrl(url);
|
|
13
|
+
fetch(url).catch(() => undefined);
|
|
14
|
+
}
|
|
@@ -133,7 +133,6 @@ describe("runAtomicCutTransaction", () => {
|
|
|
133
133
|
expect(writeProjectFile).not.toHaveBeenCalled();
|
|
134
134
|
expect(recordEdit).toHaveBeenCalledWith({
|
|
135
135
|
label: "Split timeline clip",
|
|
136
|
-
kind: "timeline",
|
|
137
136
|
files: { "index.html": { before: "before", after: "after" } },
|
|
138
137
|
});
|
|
139
138
|
expect(observe).toHaveBeenCalledWith("index.html", '"v1"');
|
|
@@ -181,7 +181,7 @@ export function runAtomicCutTransaction(input: RunAtomicCutInput): Promise<Atomi
|
|
|
181
181
|
result.files.map((file) => [file.path, { before: file.before, after: file.after }]),
|
|
182
182
|
);
|
|
183
183
|
try {
|
|
184
|
-
await input.recordEdit({ label: input.label,
|
|
184
|
+
await input.recordEdit({ label: input.label, files: snapshots });
|
|
185
185
|
} catch (error) {
|
|
186
186
|
try {
|
|
187
187
|
await rollbackUnrecordedCut(result.files, input.writeProjectFile);
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { openComposition, type Composition } from "@hyperframes/sdk";
|
|
2
|
-
import type { EditHistoryKind } from "./editHistory";
|
|
3
2
|
import { hashContent, markSelfWrite } from "../hooks/sdkSelfWriteRegistry";
|
|
4
3
|
import { trackStudioEvent } from "./studioTelemetry";
|
|
5
4
|
import { serializeStudioFileMutation } from "./studioFileMutationCoordinator";
|
|
@@ -32,7 +31,6 @@ export interface CutoverDeps {
|
|
|
32
31
|
editHistory: {
|
|
33
32
|
recordEdit: (entry: {
|
|
34
33
|
label: string;
|
|
35
|
-
kind: EditHistoryKind;
|
|
36
34
|
coalesceKey?: string;
|
|
37
35
|
coalesceMs?: number;
|
|
38
36
|
files: Record<string, { before: string; after: string }>;
|
|
@@ -248,7 +246,6 @@ async function writeAndRecord(
|
|
|
248
246
|
try {
|
|
249
247
|
await deps.editHistory.recordEdit({
|
|
250
248
|
label: options?.label ?? "Edit layer",
|
|
251
|
-
kind: "manual",
|
|
252
249
|
...(options?.coalesceKey ? { coalesceKey: options.coalesceKey } : {}),
|
|
253
250
|
...(options?.coalesceMs != null ? { coalesceMs: options.coalesceMs } : {}),
|
|
254
251
|
files: { [targetPath]: { before: originalContent, after } },
|
|
@@ -11,7 +11,6 @@ describe("saveProjectFilesWithHistory", () => {
|
|
|
11
11
|
await saveProjectFilesWithHistory({
|
|
12
12
|
projectId: "project-1",
|
|
13
13
|
label: "Move layer",
|
|
14
|
-
kind: "manual",
|
|
15
14
|
files: { "index.html": "after" },
|
|
16
15
|
readFile: async (path) => reads[path],
|
|
17
16
|
writeFile: async (path, content) => {
|
|
@@ -23,7 +22,6 @@ describe("saveProjectFilesWithHistory", () => {
|
|
|
23
22
|
expect(writes).toEqual({ "index.html": "after" });
|
|
24
23
|
expect(recordEdit).toHaveBeenCalledWith({
|
|
25
24
|
label: "Move layer",
|
|
26
|
-
kind: "manual",
|
|
27
25
|
coalesceKey: undefined,
|
|
28
26
|
files: { "index.html": { before: "before", after: "after" } },
|
|
29
27
|
});
|
|
@@ -42,7 +40,6 @@ describe("saveProjectFilesWithHistory", () => {
|
|
|
42
40
|
await saveProjectFilesWithHistory({
|
|
43
41
|
projectId: "project-1",
|
|
44
42
|
label: "Delete timeline clip",
|
|
45
|
-
kind: "timeline",
|
|
46
43
|
files: { "index.html": "removed+shrunk" },
|
|
47
44
|
readFile: async () => "original",
|
|
48
45
|
diskContent: { "index.html": "removed" },
|
|
@@ -67,7 +64,6 @@ describe("saveProjectFilesWithHistory", () => {
|
|
|
67
64
|
await saveProjectFilesWithHistory({
|
|
68
65
|
projectId: "project-1",
|
|
69
66
|
label: "Move layer",
|
|
70
|
-
kind: "manual",
|
|
71
67
|
files: { "index.html": "after" },
|
|
72
68
|
readFile: async () => "before",
|
|
73
69
|
writeFile: async (path, _content, expectedContent) => {
|
|
@@ -85,7 +81,6 @@ describe("saveProjectFilesWithHistory", () => {
|
|
|
85
81
|
const changedPaths = await saveProjectFilesWithHistory({
|
|
86
82
|
projectId: "project-1",
|
|
87
83
|
label: "Edit layer",
|
|
88
|
-
kind: "manual",
|
|
89
84
|
files: { "index.html": "same" },
|
|
90
85
|
readFile: async () => "same",
|
|
91
86
|
writeFile,
|
|
@@ -109,7 +104,6 @@ describe("saveProjectFilesWithHistory", () => {
|
|
|
109
104
|
saveProjectFilesWithHistory({
|
|
110
105
|
projectId: "project-1",
|
|
111
106
|
label: "Move layer",
|
|
112
|
-
kind: "manual",
|
|
113
107
|
files: {
|
|
114
108
|
"index.html": "index-after",
|
|
115
109
|
"scene.html": "scene-after",
|
|
@@ -144,7 +138,6 @@ describe("saveProjectFilesWithHistory", () => {
|
|
|
144
138
|
saveProjectFilesWithHistory({
|
|
145
139
|
projectId: "project-1",
|
|
146
140
|
label: "Move layer",
|
|
147
|
-
kind: "manual",
|
|
148
141
|
files: {
|
|
149
142
|
"index.html": "index-after",
|
|
150
143
|
"scene.html": "scene-after",
|
|
@@ -178,7 +171,6 @@ describe("saveProjectFilesWithHistory", () => {
|
|
|
178
171
|
saveProjectFilesWithHistory({
|
|
179
172
|
projectId: "project-1",
|
|
180
173
|
label: "Move layer",
|
|
181
|
-
kind: "manual",
|
|
182
174
|
files: {
|
|
183
175
|
"index.html": "index-after",
|
|
184
176
|
"scene.html": "scene-after",
|
|
@@ -223,7 +215,6 @@ describe("saveProjectFilesWithHistory", () => {
|
|
|
223
215
|
const save = saveProjectFilesWithHistory({
|
|
224
216
|
projectId: "project-1",
|
|
225
217
|
label: "Edit source",
|
|
226
|
-
kind: "source",
|
|
227
218
|
files: { "index.html": "editor-after" },
|
|
228
219
|
readFile,
|
|
229
220
|
writeFile,
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { buildProjectApiPath } from "./projectRouting";
|
|
2
2
|
import type { MutableRefObject } from "react";
|
|
3
|
-
import type { EditHistoryKind } from "./editHistory";
|
|
4
3
|
import { serializeStudioFileMutations } from "./studioFileMutationCoordinator";
|
|
5
4
|
import { createStudioSaveHttpError } from "./studioSaveDiagnostics";
|
|
6
5
|
|
|
7
6
|
export interface RecordEditInput {
|
|
8
7
|
label: string;
|
|
9
|
-
kind: EditHistoryKind;
|
|
10
8
|
coalesceKey?: string;
|
|
11
9
|
coalesceMs?: number;
|
|
12
10
|
files: Record<string, { before: string; after: string }>;
|
|
@@ -27,7 +25,6 @@ type ProjectFileWriter = (path: string, content: string, expectedContent?: strin
|
|
|
27
25
|
interface SaveProjectFilesWithHistoryInput {
|
|
28
26
|
projectId: string;
|
|
29
27
|
label: string;
|
|
30
|
-
kind: EditHistoryKind;
|
|
31
28
|
coalesceKey?: string;
|
|
32
29
|
coalesceMs?: number;
|
|
33
30
|
files: Record<string, string>;
|
|
@@ -65,7 +62,6 @@ export async function readProjectFileContent(pid: string, path: string): Promise
|
|
|
65
62
|
|
|
66
63
|
export async function saveProjectFilesWithHistory({
|
|
67
64
|
label,
|
|
68
|
-
kind,
|
|
69
65
|
coalesceKey,
|
|
70
66
|
coalesceMs,
|
|
71
67
|
files,
|
|
@@ -93,7 +89,7 @@ export async function saveProjectFilesWithHistory({
|
|
|
93
89
|
writtenPaths.push(path);
|
|
94
90
|
}
|
|
95
91
|
|
|
96
|
-
await recordEdit({ label,
|
|
92
|
+
await recordEdit({ label, coalesceKey, coalesceMs, files: snapshots });
|
|
97
93
|
} catch (error) {
|
|
98
94
|
try {
|
|
99
95
|
for (const path of writtenPaths.reverse()) {
|
|
@@ -70,7 +70,6 @@ export async function commitTimelineCompositionInsertion(input: {
|
|
|
70
70
|
try {
|
|
71
71
|
await input.recordEdit({
|
|
72
72
|
label: "Add composition to timeline",
|
|
73
|
-
kind: "timeline",
|
|
74
73
|
files: { [input.targetPath]: { before: result.before, after: result.after } },
|
|
75
74
|
});
|
|
76
75
|
} catch (error) {
|