@hyperframes/studio 0.7.56 → 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-C4csZims.js → index-Bf1x1y8H.js} +1 -1
- package/dist/assets/{index-CmVCjZjp.js → index-DfmYkU44.js} +196 -196
- package/dist/assets/{index-BWnOxAiH.js → index-KsfE1bUu.js} +1 -1
- package/dist/assets/index-_pqzyxB1.css +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.html +2 -2
- package/dist/index.js +4345 -3812
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/components/editor/CanvasContextMenu.test.tsx +152 -1
- package/src/components/editor/CanvasContextMenu.tsx +33 -18
- package/src/components/editor/DomEditOverlay.tsx +31 -62
- package/src/components/editor/LayersPanel.tsx +10 -1
- package/src/components/editor/canvasContextMenuZOrder.test.ts +186 -2
- package/src/components/editor/canvasContextMenuZOrder.ts +144 -22
- package/src/components/editor/propertyPanelColorGradingSection.tsx +15 -21
- package/src/components/editor/useCanvasContextMenuState.ts +92 -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 +36 -16
- 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 +12 -1
- package/src/hooks/timelineEditingHelpers.test.ts +91 -0
- package/src/hooks/timelineEditingHelpers.ts +31 -244
- package/src/hooks/timelineMoveAdapter.test.ts +44 -40
- package/src/hooks/timelineMoveAdapter.ts +5 -3
- package/src/hooks/timelineTimingSync.test.ts +150 -0
- package/src/hooks/timelineTimingSync.ts +412 -0
- package/src/hooks/useCaptionDetection.ts +2 -0
- package/src/hooks/useCompositionDimensions.ts +32 -5
- package/src/hooks/useDomEditCommits.test.tsx +92 -1
- package/src/hooks/useDomEditCommits.ts +63 -1
- package/src/hooks/useElementLifecycleOps.test.tsx +165 -0
- package/src/hooks/useElementLifecycleOps.ts +20 -7
- package/src/hooks/useElementPicker.ts +7 -8
- package/src/hooks/useTimelineAssetDropOps.ts +175 -0
- package/src/hooks/useTimelineEditing.test.tsx +378 -212
- package/src/hooks/useTimelineEditing.ts +181 -265
- package/src/hooks/useTimelineGroupEditing.ts +153 -132
- package/src/player/components/PlayerControls.tsx +5 -5
- package/src/player/components/PlayheadIndicator.tsx +7 -2
- package/src/player/components/Timeline.test.ts +85 -8
- package/src/player/components/Timeline.tsx +9 -9
- package/src/player/components/TimelineCanvas.tsx +11 -2
- package/src/player/components/TimelineRuler.tsx +58 -48
- package/src/player/components/timelineClipDragCommit.test.ts +91 -8
- package/src/player/components/timelineClipDragCommit.ts +106 -9
- package/src/player/components/timelineLayout.ts +99 -26
- 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/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/useTimelineRevealClip.ts +56 -0
- package/src/player/components/useTimelineStackingSync.test.tsx +11 -1
- package/src/player/components/useTimelineStackingSync.ts +13 -2
- package/src/player/hooks/useExpandedTimelineElements.ts +7 -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 +4 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +9 -1
- package/src/player/index.ts +3 -1
- package/src/player/lib/runtimeProtocol.test.ts +48 -0
- package/src/player/lib/runtimeProtocol.ts +65 -0
- package/src/player/lib/timelineDOM.ts +9 -0
- package/src/player/lib/timelineElementHelpers.ts +19 -0
- package/src/player/lib/timelineIframeHelpers.ts +2 -4
- package/src/player/store/playerStore.test.ts +38 -13
- package/src/player/store/playerStore.ts +37 -0
- package/src/utils/assetPreviewDismiss.test.ts +27 -0
- package/src/utils/assetPreviewDismiss.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
|
@@ -3,35 +3,27 @@ import { useCallback, useRef } from "react";
|
|
|
3
3
|
import type { TimelineElement } from "../player";
|
|
4
4
|
import { usePlayerStore } from "../player";
|
|
5
5
|
import { useRazorSplit } from "./useRazorSplit";
|
|
6
|
-
import {
|
|
7
|
-
buildTimelineAssetId,
|
|
8
|
-
buildTimelineAssetInsertHtml,
|
|
9
|
-
buildTimelineFileDropPlacements,
|
|
10
|
-
getTimelineAssetKind,
|
|
11
|
-
insertTimelineAssetIntoSource,
|
|
12
|
-
resolveTimelineAssetInitialGeometry,
|
|
13
|
-
resolveTimelineAssetSrc,
|
|
14
|
-
} from "../utils/timelineAssetDrop";
|
|
6
|
+
import { useTimelineAssetDropOps } from "./useTimelineAssetDropOps";
|
|
15
7
|
import { saveProjectFilesWithHistory } from "../utils/studioFileHistory";
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
resolveDroppedAssetDuration,
|
|
20
|
-
} from "../utils/studioHelpers";
|
|
8
|
+
import { setCompositionDurationToContent } from "../utils/timelineAssetDrop";
|
|
9
|
+
import { furthestClipEndFromSource } from "../player/lib/timelineElementHelpers";
|
|
10
|
+
import { getTimelineElementLabel } from "../utils/studioHelpers";
|
|
21
11
|
import {
|
|
22
12
|
applyTimelineStackingReorder,
|
|
23
13
|
buildPatchTarget,
|
|
24
14
|
patchIframeDomTiming,
|
|
25
15
|
persistTimelineEdit,
|
|
26
|
-
readFileContent,
|
|
27
|
-
foldedShiftGsapMutation,
|
|
28
|
-
foldedScaleGsapMutation,
|
|
29
16
|
formatTimelineAttributeNumber,
|
|
30
|
-
finishTimelineTimingFallback,
|
|
31
17
|
extendRootDurationIfNeeded,
|
|
32
18
|
buildTimelineMoveTimingPatch,
|
|
33
19
|
buildTimelineResizeTimingPatch,
|
|
34
20
|
} from "./timelineEditingHelpers";
|
|
21
|
+
import {
|
|
22
|
+
captureDurationRollback,
|
|
23
|
+
finishClipTimingFallback,
|
|
24
|
+
readFileContent,
|
|
25
|
+
syncPreviewContentDuration,
|
|
26
|
+
} from "./timelineTimingSync";
|
|
35
27
|
import type { PersistTimelineEditInput } from "./timelineEditingHelpers";
|
|
36
28
|
import type { TimelineStackingReorderIntent } from "../player/components/timelineEditing";
|
|
37
29
|
import {
|
|
@@ -137,11 +129,22 @@ export function useTimelineEditing({
|
|
|
137
129
|
(element: TimelineElement, updates: TimelineMoveUpdates) => {
|
|
138
130
|
const targetPath = element.sourceFile || activeCompPath || "index.html";
|
|
139
131
|
const startChanged = updates.start !== element.start;
|
|
132
|
+
// A vertical-only lane move arrives with start unchanged but track changed
|
|
133
|
+
// (on this single-element path the drag commit has already folded the
|
|
134
|
+
// AUTHORED persist track into updates.track). It must persist like any
|
|
135
|
+
// other move — early-returning on !startChanged alone silently dropped
|
|
136
|
+
// the file write, so the lane snapped back on reload.
|
|
137
|
+
const trackChanged = updates.track !== element.track;
|
|
140
138
|
|
|
141
|
-
if (startChanged) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
139
|
+
if (startChanged || trackChanged) {
|
|
140
|
+
const liveAttrs: Array<[string, string]> = [];
|
|
141
|
+
if (startChanged) {
|
|
142
|
+
liveAttrs.push(["data-start", formatTimelineAttributeNumber(updates.start)]);
|
|
143
|
+
}
|
|
144
|
+
if (trackChanged) {
|
|
145
|
+
liveAttrs.push(["data-track-index", formatTimelineAttributeNumber(updates.track)]);
|
|
146
|
+
}
|
|
147
|
+
patchIframeDomTiming(previewIframeRef.current, element, liveAttrs);
|
|
145
148
|
}
|
|
146
149
|
|
|
147
150
|
const reorderDone = applyTimelineStackingReorder({
|
|
@@ -153,62 +156,76 @@ export function useTimelineEditing({
|
|
|
153
156
|
commit: handleDomZIndexReorderCommitRef?.current,
|
|
154
157
|
});
|
|
155
158
|
|
|
156
|
-
if (!startChanged) return reorderDone;
|
|
159
|
+
if (!startChanged && !trackChanged) return reorderDone;
|
|
160
|
+
|
|
161
|
+
// Snapshot the duration BEFORE the optimistic updates below so a failed
|
|
162
|
+
// persist can roll the readout + live root back (see captureDurationRollback).
|
|
163
|
+
const rollbackDuration = captureDurationRollback(previewIframeRef.current);
|
|
164
|
+
// needsExtension gates the SDK path (setTiming can't grow the root duration), so read the store BEFORE the readout sync below optimistically updates it.
|
|
165
|
+
const needsExtension = extendRootDurationIfNeeded(updates.start + element.duration);
|
|
166
|
+
// Optimistic duration readout: content-driven (grow AND shrink), from the just-patched live DOM. See syncPreviewContentDuration.
|
|
167
|
+
syncPreviewContentDuration(previewIframeRef.current);
|
|
157
168
|
|
|
158
169
|
const buildMovePatches: PersistTimelineEditInput["buildPatches"] = (original, target) => {
|
|
159
|
-
|
|
170
|
+
// Persist lane changes too — data-start-only writes let reload snap the lane back.
|
|
171
|
+
const track = trackChanged ? updates.track : undefined;
|
|
172
|
+
return buildTimelineMoveTimingPatch(
|
|
173
|
+
original,
|
|
174
|
+
target,
|
|
175
|
+
updates.start,
|
|
176
|
+
element.duration,
|
|
177
|
+
track,
|
|
178
|
+
);
|
|
160
179
|
};
|
|
161
180
|
const coalesceKey = `timeline-move:${element.hfId ?? element.id}`;
|
|
162
181
|
const moveFallback = () =>
|
|
163
|
-
enqueueEdit(element, "Move timeline clip", buildMovePatches, coalesceKey).then(() =>
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return finishTimelineTimingFallback({
|
|
182
|
+
enqueueEdit(element, "Move timeline clip", buildMovePatches, coalesceKey).then(() =>
|
|
183
|
+
// Soft-reload with the server's rewritten GSAP script — the timing-only move already patched
|
|
184
|
+
// DOM + store, so swapping the script avoids the all-clips flash; falls back to reloadPreview().
|
|
185
|
+
finishClipTimingFallback({
|
|
168
186
|
iframe: previewIframeRef.current,
|
|
169
|
-
needsExtension,
|
|
170
|
-
rootDurationSeconds: updates.start + element.duration,
|
|
171
187
|
reloadPreview,
|
|
172
|
-
|
|
173
|
-
delta !== 0 && domId && pid
|
|
174
|
-
? foldedShiftGsapMutation({
|
|
175
|
-
projectId: pid,
|
|
176
|
-
targetPath,
|
|
177
|
-
domId,
|
|
178
|
-
delta,
|
|
179
|
-
label: "Move timeline clip",
|
|
180
|
-
coalesceKey,
|
|
181
|
-
recordEdit,
|
|
182
|
-
})
|
|
183
|
-
: undefined,
|
|
184
|
-
onGsapError: (err) => console.error("[Timeline] Failed to shift GSAP positions", err),
|
|
185
|
-
});
|
|
186
|
-
});
|
|
187
|
-
const needsExtension = extendRootDurationIfNeeded(updates.start + element.duration);
|
|
188
|
-
return reorderDone.then(() => {
|
|
189
|
-
if (sdkSession && element.hfId && !needsExtension) {
|
|
190
|
-
return sdkTimingPersist(
|
|
191
|
-
element.hfId,
|
|
188
|
+
projectId: projectIdRef.current,
|
|
192
189
|
targetPath,
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
190
|
+
domId: element.domId,
|
|
191
|
+
label: "Move timeline clip",
|
|
192
|
+
coalesceKey,
|
|
193
|
+
recordEdit,
|
|
194
|
+
edit: { kind: "shift", delta: updates.start - element.start },
|
|
195
|
+
}),
|
|
196
|
+
);
|
|
197
|
+
return reorderDone
|
|
198
|
+
.then(() => {
|
|
199
|
+
// The SDK setTiming path writes start only — a lane change must take
|
|
200
|
+
// the fallback, whose patch builder writes data-track-index too.
|
|
201
|
+
if (sdkSession && element.hfId && !needsExtension && !trackChanged) {
|
|
202
|
+
return sdkTimingPersist(
|
|
203
|
+
element.hfId,
|
|
204
|
+
targetPath,
|
|
205
|
+
{ start: updates.start },
|
|
206
|
+
sdkSession,
|
|
207
|
+
{
|
|
208
|
+
editHistory: { recordEdit },
|
|
209
|
+
writeProjectFile,
|
|
210
|
+
reloadPreview,
|
|
211
|
+
domEditSaveTimestampRef,
|
|
212
|
+
compositionPath: activeCompPath,
|
|
213
|
+
// Capture on-disk bytes as the undo `before` so undoing a timing move
|
|
214
|
+
// restores the file verbatim, not a normalized full-DOM re-emit.
|
|
215
|
+
readProjectFile: (path) => readFileContent(projectIdRef.current ?? "", path),
|
|
216
|
+
},
|
|
217
|
+
{ label: "Move timeline clip", coalesceKey },
|
|
218
|
+
).then((handled) => {
|
|
219
|
+
if (!handled) return moveFallback();
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return moveFallback();
|
|
223
|
+
})
|
|
224
|
+
.catch((error) => {
|
|
225
|
+
// Failed persist: revert the optimistic duration readout + live root.
|
|
226
|
+
rollbackDuration();
|
|
227
|
+
throw error;
|
|
228
|
+
});
|
|
212
229
|
},
|
|
213
230
|
[
|
|
214
231
|
previewIframeRef,
|
|
@@ -245,6 +262,13 @@ export function useTimelineEditing({
|
|
|
245
262
|
liveAttrs.push([liveAttr, formatTimelineAttributeNumber(updates.playbackStart)]);
|
|
246
263
|
}
|
|
247
264
|
patchIframeDomTiming(previewIframeRef.current, element, liveAttrs);
|
|
265
|
+
// Snapshot the duration BEFORE the optimistic updates below so a failed
|
|
266
|
+
// persist can roll the readout + live root back (see captureDurationRollback).
|
|
267
|
+
const rollbackDuration = captureDurationRollback(previewIframeRef.current);
|
|
268
|
+
// needsExtension gates the SDK path (setTiming can't grow the root duration), so read the store BEFORE the readout sync below optimistically updates it.
|
|
269
|
+
const needsExtension = extendRootDurationIfNeeded(updates.start + updates.duration);
|
|
270
|
+
// Optimistic duration readout: content-driven (grow AND shrink), from the just-patched live DOM. See syncPreviewContentDuration.
|
|
271
|
+
syncPreviewContentDuration(previewIframeRef.current);
|
|
248
272
|
const targetPath = element.sourceFile || activeCompPath || "index.html";
|
|
249
273
|
const buildResizePatches: PersistTimelineEditInput["buildPatches"] = (original, target) => {
|
|
250
274
|
return buildTimelineResizeTimingPatch(original, target, element, updates);
|
|
@@ -253,59 +277,55 @@ export function useTimelineEditing({
|
|
|
253
277
|
updates.playbackStart != null ||
|
|
254
278
|
(updates.start !== element.start && element.playbackStart != null);
|
|
255
279
|
// Server-path fallback: after persisting the attr patch, scale GSAP tween
|
|
256
|
-
// positions/durations on the server
|
|
257
|
-
//
|
|
280
|
+
// positions/durations on the server, then soft-reload with the rewritten
|
|
281
|
+
// script (timing-only resize) — same no-flash path as move; full reload is
|
|
282
|
+
// the fallback.
|
|
258
283
|
const coalesceKey = `timeline-resize:${element.hfId ?? element.id}`;
|
|
259
|
-
const timingChanged =
|
|
260
|
-
updates.start !== element.start || updates.duration !== element.duration;
|
|
261
|
-
const needsExtension = extendRootDurationIfNeeded(updates.start + updates.duration);
|
|
262
284
|
const resizeFallback = () =>
|
|
263
|
-
enqueueEdit(element, "Resize timeline clip", buildResizePatches, coalesceKey).then(() =>
|
|
264
|
-
|
|
265
|
-
const domId = element.domId;
|
|
266
|
-
return finishTimelineTimingFallback({
|
|
285
|
+
enqueueEdit(element, "Resize timeline clip", buildResizePatches, coalesceKey).then(() =>
|
|
286
|
+
finishClipTimingFallback({
|
|
267
287
|
iframe: previewIframeRef.current,
|
|
268
|
-
needsExtension,
|
|
269
|
-
rootDurationSeconds: updates.start + updates.duration,
|
|
270
288
|
reloadPreview,
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
289
|
+
projectId: projectIdRef.current,
|
|
290
|
+
targetPath,
|
|
291
|
+
domId: element.domId,
|
|
292
|
+
label: "Resize timeline clip",
|
|
293
|
+
coalesceKey,
|
|
294
|
+
recordEdit,
|
|
295
|
+
edit: {
|
|
296
|
+
kind: "scale",
|
|
297
|
+
from: { start: element.start, duration: element.duration },
|
|
298
|
+
to: { start: updates.start, duration: updates.duration },
|
|
299
|
+
},
|
|
300
|
+
}),
|
|
301
|
+
);
|
|
302
|
+
const persistDone =
|
|
303
|
+
sdkSession && element.hfId && !hasPbsAdjustment && !needsExtension
|
|
304
|
+
? sdkTimingPersist(
|
|
305
|
+
element.hfId,
|
|
306
|
+
targetPath,
|
|
307
|
+
{ start: updates.start, duration: updates.duration },
|
|
308
|
+
sdkSession,
|
|
309
|
+
{
|
|
310
|
+
editHistory: { recordEdit },
|
|
311
|
+
writeProjectFile,
|
|
312
|
+
reloadPreview,
|
|
313
|
+
domEditSaveTimestampRef,
|
|
314
|
+
compositionPath: activeCompPath,
|
|
315
|
+
// Capture on-disk bytes as the undo `before` so undoing a timing
|
|
316
|
+
// resize restores the file verbatim, not a normalized full-DOM re-emit.
|
|
317
|
+
readProjectFile: (path) => readFileContent(projectIdRef.current ?? "", path),
|
|
318
|
+
},
|
|
319
|
+
{ label: "Resize timeline clip", coalesceKey },
|
|
320
|
+
).then((handled) => {
|
|
321
|
+
if (!handled) return resizeFallback();
|
|
322
|
+
})
|
|
323
|
+
: resizeFallback();
|
|
324
|
+
return persistDone.catch((error) => {
|
|
325
|
+
// Failed persist: revert the optimistic duration readout + live root.
|
|
326
|
+
rollbackDuration();
|
|
327
|
+
throw error;
|
|
328
|
+
});
|
|
309
329
|
},
|
|
310
330
|
[
|
|
311
331
|
previewIframeRef,
|
|
@@ -384,19 +404,37 @@ export function useTimelineEditing({
|
|
|
384
404
|
changed?: boolean;
|
|
385
405
|
content?: string;
|
|
386
406
|
};
|
|
387
|
-
const
|
|
407
|
+
const removedContent =
|
|
388
408
|
typeof removeData.content === "string" ? removeData.content : originalContent;
|
|
409
|
+
// Content-driven duration: shrink the composition to the furthest
|
|
410
|
+
// remaining clip end, read from the post-removal SOURCE (raw
|
|
411
|
+
// data-duration), so deleting the last/longest clip removes trailing
|
|
412
|
+
// empty space. Measured from the source, not the store, whose
|
|
413
|
+
// durations are runtime-truncated.
|
|
414
|
+
const deleteContentEnd = furthestClipEndFromSource(removedContent);
|
|
415
|
+
const patchedContent = setCompositionDurationToContent(removedContent, deleteContentEnd);
|
|
416
|
+
// Optimistically reflect the shrunk length in the readout/seek bar,
|
|
417
|
+
// rolling it back if the persist below fails (see captureDurationRollback).
|
|
418
|
+
const rollbackDuration = captureDurationRollback(previewIframeRef.current);
|
|
419
|
+
if (deleteContentEnd > 0 && targetPath === (activeCompPath || "index.html")) {
|
|
420
|
+
usePlayerStore.getState().setDuration(deleteContentEnd);
|
|
421
|
+
}
|
|
389
422
|
|
|
390
423
|
domEditSaveTimestampRef.current = Date.now();
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
424
|
+
try {
|
|
425
|
+
await saveProjectFilesWithHistory({
|
|
426
|
+
projectId: pid,
|
|
427
|
+
label: "Delete timeline clip",
|
|
428
|
+
kind: "timeline",
|
|
429
|
+
files: { [targetPath]: patchedContent },
|
|
430
|
+
readFile: async () => originalContent,
|
|
431
|
+
writeFile: writeProjectFile,
|
|
432
|
+
recordEdit,
|
|
433
|
+
});
|
|
434
|
+
} catch (error) {
|
|
435
|
+
rollbackDuration();
|
|
436
|
+
throw error;
|
|
437
|
+
}
|
|
400
438
|
|
|
401
439
|
usePlayerStore
|
|
402
440
|
.getState()
|
|
@@ -422,145 +460,23 @@ export function useTimelineEditing({
|
|
|
422
460
|
reloadPreview,
|
|
423
461
|
isRecordingRef,
|
|
424
462
|
forceReloadSdkSession,
|
|
463
|
+
previewIframeRef,
|
|
425
464
|
],
|
|
426
465
|
);
|
|
427
466
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
if (!pid) throw new Error("No active project");
|
|
442
|
-
|
|
443
|
-
const kind = getTimelineAssetKind(assetPath);
|
|
444
|
-
if (!kind) {
|
|
445
|
-
showToast("Only image, video, and audio assets can be dropped onto the timeline.");
|
|
446
|
-
return;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
const targetPath = activeCompPath || "index.html";
|
|
450
|
-
try {
|
|
451
|
-
const originalContent = await readFileContent(pid, targetPath);
|
|
452
|
-
|
|
453
|
-
const normalizedStart = Number(formatTimelineAttributeNumber(placement.start));
|
|
454
|
-
const duration =
|
|
455
|
-
Number.isFinite(durationOverride) && durationOverride != null && durationOverride > 0
|
|
456
|
-
? durationOverride
|
|
457
|
-
: await resolveDroppedAssetDuration(pid, assetPath, kind);
|
|
458
|
-
const normalizedDuration = Number(formatTimelineAttributeNumber(duration));
|
|
459
|
-
const newId = buildTimelineAssetId(assetPath, collectHtmlIds(originalContent));
|
|
460
|
-
const resolvedAssetSrc = resolveTimelineAssetSrc(targetPath, assetPath);
|
|
461
|
-
|
|
462
|
-
const resolvedTargetPath = targetPath || "index.html";
|
|
463
|
-
const relevantElements = timelineElements.filter(
|
|
464
|
-
(te) => (te.sourceFile || activeCompPath || "index.html") === resolvedTargetPath,
|
|
465
|
-
);
|
|
466
|
-
const newElementZIndex = Math.max(1, relevantElements.length + 1);
|
|
467
|
-
|
|
468
|
-
const patchedContent = insertTimelineAssetIntoSource(
|
|
469
|
-
originalContent,
|
|
470
|
-
buildTimelineAssetInsertHtml({
|
|
471
|
-
id: newId,
|
|
472
|
-
assetPath: resolvedAssetSrc,
|
|
473
|
-
kind,
|
|
474
|
-
start: normalizedStart,
|
|
475
|
-
duration: normalizedDuration,
|
|
476
|
-
track: placement.track,
|
|
477
|
-
zIndex: newElementZIndex,
|
|
478
|
-
geometry: resolveTimelineAssetInitialGeometry(originalContent),
|
|
479
|
-
}),
|
|
480
|
-
);
|
|
481
|
-
|
|
482
|
-
domEditSaveTimestampRef.current = Date.now();
|
|
483
|
-
await saveProjectFilesWithHistory({
|
|
484
|
-
projectId: pid,
|
|
485
|
-
label: "Add timeline asset",
|
|
486
|
-
kind: "timeline",
|
|
487
|
-
files: { [targetPath]: patchedContent },
|
|
488
|
-
readFile: async () => originalContent,
|
|
489
|
-
writeFile: writeProjectFile,
|
|
490
|
-
recordEdit,
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
forceReloadSdkSession?.();
|
|
494
|
-
reloadPreview();
|
|
495
|
-
} catch (error) {
|
|
496
|
-
const message =
|
|
497
|
-
error instanceof Error ? error.message : "Failed to drop asset onto timeline";
|
|
498
|
-
showToast(message);
|
|
499
|
-
}
|
|
500
|
-
},
|
|
501
|
-
[
|
|
502
|
-
activeCompPath,
|
|
503
|
-
recordEdit,
|
|
504
|
-
showToast,
|
|
505
|
-
timelineElements,
|
|
506
|
-
writeProjectFile,
|
|
507
|
-
domEditSaveTimestampRef,
|
|
508
|
-
reloadPreview,
|
|
509
|
-
isRecordingRef,
|
|
510
|
-
forceReloadSdkSession,
|
|
511
|
-
],
|
|
512
|
-
);
|
|
513
|
-
|
|
514
|
-
// fallow-ignore-next-line complexity
|
|
515
|
-
const handleTimelineFileDrop = useCallback(
|
|
516
|
-
// fallow-ignore-next-line complexity
|
|
517
|
-
async (files: File[], placement?: Pick<TimelineElement, "start" | "track">) => {
|
|
518
|
-
if (isRecordingRef?.current) {
|
|
519
|
-
showToast("Cannot edit timeline while recording", "error");
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
-
const pid = projectIdRef.current;
|
|
523
|
-
if (!pid) return;
|
|
524
|
-
const uploaded = await uploadProjectFiles(files);
|
|
525
|
-
if (uploaded.length === 0) return;
|
|
526
|
-
const durations: number[] = [];
|
|
527
|
-
for (const assetPath of uploaded) {
|
|
528
|
-
const kind = getTimelineAssetKind(assetPath);
|
|
529
|
-
const duration = kind ? await resolveDroppedAssetDuration(pid, assetPath, kind) : 0;
|
|
530
|
-
durations.push(Number(formatTimelineAttributeNumber(duration)));
|
|
531
|
-
}
|
|
532
|
-
const placements = buildTimelineFileDropPlacements(
|
|
533
|
-
placement ?? { start: 0, track: 0 },
|
|
534
|
-
durations,
|
|
535
|
-
timelineElements
|
|
536
|
-
.filter(
|
|
537
|
-
(te) =>
|
|
538
|
-
(te.sourceFile || activeCompPath || "index.html") ===
|
|
539
|
-
(activeCompPath || "index.html"),
|
|
540
|
-
)
|
|
541
|
-
.map((te) => ({
|
|
542
|
-
start: te.start,
|
|
543
|
-
duration: te.duration,
|
|
544
|
-
track: te.track,
|
|
545
|
-
})),
|
|
546
|
-
);
|
|
547
|
-
for (const [index, assetPath] of uploaded.entries()) {
|
|
548
|
-
await handleTimelineAssetDrop(
|
|
549
|
-
assetPath,
|
|
550
|
-
placements[index] ?? placements[0],
|
|
551
|
-
durations[index],
|
|
552
|
-
);
|
|
553
|
-
}
|
|
554
|
-
},
|
|
555
|
-
[
|
|
556
|
-
activeCompPath,
|
|
557
|
-
handleTimelineAssetDrop,
|
|
558
|
-
timelineElements,
|
|
559
|
-
uploadProjectFiles,
|
|
560
|
-
isRecordingRef,
|
|
561
|
-
showToast,
|
|
562
|
-
],
|
|
563
|
-
);
|
|
467
|
+
const { handleTimelineAssetDrop, handleTimelineFileDrop } = useTimelineAssetDropOps({
|
|
468
|
+
projectIdRef,
|
|
469
|
+
activeCompPath,
|
|
470
|
+
timelineElements,
|
|
471
|
+
showToast,
|
|
472
|
+
writeProjectFile,
|
|
473
|
+
recordEdit,
|
|
474
|
+
domEditSaveTimestampRef,
|
|
475
|
+
reloadPreview,
|
|
476
|
+
uploadProjectFiles,
|
|
477
|
+
isRecordingRef,
|
|
478
|
+
forceReloadSdkSession,
|
|
479
|
+
});
|
|
564
480
|
|
|
565
481
|
const handleBlockedTimelineEdit = useCallback(
|
|
566
482
|
(_element: TimelineElement) => {
|