@hyperframes/studio 0.7.55 → 0.7.57
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--Z69cEkE.js +459 -0
- package/dist/assets/{index-CPetwHFV.js → index-Bf1x1y8H.js} +1 -1
- package/dist/assets/index-DfmYkU44.js +423 -0
- package/dist/assets/{index-BXaqaVKt.js → index-KsfE1bUu.js} +1 -1
- package/dist/assets/index-_pqzyxB1.css +1 -0
- package/dist/{chunk-SOTCF4DF.js → chunk-5QSIMBEJ.js} +2 -1
- package/dist/chunk-5QSIMBEJ.js.map +1 -0
- package/dist/{domEditingLayers-2ECJK24D.js → domEditingLayers-6LQGKPOI.js} +2 -2
- package/dist/index.d.ts +172 -144
- package/dist/index.html +2 -2
- package/dist/index.js +43613 -40346
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +106 -96
- package/src/components/EditorShell.tsx +253 -0
- package/src/components/StudioGlobalDragOverlay.tsx +1 -3
- package/src/components/StudioLeftSidebar.tsx +13 -3
- package/src/components/StudioRightPanel.tsx +8 -3
- package/src/components/TimelineToolbar.test.tsx +2 -2
- package/src/components/TimelineToolbar.tsx +179 -121
- package/src/components/editor/CanvasContextMenu.test.tsx +152 -1
- package/src/components/editor/CanvasContextMenu.tsx +33 -18
- package/src/components/editor/DomEditCropHandles.test.tsx +74 -3
- package/src/components/editor/DomEditCropHandles.tsx +64 -29
- package/src/components/editor/DomEditOverlay.test.ts +245 -279
- package/src/components/editor/DomEditOverlay.tsx +126 -125
- package/src/components/editor/DomEditOverlayHover.test.tsx +90 -0
- package/src/components/editor/DomEditRotateHandle.tsx +38 -18
- package/src/components/editor/DomEditSelectionChrome.test.tsx +80 -0
- package/src/components/editor/DomEditSelectionChrome.tsx +257 -0
- package/src/components/editor/LayersPanel.test.ts +66 -2
- package/src/components/editor/LayersPanel.tsx +53 -2
- package/src/components/editor/OffCanvasIndicators.test.tsx +34 -0
- package/src/components/editor/OffCanvasIndicators.tsx +49 -10
- package/src/components/editor/SnapGuideOverlay.tsx +28 -23
- package/src/components/editor/SnapToolbar.test.tsx +0 -1
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +214 -0
- package/src/components/editor/anchoredResizeReleaseShift.test.ts +106 -10
- package/src/components/editor/canvasContextMenuZOrder.test.ts +186 -2
- package/src/components/editor/canvasContextMenuZOrder.ts +144 -22
- package/src/components/editor/domEditNudge.test.ts +80 -0
- package/src/components/editor/domEditNudge.ts +44 -0
- package/src/components/editor/domEditOverlayGeometry.test.ts +167 -1
- package/src/components/editor/domEditOverlayGeometry.ts +280 -20
- package/src/components/editor/domEditOverlayGestures.ts +44 -41
- package/src/components/editor/domEditOverlayStartGesture.ts +62 -2
- package/src/components/editor/domEditResizeLocal.test.ts +131 -0
- package/src/components/editor/domEditResizeLocal.ts +125 -0
- package/src/components/editor/domEditingDom.ts +1 -1
- package/src/components/editor/manualEditsDomPatches.test.ts +47 -0
- package/src/components/editor/manualOffsetDrag.ts +55 -12
- package/src/components/editor/offCanvasIndicatorGeometry.ts +31 -10
- package/src/components/editor/offCanvasIndicatorRefresh.test.tsx +25 -1
- package/src/components/editor/propertyPanelColorGradingSection.tsx +15 -21
- package/src/components/editor/resizeDraft.ts +108 -0
- package/src/components/editor/snapEngine.test.ts +12 -78
- package/src/components/editor/snapEngine.ts +13 -53
- package/src/components/editor/useCanvasContextMenuState.ts +92 -0
- package/src/components/editor/useDomEditNudge.test.tsx +228 -0
- package/src/components/editor/useDomEditNudge.ts +254 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +115 -116
- package/src/components/editor/useDomEditOverlayRects.ts +9 -2
- package/src/components/editor/useZOrderCrossedFlash.tsx +68 -0
- package/src/components/nle/AssetPreviewOverlay.test.tsx +106 -0
- package/src/components/nle/AssetPreviewOverlay.tsx +171 -0
- package/src/components/nle/NLEContext.test.ts +144 -0
- package/src/components/nle/NLEContext.tsx +328 -0
- package/src/components/nle/NLEPreview.tsx +1 -1
- package/src/components/nle/PreviewOverlays.tsx +260 -0
- package/src/components/nle/PreviewPane.tsx +163 -0
- package/src/components/nle/TimelinePane.test.ts +60 -0
- package/src/components/nle/TimelinePane.tsx +293 -0
- package/src/components/nle/TimelineResizeDivider.tsx +8 -2
- package/src/components/nle/useCompositionStack.test.tsx +44 -0
- package/src/components/nle/useCompositionStack.ts +17 -9
- package/src/components/nle/useTimelineEditCallbacks.ts +217 -0
- package/src/components/sidebar/AssetCard.test.tsx +106 -0
- package/src/components/sidebar/AssetCard.tsx +343 -0
- package/src/components/sidebar/AssetContextMenu.tsx +15 -30
- package/src/components/sidebar/AssetsTab.test.ts +96 -0
- package/src/components/sidebar/AssetsTab.tsx +71 -206
- package/src/components/sidebar/AudioRow.tsx +56 -2
- package/src/components/sidebar/BlocksTab.tsx +8 -1
- package/src/components/sidebar/LeftSidebar.tsx +4 -1
- package/src/components/sidebar/assetHelpers.ts +29 -0
- package/src/contexts/DomEditContext.tsx +9 -0
- package/src/contexts/StudioContext.tsx +9 -8
- package/src/contexts/TimelineEditContext.tsx +3 -7
- package/src/hooks/deleteSelectedKeyframes.ts +35 -0
- package/src/hooks/domEditCommitTypes.ts +23 -1
- package/src/hooks/domSelectionTestHarness.ts +14 -0
- package/src/hooks/gestureTransaction.test.ts +311 -0
- package/src/hooks/gestureTransaction.ts +199 -0
- package/src/hooks/gsapDragCommit.test.ts +141 -41
- package/src/hooks/gsapDragCommit.ts +64 -74
- package/src/hooks/gsapDragStaticSetHelpers.ts +9 -21
- package/src/hooks/gsapResizeIntercept.test.ts +69 -0
- package/src/hooks/gsapResizeIntercept.ts +38 -10
- package/src/hooks/gsapRuntimeBridge.test.ts +49 -13
- package/src/hooks/gsapRuntimeBridge.ts +4 -4
- package/src/hooks/gsapScriptCommitTypes.ts +16 -5
- package/src/hooks/gsapShared.test.ts +19 -1
- package/src/hooks/gsapShared.ts +14 -0
- package/src/hooks/patchDocumentRootDuration.test.ts +71 -0
- package/src/hooks/timelineEditingGsap.ts +14 -0
- package/src/hooks/timelineEditingHelpers.test.ts +125 -1
- package/src/hooks/timelineEditingHelpers.ts +53 -292
- package/src/hooks/timelineMoveAdapter.test.ts +81 -0
- package/src/hooks/timelineMoveAdapter.ts +39 -0
- package/src/hooks/timelineTimingSync.test.ts +150 -0
- package/src/hooks/timelineTimingSync.ts +412 -0
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +36 -8
- package/src/hooks/useAnimatedPropertyCommit.ts +31 -26
- package/src/hooks/useAppHotkeys.ts +16 -25
- package/src/hooks/useBlockHandlers.ts +55 -27
- package/src/hooks/useCaptionDetection.ts +2 -0
- package/src/hooks/useCompositionDimensions.ts +32 -5
- package/src/hooks/useContextMenuDismiss.ts +31 -7
- package/src/hooks/useDomEditCommits.test.tsx +245 -1
- package/src/hooks/useDomEditCommits.ts +162 -3
- package/src/hooks/useDomEditPositionPatchCommit.ts +2 -0
- package/src/hooks/useDomEditSession.ts +2 -2
- package/src/hooks/useDomEditTextCommits.test.tsx +135 -0
- package/src/hooks/useDomEditTextCommits.ts +8 -3
- package/src/hooks/useDomEditWiring.ts +7 -9
- package/src/hooks/useDomGeometryCommits.ts +17 -2
- package/src/hooks/useDomSelection.test.ts +2 -36
- package/src/hooks/useDomSelection.ts +81 -51
- package/src/hooks/useDomSelectionSelectionGuards.test.ts +276 -0
- package/src/hooks/useElementLifecycleOps.test.tsx +423 -0
- package/src/hooks/useElementLifecycleOps.ts +53 -71
- package/src/hooks/useElementPicker.ts +7 -8
- package/src/hooks/useEnableKeyframes.test.ts +142 -1
- package/src/hooks/useEnableKeyframes.ts +73 -29
- package/src/hooks/useGestureCommit.test.tsx +130 -0
- package/src/hooks/useGestureCommit.ts +32 -8
- package/src/hooks/useGroupCommits.ts +7 -28
- package/src/hooks/useGsapAwareEditing.test.tsx +239 -0
- package/src/hooks/useGsapAwareEditing.ts +115 -20
- package/src/hooks/useGsapKeyframeOps.test.tsx +59 -0
- package/src/hooks/useGsapKeyframeOps.ts +38 -10
- package/src/hooks/useGsapScriptCommits.test.tsx +40 -1
- package/src/hooks/useGsapScriptCommits.ts +134 -50
- package/src/hooks/useGsapSelectionHandlers.ts +49 -16
- package/src/hooks/useMusicBeatAnalysis.ts +72 -36
- package/src/hooks/usePersistentEditHistory.test.ts +33 -0
- package/src/hooks/usePersistentEditHistory.ts +92 -58
- package/src/hooks/usePreviewPersistence.ts +25 -7
- package/src/hooks/useRazorSplit.history.test.tsx +148 -18
- package/src/hooks/useRazorSplit.test.ts +269 -0
- package/src/hooks/useRazorSplit.testHelpers.ts +64 -0
- package/src/hooks/useRazorSplit.ts +107 -36
- package/src/hooks/useRenderClipContent.ts +24 -6
- package/src/hooks/useStudioContextValue.ts +15 -5
- package/src/hooks/useStudioUrlState.ts +2 -4
- package/src/hooks/useTimelineAssetDropOps.ts +175 -0
- package/src/hooks/useTimelineEditing.test.tsx +398 -241
- package/src/hooks/useTimelineEditing.ts +181 -265
- package/src/hooks/useTimelineEditingTypes.ts +6 -0
- package/src/hooks/useTimelineGroupEditing.ts +167 -132
- package/src/index.ts +1 -1
- package/src/player/components/ImageThumbnail.test.tsx +173 -0
- package/src/player/components/ImageThumbnail.tsx +160 -0
- package/src/player/components/PlayerControls.tsx +5 -5
- package/src/player/components/PlayheadIndicator.tsx +55 -5
- package/src/player/components/Timeline.test.ts +183 -23
- package/src/player/components/Timeline.tsx +194 -194
- package/src/player/components/TimelineCanvas.tsx +218 -523
- package/src/player/components/TimelineClip.tsx +4 -1
- package/src/player/components/TimelineClipDiamonds.tsx +9 -40
- package/src/player/components/TimelineEmptyState.tsx +3 -1
- package/src/player/components/TimelineLanes.tsx +487 -0
- package/src/player/components/TimelineOverlays.tsx +122 -0
- package/src/player/components/TimelineRuler.tsx +58 -48
- package/src/player/components/VideoThumbnail.test.tsx +152 -0
- package/src/player/components/VideoThumbnail.tsx +28 -7
- package/src/player/components/thumbnailUtils.test.ts +127 -0
- package/src/player/components/thumbnailUtils.ts +54 -0
- package/src/player/components/timelineCallbacks.ts +18 -18
- package/src/player/components/timelineClipChildren.tsx +39 -0
- package/src/player/components/timelineClipDragCommit.test.ts +1026 -0
- package/src/player/components/timelineClipDragCommit.ts +498 -0
- package/src/player/components/timelineClipDragPreview.test.ts +144 -0
- package/src/player/components/timelineClipDragPreview.ts +323 -38
- package/src/player/components/timelineClipDragTypes.ts +65 -0
- package/src/player/components/timelineCollision.test.ts +42 -2
- package/src/player/components/timelineDragDrop.ts +69 -60
- package/src/player/components/timelineEditCapabilities.ts +52 -0
- package/src/player/components/timelineEditing.test.ts +1 -1
- package/src/player/components/timelineEditing.ts +76 -59
- package/src/player/components/timelineGroupEditing.test.ts +126 -0
- package/src/player/components/timelineGroupEditing.ts +133 -0
- package/src/player/components/timelineGroupResizeCommit.ts +54 -0
- package/src/player/components/timelineLayout.test.ts +95 -23
- package/src/player/components/timelineLayout.ts +207 -75
- package/src/player/components/timelineMarquee.test.ts +197 -0
- package/src/player/components/timelineMarquee.ts +112 -0
- package/src/player/components/timelineOptimisticRevision.ts +42 -0
- package/src/player/components/timelineRevealScroll.test.ts +133 -0
- package/src/player/components/timelineRevealScroll.ts +91 -0
- package/src/player/components/timelineStackingSync.test.ts +60 -0
- package/src/player/components/timelineStackingSync.ts +27 -2
- package/src/player/components/timelineTheme.ts +6 -1
- package/src/player/components/timelineTrackPersistPipeline.test.ts +168 -0
- package/src/player/components/timelineZones.test.ts +122 -310
- package/src/player/components/timelineZones.ts +82 -135
- package/src/player/components/useResolvedTimelineEditCallbacks.ts +25 -3
- package/src/player/components/useTimelineClipDrag.resize.test.tsx +241 -0
- package/src/player/components/useTimelineClipDrag.ts +380 -386
- package/src/player/components/useTimelineEditPinning.ts +121 -0
- package/src/player/components/useTimelineGeometry.ts +129 -0
- package/src/player/components/useTimelinePlayhead.ts +4 -16
- package/src/player/components/useTimelineRangeSelection.ts +341 -50
- package/src/player/components/useTimelineRevealClip.ts +56 -0
- package/src/player/components/useTimelineStackingSync.test.tsx +77 -0
- package/src/player/components/useTimelineStackingSync.ts +94 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +7 -3
- package/src/player/hooks/usePlaybackKeyboard.ts +5 -0
- package/src/player/hooks/useTimelinePlayer.seek.test.ts +55 -0
- package/src/player/hooks/useTimelinePlayer.test.ts +36 -3
- package/src/player/hooks/useTimelinePlayer.ts +52 -53
- package/src/player/hooks/useTimelinePlayerLoop.ts +85 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +9 -1
- package/src/player/index.ts +5 -2
- package/src/player/lib/runtimeProtocol.test.ts +48 -0
- package/src/player/lib/runtimeProtocol.ts +65 -0
- package/src/player/lib/time.test.ts +19 -19
- package/src/player/lib/time.ts +6 -2
- package/src/player/lib/timelineDOM.test.ts +25 -98
- package/src/player/lib/timelineDOM.ts +21 -81
- package/src/player/lib/timelineElementHelpers.ts +73 -5
- package/src/player/lib/timelineIframeHelpers.ts +4 -4
- package/src/player/store/playerStore.test.ts +38 -13
- package/src/player/store/playerStore.ts +99 -3
- package/src/styles/studio.css +27 -0
- package/src/utils/assetPreviewDismiss.test.ts +27 -0
- package/src/utils/assetPreviewDismiss.ts +29 -0
- package/src/utils/assetPreviewStore.ts +33 -0
- package/src/utils/blockInstaller.ts +10 -2
- package/src/utils/editHistory.test.ts +35 -0
- package/src/utils/gsapSoftReload.test.ts +119 -1
- package/src/utils/gsapSoftReload.ts +153 -0
- package/src/utils/mediaTypes.ts +3 -2
- package/src/utils/resizeDebug.ts +55 -0
- package/src/utils/rootDuration.test.ts +90 -1
- package/src/utils/rootDuration.ts +76 -13
- package/src/utils/sdkCutover.ts +4 -0
- package/src/utils/studioFileHistory.ts +37 -7
- package/src/utils/studioHelpers.ts +62 -0
- package/src/utils/studioPreviewHelpers.test.ts +65 -8
- package/src/utils/studioPreviewHelpers.ts +10 -0
- package/src/utils/studioTelemetry.ts +4 -1
- package/src/utils/studioUrlState.test.ts +0 -1
- package/src/utils/timelineAssetDrop.test.ts +144 -52
- package/src/utils/timelineAssetDrop.ts +67 -29
- package/src/utils/timelineDiscovery.ts +0 -17
- package/dist/assets/hyperframes-player-BBrKzTGd.js +0 -459
- package/dist/assets/index-BRwkMj0w.js +0 -423
- package/dist/assets/index-Dq7FEg0K.css +0 -1
- package/dist/chunk-SOTCF4DF.js.map +0 -1
- package/src/components/StudioPreviewArea.tsx +0 -500
- package/src/components/nle/NLELayout.test.ts +0 -12
- package/src/components/nle/NLELayout.tsx +0 -591
- package/src/player/components/TimelineLayerGutter.tsx +0 -61
- package/src/player/components/TimelineSelectionOverlays.tsx +0 -55
- package/src/player/components/timelineMarqueeSelection.test.ts +0 -87
- package/src/player/components/timelineSnapTargets.test.ts +0 -144
- package/src/player/components/timelineSnapTargets.ts +0 -164
- package/src/player/components/useTimelineClipDrag.test.tsx +0 -506
- package/src/player/components/useTimelineClipGroupDrag.ts +0 -417
- package/src/player/components/useTimelineMarqueeSelection.test.tsx +0 -231
- package/src/player/components/useTimelineMarqueeSelection.ts +0 -276
- package/src/utils/timelineDiscovery.test.ts +0 -90
- /package/dist/{domEditingLayers-2ECJK24D.js.map → domEditingLayers-6LQGKPOI.js.map} +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useMemo, useRef } from "react";
|
|
2
2
|
import { findUnsafeMutationValues } from "@hyperframes/core/studio-api/finite-mutation";
|
|
3
|
+
import { readProjectFileContent as readSharedProjectFileContent } from "../utils/studioFileHistory";
|
|
3
4
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
4
5
|
import { usePlayerStore } from "../player/store/playerStore";
|
|
5
6
|
import { applySoftReload, extractGsapScriptText } from "../utils/gsapSoftReload";
|
|
@@ -15,6 +16,8 @@ import {
|
|
|
15
16
|
readJsonResponseBody,
|
|
16
17
|
} from "./gsapScriptCommitHelpers";
|
|
17
18
|
import type {
|
|
19
|
+
CommitMutation,
|
|
20
|
+
CommitMutationCall,
|
|
18
21
|
CommitMutationOptions,
|
|
19
22
|
GsapScriptCommitsParams,
|
|
20
23
|
MutationResult,
|
|
@@ -47,6 +50,76 @@ async function mutateGsapScript(
|
|
|
47
50
|
return result;
|
|
48
51
|
}
|
|
49
52
|
|
|
53
|
+
async function mutateGsapScriptBatch(
|
|
54
|
+
projectId: string,
|
|
55
|
+
sourceFile: string,
|
|
56
|
+
mutations: Record<string, unknown>[],
|
|
57
|
+
): Promise<MutationResult> {
|
|
58
|
+
const res = await fetch(
|
|
59
|
+
`/api/projects/${encodeURIComponent(projectId)}/gsap-mutations-batch/${encodeURIComponent(sourceFile)}`,
|
|
60
|
+
{
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: { "Content-Type": "application/json" },
|
|
63
|
+
body: JSON.stringify({ mutations }),
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
if (!res.ok) throw new GsapMutationHttpError(res.status, await readJsonResponseBody(res));
|
|
67
|
+
const result = (await res.json()) as MutationResult;
|
|
68
|
+
if (!result.ok) throw new Error(`Failed to update GSAP in ${sourceFile}`);
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
type ShowToast = (message: string, tone?: "error" | "info") => void;
|
|
73
|
+
|
|
74
|
+
async function runMutationRequest(
|
|
75
|
+
mutations: Record<string, unknown>[],
|
|
76
|
+
options: CommitMutationOptions,
|
|
77
|
+
showToast: ShowToast | undefined,
|
|
78
|
+
request: () => Promise<MutationResult>,
|
|
79
|
+
): Promise<MutationResult | undefined> {
|
|
80
|
+
const unsafeFields = mutations.flatMap((mutation) => findUnsafeMutationValues(mutation));
|
|
81
|
+
if (unsafeFields.length > 0) {
|
|
82
|
+
showToast?.("Couldn't read element layout — try again at a different playhead time", "error");
|
|
83
|
+
if (options.skipReload) return;
|
|
84
|
+
throw new Error(
|
|
85
|
+
`Mutation contains unsafe values: ${unsafeFields.map((field) => field.path).join(", ")}`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
return await request();
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (error instanceof GsapMutationHttpError)
|
|
92
|
+
showToast?.(formatGsapMutationRejectionToast(error), "error");
|
|
93
|
+
if (options.skipReload) return;
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function finishUnchangedMutation(
|
|
99
|
+
iframe: HTMLIFrameElement | null,
|
|
100
|
+
result: MutationResult,
|
|
101
|
+
options: CommitMutationOptions,
|
|
102
|
+
reloadPreview: () => void,
|
|
103
|
+
): boolean {
|
|
104
|
+
if (result.changed !== false) return false;
|
|
105
|
+
if (!options.skipReload && options.instantPatch) {
|
|
106
|
+
applyPreviewSync(iframe, result, options, reloadPreview);
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function refreshMutationPreview(
|
|
112
|
+
iframe: HTMLIFrameElement | null,
|
|
113
|
+
result: MutationResult,
|
|
114
|
+
options: CommitMutationOptions,
|
|
115
|
+
reloadPreview: () => void,
|
|
116
|
+
onCacheInvalidate: () => void,
|
|
117
|
+
): void {
|
|
118
|
+
options.beforeReload?.();
|
|
119
|
+
applyPreviewSync(iframe, result, options, reloadPreview);
|
|
120
|
+
onCacheInvalidate();
|
|
121
|
+
}
|
|
122
|
+
|
|
50
123
|
/**
|
|
51
124
|
* Apply a soft reload and enforce the U4 invariant via the richer
|
|
52
125
|
* `SoftReloadResult`, with telemetry on every non-success path so the invariant
|
|
@@ -141,63 +214,78 @@ export function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIfra
|
|
|
141
214
|
// the same animationId so their POSTs can't interleave. Held in a ref so the
|
|
142
215
|
// chain survives re-renders.
|
|
143
216
|
const serializerRef = useRef(createKeyedSerializer());
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
} catch (error) {
|
|
159
|
-
if (error instanceof GsapMutationHttpError) showToast?.(formatGsapMutationRejectionToast(error), "error");
|
|
160
|
-
if (options.skipReload) return;
|
|
161
|
-
throw error;
|
|
162
|
-
}
|
|
163
|
-
if (result.changed === false) {
|
|
164
|
-
// The FILE already matched, but a deferred instant patch may still be
|
|
165
|
-
// owed to the RUNTIME: paired commits (x with skipReload, then y carrying
|
|
166
|
-
// the patch for both) rely on the SECOND commit to sync the preview — if
|
|
167
|
-
// that half happens to be a no-op (a purely-horizontal drag or resize
|
|
168
|
-
// compensation), returning here would leave the runtime showing the old
|
|
169
|
-
// value while the file holds the new one. Patching in place is idempotent
|
|
170
|
-
// when the values truly match everywhere.
|
|
171
|
-
if (!options.skipReload && options.instantPatch) {
|
|
172
|
-
applyPreviewSync(previewIframeRef.current, result, options, reloadPreview);
|
|
173
|
-
}
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
217
|
+
const recordMutationEdit = useCallback(async (targetPath: string, result: MutationResult, options: CommitMutationOptions) => {
|
|
218
|
+
if (result.before == null || result.after == null) return;
|
|
219
|
+
await editHistory.recordEdit({
|
|
220
|
+
label: options.label,
|
|
221
|
+
kind: "manual",
|
|
222
|
+
coalesceKey: options.coalesceKey,
|
|
223
|
+
coalesceMs: options.coalesceMs,
|
|
224
|
+
files: { [targetPath]: { before: result.before, after: result.after } },
|
|
225
|
+
});
|
|
226
|
+
}, [editHistory]);
|
|
227
|
+
|
|
228
|
+
const finalizeSuccessfulMutation = useCallback(async (selection: DomEditSelection, mutation: Record<string, unknown>, targetPath: string, result: MutationResult, options: CommitMutationOptions) => {
|
|
229
|
+
// A no-op file write may still owe the runtime a deferred instant patch.
|
|
230
|
+
if (finishUnchangedMutation(previewIframeRef.current, result, options, reloadPreview)) return;
|
|
176
231
|
domEditSaveTimestampRef.current = Date.now();
|
|
177
|
-
|
|
178
|
-
await editHistory.recordEdit({ label: options.label, kind: "manual", coalesceKey: options.coalesceKey, files: { [targetPath]: { before: result.before, after: result.after } } });
|
|
179
|
-
}
|
|
232
|
+
await recordMutationEdit(targetPath, result, options);
|
|
180
233
|
if (result.after != null) onFileContentChanged?.(targetPath, result.after);
|
|
181
234
|
// Server wrote the file; the in-memory SDK doc is now stale. Resync it so a
|
|
182
235
|
// later SDK-routed edit doesn't serialize the pre-write doc and revert this.
|
|
183
236
|
forceReloadSdkSession?.();
|
|
184
237
|
if (options.skipReload) return;
|
|
185
238
|
if (result.parsed?.animations) updateKeyframeCacheFromParsed(result.parsed.animations, targetPath, selection.id ?? undefined, mutation);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
239
|
+
refreshMutationPreview(
|
|
240
|
+
previewIframeRef.current,
|
|
241
|
+
result,
|
|
242
|
+
options,
|
|
243
|
+
reloadPreview,
|
|
244
|
+
onCacheInvalidate,
|
|
245
|
+
);
|
|
246
|
+
}, [previewIframeRef, domEditSaveTimestampRef, reloadPreview, onCacheInvalidate, onFileContentChanged, forceReloadSdkSession, recordMutationEdit]);
|
|
247
|
+
|
|
248
|
+
const runCommit = useCallback(async (selection: DomEditSelection, mutation: Record<string, unknown>, options: CommitMutationOptions) => {
|
|
249
|
+
const pid = projectIdRef.current;
|
|
250
|
+
if (!pid) return;
|
|
251
|
+
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
|
252
|
+
const result = await runMutationRequest([mutation], options, showToast, () =>
|
|
253
|
+
mutateGsapScript(pid, targetPath, mutation),
|
|
254
|
+
);
|
|
255
|
+
if (!result) return;
|
|
256
|
+
await finalizeSuccessfulMutation(selection, mutation, targetPath, result, options);
|
|
257
|
+
}, [projectIdRef, activeCompPath, showToast, finalizeSuccessfulMutation]);
|
|
258
|
+
|
|
259
|
+
const runBatchCommit = useCallback(async (calls: CommitMutationCall[], options: CommitMutationOptions) => {
|
|
260
|
+
const pid = projectIdRef.current;
|
|
261
|
+
const first = calls[0];
|
|
262
|
+
const last = calls.at(-1);
|
|
263
|
+
if (!pid || !first || !last) return;
|
|
264
|
+
const targetPath = first.selection.sourceFile || activeCompPath || "index.html";
|
|
265
|
+
const mutations = calls.map(({ mutation }) => mutation);
|
|
266
|
+
const result = await runMutationRequest(mutations, options, showToast, () =>
|
|
267
|
+
mutateGsapScriptBatch(pid, targetPath, mutations),
|
|
268
|
+
);
|
|
269
|
+
if (!result) return;
|
|
270
|
+
await finalizeSuccessfulMutation(last.selection, last.mutation, targetPath, result, options);
|
|
271
|
+
}, [projectIdRef, activeCompPath, showToast, finalizeSuccessfulMutation]);
|
|
272
|
+
|
|
190
273
|
// Every GSAP-script commit is a read-modify-write of one file. Overlapping
|
|
191
274
|
// commits to the SAME file (any op type, any animation) interleave server-side,
|
|
192
275
|
// so serialize per target file by default; an explicit serializeKey overrides.
|
|
193
|
-
const commitMutation =
|
|
194
|
-
(selection
|
|
276
|
+
const commitMutation = useMemo<CommitMutation>(() => {
|
|
277
|
+
const commit: CommitMutation = (selection, mutation, options) => {
|
|
195
278
|
const file = selection.sourceFile || activeCompPath || "index.html";
|
|
196
279
|
const key = options.serializeKey ?? `gsap-file:${file}`;
|
|
197
280
|
return serializerRef.current(key, () => runCommit(selection, mutation, options));
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
|
|
281
|
+
};
|
|
282
|
+
commit.batch = (calls, options) => {
|
|
283
|
+
const file = calls[0]?.selection.sourceFile || activeCompPath || "index.html";
|
|
284
|
+
const key = options.serializeKey ?? `gsap-file:${file}`;
|
|
285
|
+
return serializerRef.current(key, () => runBatchCommit(calls, options));
|
|
286
|
+
};
|
|
287
|
+
return commit;
|
|
288
|
+
}, [runCommit, runBatchCommit, activeCompPath]);
|
|
201
289
|
const trackGsapSaveFailure = useGsapSaveFailureTelemetry(activeCompPath);
|
|
202
290
|
const commitMutationSafely = useSafeGsapCommitMutation(commitMutation, trackGsapSaveFailure, showToast);
|
|
203
291
|
|
|
@@ -238,14 +326,10 @@ export function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIfra
|
|
|
238
326
|
// exact prior content as its undo `before` (matching the style/delete paths),
|
|
239
327
|
// instead of a normalized full-DOM re-emit that would reformat the whole file.
|
|
240
328
|
const readProjectFileContent = useCallback(
|
|
241
|
-
|
|
329
|
+
(path: string): Promise<string> => {
|
|
242
330
|
const pid = projectIdRef.current;
|
|
243
331
|
if (!pid) throw new Error("No active project");
|
|
244
|
-
|
|
245
|
-
if (!res.ok) throw new Error(`Failed to read ${path}`);
|
|
246
|
-
const data = (await res.json()) as { content?: string };
|
|
247
|
-
if (typeof data.content !== "string") throw new Error(`Missing file contents for ${path}`);
|
|
248
|
-
return data.content;
|
|
332
|
+
return readSharedProjectFileContent(pid, path);
|
|
249
333
|
},
|
|
250
334
|
[projectIdRef],
|
|
251
335
|
);
|
|
@@ -5,6 +5,7 @@ import { usePlayerStore } from "../player";
|
|
|
5
5
|
import { computeCurrentPercentage } from "./gsapDragCommit";
|
|
6
6
|
import { trackStudioSaveFailure } from "../utils/studioSaveDiagnostics";
|
|
7
7
|
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
8
|
+
import type { CommitMutationOptions } from "./gsapScriptCommitTypes";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Thin useCallback wrappers that guard on `domEditSelection` before
|
|
@@ -75,8 +76,14 @@ export function useGsapSelectionHandlers({
|
|
|
75
76
|
animId: string,
|
|
76
77
|
percentage: number,
|
|
77
78
|
properties: Record<string, number | string>,
|
|
79
|
+
commitOverrides?: Partial<CommitMutationOptions>,
|
|
78
80
|
) => Promise<void>;
|
|
79
|
-
removeKeyframe: (
|
|
81
|
+
removeKeyframe: (
|
|
82
|
+
sel: DomEditSelection,
|
|
83
|
+
animId: string,
|
|
84
|
+
percentage: number,
|
|
85
|
+
commitOverrides?: Partial<CommitMutationOptions>,
|
|
86
|
+
) => void;
|
|
80
87
|
moveKeyframe: (
|
|
81
88
|
sel: DomEditSelection,
|
|
82
89
|
animId: string,
|
|
@@ -95,6 +102,7 @@ export function useGsapSelectionHandlers({
|
|
|
95
102
|
animId: string,
|
|
96
103
|
resolvedFromValues?: Record<string, number | string>,
|
|
97
104
|
duration?: number,
|
|
105
|
+
commitOverrides?: Partial<CommitMutationOptions>,
|
|
98
106
|
) => Promise<void>;
|
|
99
107
|
removeAllKeyframes: (sel: DomEditSelection, animId: string) => void;
|
|
100
108
|
|
|
@@ -232,20 +240,36 @@ export function useGsapSelectionHandlers({
|
|
|
232
240
|
);
|
|
233
241
|
|
|
234
242
|
const handleGsapAddKeyframeBatch = useCallback(
|
|
235
|
-
(
|
|
243
|
+
(
|
|
244
|
+
animId: string,
|
|
245
|
+
percentage: number,
|
|
246
|
+
properties: Record<string, number | string>,
|
|
247
|
+
commitOverrides?: Partial<CommitMutationOptions>,
|
|
248
|
+
) => {
|
|
236
249
|
if (!domEditSelection) return Promise.resolve();
|
|
237
|
-
return addKeyframeBatch(
|
|
250
|
+
return addKeyframeBatch(
|
|
251
|
+
domEditSelection,
|
|
252
|
+
animId,
|
|
253
|
+
percentage,
|
|
254
|
+
properties,
|
|
255
|
+
commitOverrides,
|
|
256
|
+
).catch((error) => {
|
|
238
257
|
trackGsapHandlerFailure(error, domEditSelection, "add-keyframe", "Add keyframe");
|
|
239
258
|
});
|
|
240
259
|
},
|
|
241
260
|
[domEditSelection, addKeyframeBatch, trackGsapHandlerFailure],
|
|
242
261
|
);
|
|
243
262
|
const handleGsapRemoveKeyframe = useCallback(
|
|
244
|
-
(
|
|
263
|
+
(
|
|
264
|
+
animId: string,
|
|
265
|
+
percentage: number,
|
|
266
|
+
commitOverrides?: Partial<CommitMutationOptions>,
|
|
267
|
+
selectionOverride?: DomEditSelection | null,
|
|
268
|
+
) => {
|
|
245
269
|
const sel = selectionOverride ?? domEditSelection ?? lastSelectionRef.current;
|
|
246
270
|
if (!sel) return;
|
|
247
271
|
trackStudioEvent("keyframe", { action: "remove" });
|
|
248
|
-
removeKeyframe(sel, animId, percentage);
|
|
272
|
+
removeKeyframe(sel, animId, percentage, commitOverrides);
|
|
249
273
|
},
|
|
250
274
|
[domEditSelection, removeKeyframe],
|
|
251
275
|
);
|
|
@@ -302,18 +326,27 @@ export function useGsapSelectionHandlers({
|
|
|
302
326
|
);
|
|
303
327
|
|
|
304
328
|
const handleGsapConvertToKeyframes = useCallback(
|
|
305
|
-
(
|
|
329
|
+
(
|
|
330
|
+
animId: string,
|
|
331
|
+
resolvedFromValues?: Record<string, number | string>,
|
|
332
|
+
duration?: number,
|
|
333
|
+
commitOverrides?: Partial<CommitMutationOptions>,
|
|
334
|
+
) => {
|
|
306
335
|
if (!domEditSelection) return Promise.resolve();
|
|
307
|
-
return convertToKeyframes(
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
336
|
+
return convertToKeyframes(
|
|
337
|
+
domEditSelection,
|
|
338
|
+
animId,
|
|
339
|
+
resolvedFromValues,
|
|
340
|
+
duration,
|
|
341
|
+
commitOverrides,
|
|
342
|
+
).catch((error) => {
|
|
343
|
+
trackGsapHandlerFailure(
|
|
344
|
+
error,
|
|
345
|
+
domEditSelection,
|
|
346
|
+
"convert-to-keyframes",
|
|
347
|
+
"Convert to keyframes",
|
|
348
|
+
);
|
|
349
|
+
});
|
|
317
350
|
},
|
|
318
351
|
[domEditSelection, convertToKeyframes, trackGsapHandlerFailure],
|
|
319
352
|
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, useMemo, useRef } from "react";
|
|
2
2
|
import { usePlayerStore } from "../player/store/playerStore";
|
|
3
|
-
import {
|
|
3
|
+
import { resolveBeatSourceTrack } from "../utils/timelineInspector";
|
|
4
4
|
import { analyzeMusicFromUrl } from "@hyperframes/core/beats";
|
|
5
5
|
import { useFileManagerContextOptional } from "../contexts/FileManagerContext";
|
|
6
6
|
import { mergeUserBeats } from "../utils/beatEditing";
|
|
@@ -52,6 +52,44 @@ async function resolveBeats(
|
|
|
52
52
|
return { ...detected, hasFile: false };
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/** True when the beats file for a track exists and holds at least one beat. */
|
|
56
|
+
async function readHasSavedBeats(io: ProjectIo, beatPath: string): Promise<boolean> {
|
|
57
|
+
try {
|
|
58
|
+
const content = await io.readOptionalProjectFile(beatPath);
|
|
59
|
+
const parsed = content ? parseBeats(content) : null;
|
|
60
|
+
return !!(parsed && parsed.times.length > 0);
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type MusicAnalysis = Awaited<ReturnType<typeof analyzeMusicFromUrl>>;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Analyze a track (memoized per URL) and fold in any saved beat edits. Null on
|
|
70
|
+
* decode/analysis failure — the caller then clears state and drops the cache
|
|
71
|
+
* entry (only when the effect is still live).
|
|
72
|
+
*/
|
|
73
|
+
async function loadBeatAnalysis(
|
|
74
|
+
musicSrc: string,
|
|
75
|
+
beatPath: string,
|
|
76
|
+
io: ProjectIo,
|
|
77
|
+
): Promise<{ analysis: MusicAnalysis; times: number[]; strengths: number[] } | null> {
|
|
78
|
+
let promise = analysisCache.get(musicSrc);
|
|
79
|
+
if (!promise) {
|
|
80
|
+
promise = analyzeMusicFromUrl(musicSrc);
|
|
81
|
+
cacheAnalysis(musicSrc, promise);
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const analysis = await promise;
|
|
85
|
+
const detected = { times: analysis.beatTimes, strengths: analysis.beatStrengths };
|
|
86
|
+
const { times, strengths } = await resolveBeats(beatPath, detected, io);
|
|
87
|
+
return { analysis, times, strengths };
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
55
93
|
export function useMusicBeatAnalysis(): void {
|
|
56
94
|
const elements = usePlayerStore((s) => s.elements);
|
|
57
95
|
const setBeatAnalysis = usePlayerStore((s) => s.setBeatAnalysis);
|
|
@@ -71,9 +109,12 @@ export function useMusicBeatAnalysis(): void {
|
|
|
71
109
|
? { readOptionalProjectFile, writeProjectFile }
|
|
72
110
|
: null;
|
|
73
111
|
|
|
74
|
-
const musicSrc = useMemo(() => {
|
|
75
|
-
const
|
|
76
|
-
return
|
|
112
|
+
const { musicSrc, isFallbackTrack } = useMemo(() => {
|
|
113
|
+
const resolved = resolveBeatSourceTrack(elements);
|
|
114
|
+
return {
|
|
115
|
+
musicSrc: resolved?.element.src ?? null,
|
|
116
|
+
isFallbackTrack: resolved?.isFallback ?? false,
|
|
117
|
+
};
|
|
77
118
|
}, [elements]);
|
|
78
119
|
|
|
79
120
|
// ── Load: decode for strength data, then use the saved beat file if present,
|
|
@@ -95,50 +136,45 @@ export function useMusicBeatAnalysis(): void {
|
|
|
95
136
|
const beatPath = beatFilePathForSrc(musicSrc);
|
|
96
137
|
const io = ioRef.current;
|
|
97
138
|
|
|
98
|
-
//
|
|
99
|
-
// explicit beats file saved. Without
|
|
100
|
-
// green lines on the timeline after
|
|
139
|
+
// For explicitly tagged/named music tracks: only run expensive audio decode
|
|
140
|
+
// + beat analysis when the user has an explicit beats file saved. Without
|
|
141
|
+
// one, skip entirely — no surprise green lines on the timeline after
|
|
142
|
+
// dragging unrelated assets.
|
|
143
|
+
//
|
|
144
|
+
// For fallback tracks (audio dropped from Finder with no role/music-id):
|
|
145
|
+
// always run analysis so the Beat tool becomes usable immediately.
|
|
101
146
|
(async () => {
|
|
102
147
|
if (!beatPath || !io) return;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
148
|
+
if (!isFallbackTrack) {
|
|
149
|
+
const hasSavedBeats = await readHasSavedBeats(io, beatPath);
|
|
150
|
+
if (cancelled) return;
|
|
151
|
+
if (!hasSavedBeats) {
|
|
152
|
+
setBeatAnalysis(null);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
110
155
|
}
|
|
111
156
|
if (cancelled) return;
|
|
112
|
-
|
|
157
|
+
|
|
158
|
+
const result = await loadBeatAnalysis(musicSrc, beatPath, io);
|
|
159
|
+
if (cancelled) return;
|
|
160
|
+
if (!result) {
|
|
113
161
|
setBeatAnalysis(null);
|
|
162
|
+
analysisCache.delete(musicSrc);
|
|
114
163
|
return;
|
|
115
164
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const analysis = await promise;
|
|
124
|
-
const detected = { times: analysis.beatTimes, strengths: analysis.beatStrengths };
|
|
125
|
-
const { times, strengths } = await resolveBeats(beatPath, detected, io);
|
|
126
|
-
if (cancelled) return;
|
|
127
|
-
setBeatEdits(null);
|
|
128
|
-
resetBeatHistory();
|
|
129
|
-
setBeatAnalysis({ ...analysis, beatTimes: times, beatStrengths: strengths });
|
|
130
|
-
} catch {
|
|
131
|
-
if (!cancelled) {
|
|
132
|
-
setBeatAnalysis(null);
|
|
133
|
-
analysisCache.delete(musicSrc);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
165
|
+
setBeatEdits(null);
|
|
166
|
+
resetBeatHistory();
|
|
167
|
+
setBeatAnalysis({
|
|
168
|
+
...result.analysis,
|
|
169
|
+
beatTimes: result.times,
|
|
170
|
+
beatStrengths: result.strengths,
|
|
171
|
+
});
|
|
136
172
|
})();
|
|
137
173
|
|
|
138
174
|
return () => {
|
|
139
175
|
cancelled = true;
|
|
140
176
|
};
|
|
141
|
-
}, [musicSrc, setBeatAnalysis, setBeatEdits, resetBeatHistory]);
|
|
177
|
+
}, [musicSrc, isFallbackTrack, setBeatAnalysis, setBeatEdits, resetBeatHistory]);
|
|
142
178
|
|
|
143
179
|
// ── Persist: register a debounced writer fired by every beat edit/undo/redo.
|
|
144
180
|
// Flushes any pending write on cleanup so the last edit is never lost. ──
|
|
@@ -213,6 +213,39 @@ describe("createPersistentEditHistoryController", () => {
|
|
|
213
213
|
expect(store.snapshot().canRedo).toBe(true);
|
|
214
214
|
});
|
|
215
215
|
|
|
216
|
+
it("returns per-file restored/previous content so the preview can soft-apply", async () => {
|
|
217
|
+
const storage = createMemoryEditHistoryStorage();
|
|
218
|
+
const store = createPersistentEditHistoryStore({
|
|
219
|
+
projectId: "project-1",
|
|
220
|
+
storage,
|
|
221
|
+
initialState: createEmptyEditHistory(),
|
|
222
|
+
now: () => 100,
|
|
223
|
+
onChange: () => {},
|
|
224
|
+
});
|
|
225
|
+
await store.recordEdit({
|
|
226
|
+
label: "Move layer",
|
|
227
|
+
kind: "manual",
|
|
228
|
+
files: { "index.html": { before: "OLD", after: "NEW" } },
|
|
229
|
+
});
|
|
230
|
+
const disk: Record<string, string> = { "index.html": "NEW" };
|
|
231
|
+
const undo = await store.undo({
|
|
232
|
+
readFile: async (p) => disk[p],
|
|
233
|
+
writeFile: async (p, c) => {
|
|
234
|
+
disk[p] = c;
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
// `restored` = bytes written (the undo target), `previous` = the current live bytes.
|
|
238
|
+
expect(undo.files).toEqual({ "index.html": { previous: "NEW", restored: "OLD" } });
|
|
239
|
+
|
|
240
|
+
const redo = await store.redo({
|
|
241
|
+
readFile: async (p) => disk[p],
|
|
242
|
+
writeFile: async (p, c) => {
|
|
243
|
+
disk[p] = c;
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
expect(redo.files).toEqual({ "index.html": { previous: "OLD", restored: "NEW" } });
|
|
247
|
+
});
|
|
248
|
+
|
|
216
249
|
it("rolls back files when an undo write fails partway through", async () => {
|
|
217
250
|
const storage = createMemoryEditHistoryStorage();
|
|
218
251
|
const store = createPersistentEditHistoryStore({
|