@hyperframes/studio 0.7.54 → 0.7.56
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/{index-uBY329wb.js → index-BWnOxAiH.js} +1 -1
- package/dist/assets/{index-CMHYjEZ5.js → index-C4csZims.js} +1 -1
- package/dist/assets/index-CmVCjZjp.js +423 -0
- package/dist/assets/index-D-GyYi2d.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 +146 -144
- package/dist/index.html +2 -2
- package/dist/index.js +40582 -37803
- 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 +115 -0
- package/src/components/editor/CanvasContextMenu.tsx +198 -0
- 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 +158 -126
- 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 +43 -1
- 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 +208 -0
- package/src/components/editor/canvasContextMenuZOrder.test.ts +435 -0
- package/src/components/editor/canvasContextMenuZOrder.ts +352 -0
- 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/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/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/nle/AssetPreviewOverlay.tsx +147 -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 +240 -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.tsx +326 -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 +39 -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 +12 -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 +34 -1
- package/src/hooks/timelineEditingHelpers.ts +22 -48
- package/src/hooks/timelineMoveAdapter.test.ts +77 -0
- package/src/hooks/timelineMoveAdapter.ts +37 -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/useContextMenuDismiss.ts +31 -7
- package/src/hooks/useDomEditCommits.test.tsx +154 -1
- package/src/hooks/useDomEditCommits.ts +100 -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 +258 -0
- package/src/hooks/useElementLifecycleOps.ts +40 -71
- 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 +303 -0
- 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/useTimelineEditing.test.tsx +20 -29
- package/src/hooks/useTimelineEditingTypes.ts +6 -0
- package/src/hooks/useTimelineGroupEditing.ts +18 -4
- 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/PlayheadIndicator.tsx +50 -5
- package/src/player/components/Timeline.test.ts +102 -19
- package/src/player/components/Timeline.tsx +194 -194
- package/src/player/components/TimelineCanvas.tsx +207 -521
- 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/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 +943 -0
- package/src/player/components/timelineClipDragCommit.ts +401 -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 +477 -0
- package/src/player/components/timelineCollision.ts +263 -0
- 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 +120 -61
- package/src/player/components/timelineMarquee.test.ts +197 -0
- package/src/player/components/timelineMarquee.ts +112 -0
- package/src/player/components/timelineMultiDragPreview.test.ts +127 -0
- package/src/player/components/timelineMultiDragPreview.ts +106 -0
- package/src/player/components/timelineOptimisticRevision.ts +42 -0
- package/src/player/components/timelineSnapping.test.ts +134 -0
- package/src/player/components/timelineSnapping.ts +110 -0
- package/src/player/components/timelineStackingSync.test.ts +244 -0
- package/src/player/components/timelineStackingSync.ts +331 -0
- package/src/player/components/timelineTheme.ts +6 -1
- package/src/player/components/timelineZones.test.ts +425 -0
- package/src/player/components/timelineZones.ts +198 -0
- package/src/player/components/timelineZoom.test.ts +67 -0
- package/src/player/components/timelineZoom.ts +51 -0
- 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/useTimelineStackingSync.test.tsx +67 -0
- package/src/player/components/useTimelineStackingSync.ts +83 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +0 -3
- package/src/player/hooks/usePlaybackKeyboard.ts +5 -0
- package/src/player/hooks/useTimelinePlayer.ts +48 -53
- package/src/player/hooks/useTimelinePlayerLoop.ts +85 -0
- package/src/player/hooks/useTimelineSyncCallbacks.test.ts +219 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +104 -4
- package/src/player/index.ts +3 -2
- 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 +12 -81
- package/src/player/lib/timelineElementHelpers.ts +54 -5
- package/src/player/lib/timelineIframeHelpers.ts +2 -0
- package/src/player/store/playerStore.ts +62 -3
- package/src/styles/studio.css +27 -0
- package/src/utils/assetClickBehavior.test.ts +104 -0
- package/src/utils/assetClickBehavior.ts +75 -0
- package/src/utils/assetPreviewStore.ts +33 -0
- package/src/utils/blockInstaller.ts +10 -2
- package/src/utils/canvasNudgeGate.test.ts +31 -0
- package/src/utils/canvasNudgeGate.ts +32 -0
- 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/studioUiPreferences.test.ts +38 -0
- package/src/utils/studioUiPreferences.ts +27 -0
- package/src/utils/studioUrlState.test.ts +0 -1
- package/src/utils/timelineAssetDrop.ts +62 -0
- package/src/utils/timelineDiscovery.ts +0 -17
- package/src/utils/timelineInspector.test.ts +121 -0
- package/src/utils/timelineInspector.ts +32 -1
- package/dist/assets/index-Dq7FEg0K.css +0 -1
- package/dist/assets/index-pRhCpGPz.js +0 -423
- 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,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({
|
|
@@ -7,8 +7,10 @@ import {
|
|
|
7
7
|
redoEditHistory,
|
|
8
8
|
undoEditHistory,
|
|
9
9
|
type BuildEditHistoryEntryInput,
|
|
10
|
+
type EditHistoryEntry,
|
|
10
11
|
type EditHistoryKind,
|
|
11
12
|
type EditHistoryState,
|
|
13
|
+
type EditHistoryTransitionResult,
|
|
12
14
|
} from "../utils/editHistory";
|
|
13
15
|
import {
|
|
14
16
|
createIndexedDbEditHistoryStorage,
|
|
@@ -21,6 +23,7 @@ interface RecordEditInput {
|
|
|
21
23
|
label: string;
|
|
22
24
|
kind: EditHistoryKind;
|
|
23
25
|
coalesceKey?: string;
|
|
26
|
+
coalesceMs?: number;
|
|
24
27
|
files: BuildEditHistoryEntryInput["files"];
|
|
25
28
|
}
|
|
26
29
|
|
|
@@ -35,11 +38,24 @@ interface UsePersistentEditHistoryOptions {
|
|
|
35
38
|
now?: () => number;
|
|
36
39
|
}
|
|
37
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Per-file content the restore just applied. `restored` is the bytes written to
|
|
43
|
+
* disk (the undo/redo target); `previous` is what was on disk immediately before
|
|
44
|
+
* (the current live preview state). The undo preview-sync diffs these to decide
|
|
45
|
+
* whether the restore is soft-reloadable (attributes/style/GSAP-script only) or
|
|
46
|
+
* needs a full iframe reload.
|
|
47
|
+
*/
|
|
48
|
+
interface ApplyRestoredFile {
|
|
49
|
+
previous: string;
|
|
50
|
+
restored: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
38
53
|
interface ApplyResult {
|
|
39
54
|
ok: boolean;
|
|
40
55
|
reason?: "empty" | "content-mismatch";
|
|
41
56
|
label?: string;
|
|
42
57
|
paths?: string[];
|
|
58
|
+
files?: Record<string, ApplyRestoredFile>;
|
|
43
59
|
}
|
|
44
60
|
|
|
45
61
|
interface PersistentEditHistoryStoreOptions {
|
|
@@ -55,6 +71,18 @@ type EditHistoryMutation<T> = (state: EditHistoryState) => Promise<{
|
|
|
55
71
|
result: T;
|
|
56
72
|
}>;
|
|
57
73
|
|
|
74
|
+
/** Pair the just-written (`restored`) bytes with the pre-write (`previous`) bytes per path. */
|
|
75
|
+
function restoredFilesMap(
|
|
76
|
+
filesToWrite: Record<string, string>,
|
|
77
|
+
currentFiles: Record<string, string>,
|
|
78
|
+
): Record<string, ApplyRestoredFile> {
|
|
79
|
+
const out: Record<string, ApplyRestoredFile> = {};
|
|
80
|
+
for (const [path, restored] of Object.entries(filesToWrite)) {
|
|
81
|
+
out[path] = { previous: currentFiles[path] ?? "", restored };
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
58
86
|
function createEntryId(now: number): string {
|
|
59
87
|
return `edit-${now.toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
60
88
|
}
|
|
@@ -120,6 +148,52 @@ async function writeFilesWithRollback({
|
|
|
120
148
|
}
|
|
121
149
|
}
|
|
122
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Apply one undo/redo step: read current on-disk hashes, run the direction's
|
|
153
|
+
* transition, write the restored files with rollback, and shape the ApplyResult.
|
|
154
|
+
* `entry` is the stack top used to know which paths to hash before applying.
|
|
155
|
+
*/
|
|
156
|
+
async function applyHistoryStep(
|
|
157
|
+
currentState: EditHistoryState,
|
|
158
|
+
entry: EditHistoryEntry | undefined,
|
|
159
|
+
transition: (
|
|
160
|
+
state: EditHistoryState,
|
|
161
|
+
currentHashes: Record<string, string>,
|
|
162
|
+
now: number,
|
|
163
|
+
) => EditHistoryTransitionResult,
|
|
164
|
+
now: () => number,
|
|
165
|
+
callbacks: ApplyCallbacks,
|
|
166
|
+
): Promise<{ state: EditHistoryState; result: ApplyResult }> {
|
|
167
|
+
if (!entry) {
|
|
168
|
+
return { state: currentState, result: { ok: false, reason: "empty" } };
|
|
169
|
+
}
|
|
170
|
+
const { currentFiles, currentHashes } = await readCurrentFileHashes(
|
|
171
|
+
Object.keys(entry.files),
|
|
172
|
+
callbacks.readFile,
|
|
173
|
+
);
|
|
174
|
+
const result = transition(currentState, currentHashes, now());
|
|
175
|
+
if (!result.ok) {
|
|
176
|
+
return {
|
|
177
|
+
state: currentState,
|
|
178
|
+
result: { ok: false, reason: result.reason },
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
await writeFilesWithRollback({
|
|
182
|
+
files: result.filesToWrite,
|
|
183
|
+
rollbackFiles: currentFiles,
|
|
184
|
+
writeFile: callbacks.writeFile,
|
|
185
|
+
});
|
|
186
|
+
return {
|
|
187
|
+
state: result.state,
|
|
188
|
+
result: {
|
|
189
|
+
ok: true,
|
|
190
|
+
label: result.entry.label,
|
|
191
|
+
paths: Object.keys(result.entry.files),
|
|
192
|
+
files: restoredFilesMap(result.filesToWrite, currentFiles),
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
123
197
|
export function createPersistentEditHistoryStore({
|
|
124
198
|
projectId,
|
|
125
199
|
storage,
|
|
@@ -171,66 +245,26 @@ export function createPersistentEditHistoryStore({
|
|
|
171
245
|
});
|
|
172
246
|
},
|
|
173
247
|
async undo(callbacks: ApplyCallbacks): Promise<ApplyResult> {
|
|
174
|
-
return mutate<ApplyResult>(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
Object.keys(entry.files),
|
|
184
|
-
callbacks.readFile,
|
|
185
|
-
);
|
|
186
|
-
const result = undoEditHistory(currentState, currentHashes, now());
|
|
187
|
-
if (!result.ok) {
|
|
188
|
-
return {
|
|
189
|
-
state: currentState,
|
|
190
|
-
result: { ok: false, reason: result.reason },
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
await writeFilesWithRollback({
|
|
194
|
-
files: result.filesToWrite,
|
|
195
|
-
rollbackFiles: currentFiles,
|
|
196
|
-
writeFile: callbacks.writeFile,
|
|
197
|
-
});
|
|
198
|
-
return {
|
|
199
|
-
state: result.state,
|
|
200
|
-
result: { ok: true, label: result.entry.label, paths: Object.keys(result.entry.files) },
|
|
201
|
-
};
|
|
202
|
-
});
|
|
248
|
+
return mutate<ApplyResult>((currentState) =>
|
|
249
|
+
applyHistoryStep(
|
|
250
|
+
currentState,
|
|
251
|
+
currentState.undo[currentState.undo.length - 1],
|
|
252
|
+
undoEditHistory,
|
|
253
|
+
now,
|
|
254
|
+
callbacks,
|
|
255
|
+
),
|
|
256
|
+
);
|
|
203
257
|
},
|
|
204
258
|
async redo(callbacks: ApplyCallbacks): Promise<ApplyResult> {
|
|
205
|
-
return mutate<ApplyResult>(
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
Object.keys(entry.files),
|
|
215
|
-
callbacks.readFile,
|
|
216
|
-
);
|
|
217
|
-
const result = redoEditHistory(currentState, currentHashes, now());
|
|
218
|
-
if (!result.ok) {
|
|
219
|
-
return {
|
|
220
|
-
state: currentState,
|
|
221
|
-
result: { ok: false, reason: result.reason },
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
await writeFilesWithRollback({
|
|
225
|
-
files: result.filesToWrite,
|
|
226
|
-
rollbackFiles: currentFiles,
|
|
227
|
-
writeFile: callbacks.writeFile,
|
|
228
|
-
});
|
|
229
|
-
return {
|
|
230
|
-
state: result.state,
|
|
231
|
-
result: { ok: true, label: result.entry.label, paths: Object.keys(result.entry.files) },
|
|
232
|
-
};
|
|
233
|
-
});
|
|
259
|
+
return mutate<ApplyResult>((currentState) =>
|
|
260
|
+
applyHistoryStep(
|
|
261
|
+
currentState,
|
|
262
|
+
currentState.redo[currentState.redo.length - 1],
|
|
263
|
+
redoEditHistory,
|
|
264
|
+
now,
|
|
265
|
+
callbacks,
|
|
266
|
+
),
|
|
267
|
+
);
|
|
234
268
|
},
|
|
235
269
|
};
|
|
236
270
|
}
|
|
@@ -10,6 +10,14 @@ import type { EditHistoryKind } from "../utils/editHistory";
|
|
|
10
10
|
import { createDomEditSaveQueue } from "../utils/domEditSaveQueue";
|
|
11
11
|
import { flushStudioPendingEdits } from "../utils/studioPendingEdits";
|
|
12
12
|
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
13
|
+
import { applyUndoRestoreToPreview, type UndoRestoreFile } from "../utils/gsapSoftReload";
|
|
14
|
+
import { usePlayerStore } from "../player";
|
|
15
|
+
|
|
16
|
+
/** The restore payload the undo/redo preview-sync consumes (from the history store). */
|
|
17
|
+
interface HistoryPreviewRestore {
|
|
18
|
+
paths?: string[];
|
|
19
|
+
files?: Record<string, UndoRestoreFile>;
|
|
20
|
+
}
|
|
13
21
|
|
|
14
22
|
// ── Types ──
|
|
15
23
|
|
|
@@ -105,13 +113,12 @@ export function usePreviewPersistence({
|
|
|
105
113
|
writeProjectFile: _writeProjectFile,
|
|
106
114
|
recordEdit: _recordEdit,
|
|
107
115
|
previewIframeRef,
|
|
108
|
-
activeCompPathRef
|
|
116
|
+
activeCompPathRef,
|
|
109
117
|
domEditSaveTimestampRef,
|
|
110
118
|
reloadPreview,
|
|
111
119
|
pendingTimelineEditPathRef,
|
|
112
120
|
}: UsePreviewPersistenceParams) {
|
|
113
121
|
void _recordEdit;
|
|
114
|
-
void _activeCompPathRef;
|
|
115
122
|
|
|
116
123
|
const [domEditSaveQueuePaused, setDomEditSaveQueuePaused] = useState<string | null>(null);
|
|
117
124
|
|
|
@@ -190,12 +197,23 @@ export function usePreviewPersistence({
|
|
|
190
197
|
// ── Sync preview after undo/redo ──
|
|
191
198
|
|
|
192
199
|
const syncHistoryPreviewAfterApply = useCallback(
|
|
193
|
-
async (
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
|
|
200
|
+
async (restore: HistoryPreviewRestore) => {
|
|
201
|
+
// Prefer an in-place soft reload for a soft-reloadable restore (the change
|
|
202
|
+
// is confined to the active comp's element attributes / inline-style and/or
|
|
203
|
+
// its GSAP script) — a full iframe remount blanks the frame black and
|
|
204
|
+
// re-flashes the WebGL context. applyUndoRestoreToPreview syncs the reverted
|
|
205
|
+
// attributes onto the live DOM and re-runs the timeline at the SAME playhead,
|
|
206
|
+
// falling back to reloadPreview for anything structural (split/delete undo),
|
|
207
|
+
// multi-file, sub-comp, or a permanent soft-reload failure.
|
|
208
|
+
applyUndoRestoreToPreview(
|
|
209
|
+
previewIframeRef.current,
|
|
210
|
+
activeCompPathRef.current,
|
|
211
|
+
restore.files,
|
|
212
|
+
usePlayerStore.getState().currentTime,
|
|
213
|
+
reloadPreview,
|
|
214
|
+
);
|
|
197
215
|
},
|
|
198
|
-
[reloadPreview],
|
|
216
|
+
[previewIframeRef, activeCompPathRef, reloadPreview],
|
|
199
217
|
);
|
|
200
218
|
|
|
201
219
|
// ── Migrate legacy studio-motion.json ──
|