@hyperframes/studio 0.7.56 → 0.7.58
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-CtTDO63S.js +459 -0
- package/dist/assets/{index-CmVCjZjp.js → index-B_UvTX3E.js} +205 -205
- package/dist/assets/{index-BWnOxAiH.js → index-C47jAC3Q.js} +1 -1
- package/dist/assets/{index-C4csZims.js → index-DeQPzqwH.js} +1 -1
- package/dist/assets/index-uahwWkgw.css +1 -0
- package/dist/{chunk-5QSIMBEJ.js → chunk-OBAG3GWK.js} +33 -21
- package/dist/chunk-OBAG3GWK.js.map +1 -0
- package/dist/{domEditingLayers-6LQGKPOI.js → domEditingLayers-2CKWGEHS.js} +2 -2
- package/dist/index.d.ts +41 -4
- package/dist/index.html +2 -2
- package/dist/index.js +7436 -5423
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +3 -3
- package/src/components/StudioFeedbackBar.tsx +2 -2
- package/src/components/editor/CanvasContextMenu.test.tsx +166 -2
- package/src/components/editor/CanvasContextMenu.tsx +100 -20
- package/src/components/editor/DomEditOverlay.tsx +34 -62
- package/src/components/editor/LayersPanel.tsx +111 -91
- package/src/components/editor/canvasContextMenuZOrder.test.ts +243 -2
- package/src/components/editor/canvasContextMenuZOrder.ts +184 -23
- package/src/components/editor/domEditingDom.ts +5 -6
- package/src/components/editor/domEditingElement.ts +15 -32
- package/src/components/editor/layersPanelSort.ts +80 -0
- package/src/components/editor/propertyPanelColorGradingSection.tsx +15 -21
- package/src/components/editor/useCanvasContextMenuState.ts +92 -0
- package/src/components/editor/useLayerRevealOverride.test.ts +169 -0
- package/src/components/editor/useLayerRevealOverride.ts +389 -0
- package/src/components/editor/useZOrderCrossedFlash.tsx +68 -0
- package/src/components/nle/AssetPreviewOverlay.test.tsx +106 -0
- package/src/components/nle/AssetPreviewOverlay.tsx +61 -37
- package/src/components/nle/PreviewOverlays.tsx +61 -16
- package/src/components/nle/TimelinePane.test.ts +3 -0
- package/src/components/nle/TimelinePane.tsx +11 -1
- package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +440 -0
- package/src/components/nle/useCanvasZOrderTimelineMirror.ts +191 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +1 -0
- package/src/components/nle/zLaneGesture.test.ts +176 -0
- package/src/components/nle/zLaneGesture.ts +77 -0
- package/src/components/sidebar/AssetCard.test.tsx +106 -0
- package/src/components/sidebar/AssetCard.tsx +19 -2
- package/src/components/sidebar/AudioRow.tsx +19 -2
- package/src/hooks/domEditCommitTypes.ts +29 -2
- package/src/hooks/elementLifecycleOpsTestUtils.ts +25 -0
- package/src/hooks/fetchStubTestUtils.ts +16 -0
- package/src/hooks/timelineEditingHelpers.test.ts +183 -1
- package/src/hooks/timelineEditingHelpers.ts +63 -259
- package/src/hooks/timelineMoveAdapter.test.ts +44 -40
- package/src/hooks/timelineMoveAdapter.ts +7 -4
- package/src/hooks/timelineTimingSync.test.ts +820 -0
- package/src/hooks/timelineTimingSync.ts +597 -0
- package/src/hooks/timelineTrackVisibility.test.ts +3 -1
- package/src/hooks/timelineTrackVisibility.ts +4 -3
- package/src/hooks/useCaptionDetection.ts +2 -0
- package/src/hooks/useClipboard.ts +19 -17
- package/src/hooks/useCompositionDimensions.ts +32 -5
- package/src/hooks/useDomEditCommits.test.tsx +392 -23
- package/src/hooks/useDomEditCommits.ts +180 -53
- package/src/hooks/useDomEditSession.test.tsx +3 -1
- package/src/hooks/useDomEditSession.ts +1 -1
- package/src/hooks/useElementLifecycleOps.test.tsx +559 -28
- package/src/hooks/useElementLifecycleOps.ts +175 -69
- package/src/hooks/useElementPicker.ts +7 -8
- package/src/hooks/usePreviewPersistence.ts +2 -2
- package/src/hooks/useTimelineAssetDropOps.ts +175 -0
- package/src/hooks/useTimelineEditing.test.tsx +591 -277
- package/src/hooks/useTimelineEditing.ts +202 -282
- package/src/hooks/useTimelineEditingTypes.ts +1 -1
- package/src/hooks/useTimelineGroupEditing.ts +187 -134
- package/src/player/components/PlayerControls.tsx +5 -5
- package/src/player/components/PlayheadIndicator.tsx +7 -2
- package/src/player/components/Timeline.test.ts +105 -18
- package/src/player/components/Timeline.tsx +69 -98
- package/src/player/components/TimelineCanvas.tsx +47 -7
- package/src/player/components/TimelineLanes.tsx +32 -10
- package/src/player/components/TimelineOverlays.tsx +36 -0
- package/src/player/components/TimelineRuler.tsx +70 -47
- package/src/player/components/TrackGapContextMenu.tsx +116 -0
- package/src/player/components/timelineCallbacks.ts +4 -1
- package/src/player/components/timelineClipDragCommit.test.ts +370 -11
- package/src/player/components/timelineClipDragCommit.ts +267 -68
- package/src/player/components/timelineGapCommit.test.ts +163 -0
- package/src/player/components/timelineGapCommit.ts +103 -0
- package/src/player/components/timelineGaps.test.ts +221 -0
- package/src/player/components/timelineGaps.ts +182 -0
- package/src/player/components/timelineLayout.test.ts +2 -1
- package/src/player/components/timelineLayout.ts +119 -30
- package/src/player/components/timelineMarquee.test.ts +20 -10
- package/src/player/components/timelineMarquee.ts +9 -2
- package/src/player/components/timelineRevealScroll.test.ts +133 -0
- package/src/player/components/timelineRevealScroll.ts +91 -0
- package/src/player/components/timelineStackingSync.test.ts +102 -1
- package/src/player/components/timelineStackingSync.ts +45 -5
- package/src/player/components/timelineTrackPersistPipeline.test.ts +168 -0
- package/src/player/components/timelineZMirror.test.ts +667 -0
- package/src/player/components/timelineZMirror.ts +393 -0
- package/src/player/components/timelineZones.test.ts +122 -310
- package/src/player/components/timelineZones.ts +82 -135
- package/src/player/components/timelineZoom.test.ts +2 -2
- package/src/player/components/timelineZoom.ts +7 -3
- package/src/player/components/useTimelineClipDrag.ts +1 -1
- package/src/player/components/useTimelineGapHighlights.test.ts +119 -0
- package/src/player/components/useTimelineGapHighlights.ts +110 -0
- package/src/player/components/useTimelinePlayhead.ts +4 -3
- package/src/player/components/useTimelineRangeSelection.ts +6 -4
- package/src/player/components/useTimelineRevealClip.ts +56 -0
- package/src/player/components/useTimelineScrollViewport.ts +73 -0
- package/src/player/components/useTimelineStackingSync.test.tsx +11 -2
- package/src/player/components/useTimelineStackingSync.ts +17 -4
- package/src/player/components/useTimelineTrackDerivations.ts +38 -0
- package/src/player/components/useTrackGapMenu.ts +155 -0
- package/src/player/hooks/useExpandedTimelineElements.test.ts +67 -4
- package/src/player/hooks/useExpandedTimelineElements.ts +19 -1
- 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 +4 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +11 -1
- package/src/player/index.ts +3 -1
- package/src/player/lib/layerOrdering.test.ts +1 -15
- package/src/player/lib/layerOrdering.ts +5 -28
- package/src/player/lib/runtimeProtocol.test.ts +48 -0
- package/src/player/lib/runtimeProtocol.ts +65 -0
- package/src/player/lib/timelineDOM.test.ts +72 -0
- package/src/player/lib/timelineDOM.ts +12 -0
- package/src/player/lib/timelineElementHelpers.ts +54 -11
- package/src/player/lib/timelineIframeHelpers.ts +2 -4
- package/src/player/store/playerStore.test.ts +38 -13
- package/src/player/store/playerStore.ts +49 -1
- package/src/telemetry/events.test.ts +10 -0
- package/src/telemetry/events.ts +1 -0
- package/src/utils/assetPreviewDismiss.test.ts +27 -0
- package/src/utils/assetPreviewDismiss.ts +29 -0
- package/src/utils/domEditSaveQueue.ts +4 -3
- package/src/utils/editHistory.test.ts +82 -0
- package/src/utils/editHistory.ts +14 -2
- package/src/utils/gsapSoftReload.test.ts +54 -115
- package/src/utils/gsapSoftReload.ts +42 -152
- package/src/utils/gsapUndoRestore.test.ts +269 -0
- package/src/utils/gsapUndoRestore.ts +269 -0
- package/src/utils/sourceScopedSelectorIndex.ts +29 -0
- package/src/utils/timelineAssetDrop.test.ts +144 -52
- package/src/utils/timelineAssetDrop.ts +62 -86
- package/dist/assets/hyperframes-player-BBrKzTGd.js +0 -459
- package/dist/assets/index-D-GyYi2d.css +0 -1
- package/dist/chunk-5QSIMBEJ.js.map +0 -1
- /package/dist/{domEditingLayers-6LQGKPOI.js.map → domEditingLayers-2CKWGEHS.js.map} +0 -0
|
@@ -6,17 +6,20 @@ import {
|
|
|
6
6
|
buildTimelineMoveTimingPatch,
|
|
7
7
|
buildTimelineResizeTimingPatch,
|
|
8
8
|
extendRootDurationIfNeeded,
|
|
9
|
-
finishTimelineTimingFallback,
|
|
10
|
-
foldGsapMutationIntoHistory,
|
|
11
9
|
formatTimelineAttributeNumber,
|
|
12
10
|
patchIframeDomTiming,
|
|
13
11
|
persistTimelineBatchEdit,
|
|
14
|
-
readFileContent,
|
|
15
|
-
scaleGsapPositions,
|
|
16
|
-
shiftGsapPositions,
|
|
17
12
|
type PersistTimelineBatchChange,
|
|
18
13
|
type RecordEditInput,
|
|
19
14
|
} from "./timelineEditingHelpers";
|
|
15
|
+
import {
|
|
16
|
+
captureDurationRollback,
|
|
17
|
+
finishGroupTimingGsapFallback,
|
|
18
|
+
readFileContent,
|
|
19
|
+
scaleGsapPositions,
|
|
20
|
+
shiftGsapPositions,
|
|
21
|
+
syncPreviewContentDuration,
|
|
22
|
+
} from "./timelineTimingSync";
|
|
20
23
|
|
|
21
24
|
export interface TimelineGroupMoveChange {
|
|
22
25
|
element: TimelineElement;
|
|
@@ -34,6 +37,8 @@ export interface TimelineGroupResizeChange {
|
|
|
34
37
|
export interface TimelineGroupCommitOptions {
|
|
35
38
|
beforeTiming?: Promise<void>;
|
|
36
39
|
coalesceKey?: string;
|
|
40
|
+
/** Per-entry undo coalesce window override (ms) — see EditHistoryEntry.coalesceMs. */
|
|
41
|
+
coalesceMs?: number;
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
interface UseTimelineGroupEditingOptions {
|
|
@@ -75,6 +80,15 @@ function resizeCoalesceKey(changes: readonly TimelineGroupResizeChange[]): strin
|
|
|
75
80
|
return `timeline-group-resize:${changes.map((change) => change.element.hfId ?? change.element.id).join(",")}`;
|
|
76
81
|
}
|
|
77
82
|
|
|
83
|
+
function toSdkTimingChanges<T extends { element: TimelineElement }>(
|
|
84
|
+
changes: readonly T[],
|
|
85
|
+
timingUpdate: (change: T) => { start: number; duration?: number },
|
|
86
|
+
): Array<{ hfId: string; timingUpdate: { start: number; duration?: number } } | null> {
|
|
87
|
+
return changes.map((change) =>
|
|
88
|
+
change.element.hfId ? { hfId: change.element.hfId, timingUpdate: timingUpdate(change) } : null,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
78
92
|
function resizeHasPlaybackStartAdjustment(change: TimelineGroupResizeChange): boolean {
|
|
79
93
|
return (
|
|
80
94
|
change.playbackStart != null ||
|
|
@@ -125,6 +139,7 @@ export function useTimelineGroupEditing({
|
|
|
125
139
|
label: string,
|
|
126
140
|
batchChanges: PersistTimelineBatchChange[],
|
|
127
141
|
coalesceKey: string,
|
|
142
|
+
coalesceMs?: number,
|
|
128
143
|
) => {
|
|
129
144
|
await persistTimelineBatchEdit({
|
|
130
145
|
projectId,
|
|
@@ -136,6 +151,7 @@ export function useTimelineGroupEditing({
|
|
|
136
151
|
domEditSaveTimestampRef,
|
|
137
152
|
pendingTimelineEditPathRef,
|
|
138
153
|
coalesceKey,
|
|
154
|
+
coalesceMs,
|
|
139
155
|
});
|
|
140
156
|
forceReloadSdkSession?.();
|
|
141
157
|
},
|
|
@@ -149,6 +165,56 @@ export function useTimelineGroupEditing({
|
|
|
149
165
|
],
|
|
150
166
|
);
|
|
151
167
|
|
|
168
|
+
// Shared SDK fast path for group move/resize: eligible when nothing needs the
|
|
169
|
+
// server (no root-duration growth, one shared file, every change SDK-addressable
|
|
170
|
+
// and `eligible` per the caller's own gate). Returns whether the SDK handled it;
|
|
171
|
+
// false → caller falls through to the server batch persist.
|
|
172
|
+
const trySdkBatchPersist = useCallback(
|
|
173
|
+
async (input: {
|
|
174
|
+
changes: readonly { element: TimelineElement }[];
|
|
175
|
+
sdkChanges: Array<{
|
|
176
|
+
hfId: string;
|
|
177
|
+
timingUpdate: { start: number; duration?: number };
|
|
178
|
+
} | null>;
|
|
179
|
+
eligible: boolean;
|
|
180
|
+
needsExtension: boolean;
|
|
181
|
+
label: string;
|
|
182
|
+
coalesceKey: string;
|
|
183
|
+
coalesceMs?: number;
|
|
184
|
+
}): Promise<boolean> => {
|
|
185
|
+
const sharedPath = allChangesSharePath(input.changes, activeCompPath);
|
|
186
|
+
const canUseSdk =
|
|
187
|
+
!input.needsExtension &&
|
|
188
|
+
sharedPath !== null &&
|
|
189
|
+
input.eligible &&
|
|
190
|
+
input.sdkChanges.every((change) => change !== null);
|
|
191
|
+
if (!canUseSdk) return false;
|
|
192
|
+
return sdkTimingBatchPersist(
|
|
193
|
+
input.sdkChanges.filter((change): change is NonNullable<typeof change> => change !== null),
|
|
194
|
+
sharedPath,
|
|
195
|
+
sdkSession,
|
|
196
|
+
{
|
|
197
|
+
editHistory: { recordEdit },
|
|
198
|
+
writeProjectFile,
|
|
199
|
+
reloadPreview,
|
|
200
|
+
domEditSaveTimestampRef,
|
|
201
|
+
compositionPath: activeCompPath,
|
|
202
|
+
readProjectFile: (path) => readFileContent(projectIdRef.current ?? "", path),
|
|
203
|
+
},
|
|
204
|
+
{ label: input.label, coalesceKey: input.coalesceKey, coalesceMs: input.coalesceMs },
|
|
205
|
+
);
|
|
206
|
+
},
|
|
207
|
+
[
|
|
208
|
+
activeCompPath,
|
|
209
|
+
domEditSaveTimestampRef,
|
|
210
|
+
projectIdRef,
|
|
211
|
+
recordEdit,
|
|
212
|
+
reloadPreview,
|
|
213
|
+
sdkSession,
|
|
214
|
+
writeProjectFile,
|
|
215
|
+
],
|
|
216
|
+
);
|
|
217
|
+
|
|
152
218
|
const handleTimelineGroupMove = useCallback(
|
|
153
219
|
(changes: TimelineGroupMoveChange[], options?: TimelineGroupCommitOptions) => {
|
|
154
220
|
if (changes.length === 0) return Promise.resolve();
|
|
@@ -159,42 +225,50 @@ export function useTimelineGroupEditing({
|
|
|
159
225
|
if (change.track != null) {
|
|
160
226
|
attrs.push(["data-track-index", formatTimelineAttributeNumber(change.track)]);
|
|
161
227
|
}
|
|
162
|
-
patchIframeDomTiming(previewIframeRef.current, change.element, attrs);
|
|
228
|
+
patchIframeDomTiming(previewIframeRef.current, change.element, attrs, activeCompPath);
|
|
163
229
|
}
|
|
164
230
|
|
|
231
|
+
// TRACK-ONLY batch: every change keeps its start (moves never carry a
|
|
232
|
+
// duration change), so nothing timing-related changed — the batch only
|
|
233
|
+
// rewrites data-track-index, which the renderer never reads (documented
|
|
234
|
+
// in core runtime/timeline.ts; track is a studio lane concept). The live
|
|
235
|
+
// DOM patch above + the gesture owner's optimistic store update fully
|
|
236
|
+
// cover the UI, so after the persist there is nothing to GSAP-shift and
|
|
237
|
+
// nothing for the preview to recompute: skip the fallback below entirely.
|
|
238
|
+
// Running it anyway is what made the mirrored z-order lane move blink —
|
|
239
|
+
// a zero-delta batch yields no scriptText, and finishGroupTimingGsapFallback
|
|
240
|
+
// used to full-reload the iframe when there was no script to soft-swap
|
|
241
|
+
// (it now rebinds the runtime timing in place, but a track-only batch
|
|
242
|
+
// needs NO preview sync at all, so the skip stays).
|
|
243
|
+
const trackOnly = changes.every((change) => change.start === change.element.start);
|
|
244
|
+
|
|
165
245
|
const maxEnd = Math.max(...changes.map((change) => change.start + change.element.duration));
|
|
246
|
+
// Snapshot the duration BEFORE the optimistic updates below so a failed
|
|
247
|
+
// persist can roll the readout + live root back (see captureDurationRollback).
|
|
248
|
+
const rollbackDuration = captureDurationRollback(previewIframeRef.current);
|
|
249
|
+
// needsExtension gates the SDK path (setTiming can't grow the root duration),
|
|
250
|
+
// so read the store BEFORE the readout sync below optimistically updates it.
|
|
251
|
+
// Track-only batches leave every clip end unchanged, so both this and the
|
|
252
|
+
// readout sync below are provable no-ops there — kept unconditional so the
|
|
253
|
+
// duration machinery stays on one code path.
|
|
166
254
|
const needsExtension = extendRootDurationIfNeeded(maxEnd);
|
|
255
|
+
// Optimistic duration readout: content-driven (grow AND shrink), read from
|
|
256
|
+
// the just-patched live DOM. See syncPreviewContentDuration.
|
|
257
|
+
syncPreviewContentDuration(previewIframeRef.current);
|
|
167
258
|
const coalesceKey = options?.coalesceKey ?? moveCoalesceKey(changes);
|
|
259
|
+
const coalesceMs = options?.coalesceMs;
|
|
168
260
|
return enqueueGroupOperation("Move timeline clips", async (projectId) => {
|
|
169
261
|
await options?.beforeTiming;
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
change.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
sdkChanges.every((change) => change !== null);
|
|
181
|
-
if (canUseSdk) {
|
|
182
|
-
const handled = await sdkTimingBatchPersist(
|
|
183
|
-
sdkChanges.filter((change): change is NonNullable<typeof change> => change !== null),
|
|
184
|
-
sharedPath,
|
|
185
|
-
sdkSession,
|
|
186
|
-
{
|
|
187
|
-
editHistory: { recordEdit },
|
|
188
|
-
writeProjectFile,
|
|
189
|
-
reloadPreview,
|
|
190
|
-
domEditSaveTimestampRef,
|
|
191
|
-
compositionPath: activeCompPath,
|
|
192
|
-
readProjectFile: (path) => readFileContent(projectIdRef.current ?? "", path),
|
|
193
|
-
},
|
|
194
|
-
{ label: "Move timeline clips", coalesceKey },
|
|
195
|
-
);
|
|
196
|
-
if (handled) return;
|
|
197
|
-
}
|
|
262
|
+
const handledBySdk = await trySdkBatchPersist({
|
|
263
|
+
changes,
|
|
264
|
+
sdkChanges: toSdkTimingChanges(changes, (change) => ({ start: change.start })),
|
|
265
|
+
eligible: changes.every((change) => change.track == null),
|
|
266
|
+
needsExtension,
|
|
267
|
+
label: "Move timeline clips",
|
|
268
|
+
coalesceKey,
|
|
269
|
+
coalesceMs,
|
|
270
|
+
});
|
|
271
|
+
if (handledBySdk) return;
|
|
198
272
|
|
|
199
273
|
await persistServerBatch(
|
|
200
274
|
projectId,
|
|
@@ -211,51 +285,45 @@ export function useTimelineGroupEditing({
|
|
|
211
285
|
),
|
|
212
286
|
})),
|
|
213
287
|
coalesceKey,
|
|
288
|
+
coalesceMs,
|
|
214
289
|
);
|
|
215
|
-
|
|
290
|
+
// Track-only: no timing delta → no GSAP positions to shift and no
|
|
291
|
+
// reload (see the trackOnly doc above). Mixed batches (any start
|
|
292
|
+
// change) keep the full fallback below.
|
|
293
|
+
if (trackOnly) return;
|
|
294
|
+
await finishGroupTimingGsapFallback({
|
|
295
|
+
projectId,
|
|
216
296
|
iframe: previewIframeRef.current,
|
|
217
|
-
needsExtension,
|
|
218
|
-
rootDurationSeconds: maxEnd,
|
|
219
297
|
reloadPreview,
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const status = await shiftGsapPositions(
|
|
234
|
-
projectId,
|
|
235
|
-
targetPathFor(change.element, activeCompPath),
|
|
236
|
-
domId,
|
|
237
|
-
delta,
|
|
238
|
-
);
|
|
239
|
-
mutated = mutated || status.mutated;
|
|
240
|
-
}
|
|
241
|
-
return { mutated };
|
|
242
|
-
},
|
|
243
|
-
}),
|
|
244
|
-
onGsapError: (err) => console.error("[Timeline] Failed to shift GSAP positions", err),
|
|
298
|
+
label: "Move timeline clips",
|
|
299
|
+
errorLabel: "Failed to shift GSAP positions",
|
|
300
|
+
coalesceKey,
|
|
301
|
+
recordEdit,
|
|
302
|
+
activeCompPath,
|
|
303
|
+
changes,
|
|
304
|
+
resolveChangePath: (element) => targetPathFor(element, activeCompPath),
|
|
305
|
+
mutateChange: (change, changePath) => {
|
|
306
|
+
const delta = change.start - change.element.start;
|
|
307
|
+
const domId = change.element.domId;
|
|
308
|
+
if (delta === 0 || !domId) return null;
|
|
309
|
+
return shiftGsapPositions(projectId, changePath, domId, delta);
|
|
310
|
+
},
|
|
245
311
|
});
|
|
312
|
+
}).catch((error) => {
|
|
313
|
+
// Failed persist: revert the optimistic duration readout + live root
|
|
314
|
+
// alongside the gesture owner's store rollback.
|
|
315
|
+
rollbackDuration();
|
|
316
|
+
throw error;
|
|
246
317
|
});
|
|
247
318
|
},
|
|
248
319
|
[
|
|
249
320
|
activeCompPath,
|
|
250
|
-
domEditSaveTimestampRef,
|
|
251
321
|
enqueueGroupOperation,
|
|
252
322
|
persistServerBatch,
|
|
253
323
|
previewIframeRef,
|
|
254
|
-
projectIdRef,
|
|
255
324
|
recordEdit,
|
|
256
325
|
reloadPreview,
|
|
257
|
-
|
|
258
|
-
writeProjectFile,
|
|
326
|
+
trySdkBatchPersist,
|
|
259
327
|
],
|
|
260
328
|
);
|
|
261
329
|
|
|
@@ -274,45 +342,36 @@ export function useTimelineGroupEditing({
|
|
|
274
342
|
: "data-media-start";
|
|
275
343
|
liveAttrs.push([liveAttr, formatTimelineAttributeNumber(change.playbackStart)]);
|
|
276
344
|
}
|
|
277
|
-
patchIframeDomTiming(previewIframeRef.current, change.element, liveAttrs);
|
|
345
|
+
patchIframeDomTiming(previewIframeRef.current, change.element, liveAttrs, activeCompPath);
|
|
278
346
|
}
|
|
279
347
|
|
|
280
348
|
const maxEnd = Math.max(...changes.map((change) => change.start + change.duration));
|
|
349
|
+
// Snapshot the duration BEFORE the optimistic updates below so a failed
|
|
350
|
+
// persist can roll the readout + live root back (see captureDurationRollback).
|
|
351
|
+
const rollbackDuration = captureDurationRollback(previewIframeRef.current);
|
|
352
|
+
// needsExtension gates the SDK path (setTiming can't grow the root duration),
|
|
353
|
+
// so read the store BEFORE the readout sync below optimistically updates it.
|
|
281
354
|
const needsExtension = extendRootDurationIfNeeded(maxEnd);
|
|
355
|
+
// Optimistic duration readout: content-driven (grow AND shrink), read from
|
|
356
|
+
// the just-patched live DOM. See syncPreviewContentDuration.
|
|
357
|
+
syncPreviewContentDuration(previewIframeRef.current);
|
|
282
358
|
const coalesceKey = options?.coalesceKey ?? resizeCoalesceKey(changes);
|
|
359
|
+
const coalesceMs = options?.coalesceMs;
|
|
283
360
|
return enqueueGroupOperation("Resize timeline clips", async (projectId) => {
|
|
284
361
|
await options?.beforeTiming;
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
change
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
sdkChanges.every((change) => change !== null);
|
|
299
|
-
if (canUseSdk) {
|
|
300
|
-
const handled = await sdkTimingBatchPersist(
|
|
301
|
-
sdkChanges.filter((change): change is NonNullable<typeof change> => change !== null),
|
|
302
|
-
sharedPath,
|
|
303
|
-
sdkSession,
|
|
304
|
-
{
|
|
305
|
-
editHistory: { recordEdit },
|
|
306
|
-
writeProjectFile,
|
|
307
|
-
reloadPreview,
|
|
308
|
-
domEditSaveTimestampRef,
|
|
309
|
-
compositionPath: activeCompPath,
|
|
310
|
-
readProjectFile: (path) => readFileContent(projectIdRef.current ?? "", path),
|
|
311
|
-
},
|
|
312
|
-
{ label: "Resize timeline clips", coalesceKey },
|
|
313
|
-
);
|
|
314
|
-
if (handled) return;
|
|
315
|
-
}
|
|
362
|
+
const handledBySdk = await trySdkBatchPersist({
|
|
363
|
+
changes,
|
|
364
|
+
sdkChanges: toSdkTimingChanges(changes, (change) => ({
|
|
365
|
+
start: change.start,
|
|
366
|
+
duration: change.duration,
|
|
367
|
+
})),
|
|
368
|
+
eligible: changes.every((change) => !resizeHasPlaybackStartAdjustment(change)),
|
|
369
|
+
needsExtension,
|
|
370
|
+
label: "Resize timeline clips",
|
|
371
|
+
coalesceKey,
|
|
372
|
+
coalesceMs,
|
|
373
|
+
});
|
|
374
|
+
if (handledBySdk) return;
|
|
316
375
|
|
|
317
376
|
await persistServerBatch(
|
|
318
377
|
projectId,
|
|
@@ -327,56 +386,50 @@ export function useTimelineGroupEditing({
|
|
|
327
386
|
}),
|
|
328
387
|
})),
|
|
329
388
|
coalesceKey,
|
|
389
|
+
coalesceMs,
|
|
330
390
|
);
|
|
331
|
-
await
|
|
391
|
+
await finishGroupTimingGsapFallback({
|
|
392
|
+
projectId,
|
|
332
393
|
iframe: previewIframeRef.current,
|
|
333
|
-
needsExtension,
|
|
334
|
-
rootDurationSeconds: maxEnd,
|
|
335
394
|
reloadPreview,
|
|
336
|
-
|
|
337
|
-
|
|
395
|
+
label: "Resize timeline clips",
|
|
396
|
+
errorLabel: "Failed to scale GSAP positions",
|
|
397
|
+
coalesceKey,
|
|
398
|
+
recordEdit,
|
|
399
|
+
activeCompPath,
|
|
400
|
+
changes,
|
|
401
|
+
resolveChangePath: (element) => targetPathFor(element, activeCompPath),
|
|
402
|
+
mutateChange: (change, changePath) => {
|
|
403
|
+
const domId = change.element.domId;
|
|
404
|
+
const timingChanged =
|
|
405
|
+
change.start !== change.element.start || change.duration !== change.element.duration;
|
|
406
|
+
if (!timingChanged || !domId) return null;
|
|
407
|
+
return scaleGsapPositions(
|
|
338
408
|
projectId,
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const timingChanged =
|
|
348
|
-
change.start !== change.element.start ||
|
|
349
|
-
change.duration !== change.element.duration;
|
|
350
|
-
if (!timingChanged || !domId) continue;
|
|
351
|
-
const status = await scaleGsapPositions(
|
|
352
|
-
projectId,
|
|
353
|
-
targetPathFor(change.element, activeCompPath),
|
|
354
|
-
domId,
|
|
355
|
-
change.element.start,
|
|
356
|
-
change.element.duration,
|
|
357
|
-
change.start,
|
|
358
|
-
change.duration,
|
|
359
|
-
);
|
|
360
|
-
mutated = mutated || status.mutated;
|
|
361
|
-
}
|
|
362
|
-
return { mutated };
|
|
363
|
-
},
|
|
364
|
-
}),
|
|
365
|
-
onGsapError: (err) => console.error("[Timeline] Failed to scale GSAP positions", err),
|
|
409
|
+
changePath,
|
|
410
|
+
domId,
|
|
411
|
+
change.element.start,
|
|
412
|
+
change.element.duration,
|
|
413
|
+
change.start,
|
|
414
|
+
change.duration,
|
|
415
|
+
);
|
|
416
|
+
},
|
|
366
417
|
});
|
|
418
|
+
}).catch((error) => {
|
|
419
|
+
// Failed persist: revert the optimistic duration readout + live root
|
|
420
|
+
// alongside the gesture owner's store rollback.
|
|
421
|
+
rollbackDuration();
|
|
422
|
+
throw error;
|
|
367
423
|
});
|
|
368
424
|
},
|
|
369
425
|
[
|
|
370
426
|
activeCompPath,
|
|
371
|
-
domEditSaveTimestampRef,
|
|
372
427
|
enqueueGroupOperation,
|
|
373
428
|
persistServerBatch,
|
|
374
429
|
previewIframeRef,
|
|
375
|
-
projectIdRef,
|
|
376
430
|
recordEdit,
|
|
377
431
|
reloadPreview,
|
|
378
|
-
|
|
379
|
-
writeProjectFile,
|
|
432
|
+
trySdkBatchPersist,
|
|
380
433
|
],
|
|
381
434
|
);
|
|
382
435
|
|
|
@@ -9,10 +9,8 @@ import { Tooltip } from "../../components/ui";
|
|
|
9
9
|
import { ShortcutsPanel } from "./ShortcutsPanel";
|
|
10
10
|
import { SpeedMenu } from "./SpeedMenu";
|
|
11
11
|
import { useSeekBarDrag, resolveSeekPercent } from "./useSeekBarDrag";
|
|
12
|
-
import { useState } from "react";
|
|
13
12
|
|
|
14
13
|
export { resolveSeekPercent };
|
|
15
|
-
type TimeDisplayMode = "time" | "frame";
|
|
16
14
|
|
|
17
15
|
/* ── Icon sub-components ─────────────────────────────────────────── */
|
|
18
16
|
|
|
@@ -369,7 +367,8 @@ export const PlayerControls = memo(function PlayerControls({
|
|
|
369
367
|
const outPoint = usePlayerStore((s) => s.outPoint);
|
|
370
368
|
const setInPoint = usePlayerStore.getState().setInPoint;
|
|
371
369
|
const setOutPoint = usePlayerStore.getState().setOutPoint;
|
|
372
|
-
const
|
|
370
|
+
const timeDisplayMode = usePlayerStore((s) => s.timeDisplayMode);
|
|
371
|
+
const setTimeDisplayMode = usePlayerStore.getState().setTimeDisplayMode;
|
|
373
372
|
|
|
374
373
|
const progressFillRef = useRef<HTMLDivElement>(null);
|
|
375
374
|
const progressThumbRef = useRef<HTMLDivElement>(null);
|
|
@@ -428,10 +427,11 @@ export const PlayerControls = memo(function PlayerControls({
|
|
|
428
427
|
|
|
429
428
|
return (
|
|
430
429
|
<div
|
|
430
|
+
// No own background/border: the transport blends into the preview
|
|
431
|
+
// panel's surface — buttons carry their own chrome.
|
|
431
432
|
className="px-4 py-2 flex flex-wrap items-center gap-x-2 gap-y-1"
|
|
432
433
|
aria-disabled={disabled || undefined}
|
|
433
434
|
style={{
|
|
434
|
-
borderTop: "1px solid rgba(255,255,255,0.04)",
|
|
435
435
|
paddingBottom: "calc(0.5rem + env(safe-area-inset-bottom))",
|
|
436
436
|
}}
|
|
437
437
|
>
|
|
@@ -456,7 +456,7 @@ export const PlayerControls = memo(function PlayerControls({
|
|
|
456
456
|
>
|
|
457
457
|
<button
|
|
458
458
|
type="button"
|
|
459
|
-
onClick={() => setTimeDisplayMode(
|
|
459
|
+
onClick={() => setTimeDisplayMode(timeDisplayMode === "time" ? "frame" : "time")}
|
|
460
460
|
disabled={disabled}
|
|
461
461
|
className="font-mono text-[11px] tabular-nums flex-shrink-0 w-[118px] text-left transition-colors disabled:pointer-events-none hover:opacity-80"
|
|
462
462
|
style={{ color: "#A1A1AA", cursor: "pointer" }}
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* no matter how far the tracks are scrolled. The head is OUTLINE-only at rest and
|
|
10
10
|
* FILLED while the playhead is actively held/scrubbed (`scrubbing`).
|
|
11
11
|
*/
|
|
12
|
+
import { PLAYHEAD_HEAD_W } from "./timelineLayout";
|
|
13
|
+
|
|
12
14
|
interface PlayheadIndicatorProps {
|
|
13
15
|
/** CSS color, defaults to the HF accent variable */
|
|
14
16
|
color?: string;
|
|
@@ -31,8 +33,11 @@ export function PlayheadIndicator({
|
|
|
31
33
|
}: PlayheadIndicatorProps) {
|
|
32
34
|
// Head chip dimensions — used to compute the centering offset and the
|
|
33
35
|
// point where the vertical line starts (so it begins at the head's bottom
|
|
34
|
-
// edge rather than running through the hollow diamond center).
|
|
35
|
-
|
|
36
|
+
// edge rather than running through the hollow diamond center). The width is
|
|
37
|
+
// the shared PLAYHEAD_HEAD_W constant: getTimelinePlayheadLeft shifts the
|
|
38
|
+
// wrapper by -PLAYHEAD_HEAD_W/2 so the 1px line (centered at 50% of the
|
|
39
|
+
// wrapper) lands exactly on GUTTER + time * pps — the ruler ticks' center x.
|
|
40
|
+
const HEAD_W = PLAYHEAD_HEAD_W;
|
|
36
41
|
const HEAD_H = 9;
|
|
37
42
|
// marginTop(1) + HEAD_H = where the line should start.
|
|
38
43
|
const HEAD_TOTAL_H = 1 + HEAD_H;
|