@hyperframes/studio 0.8.78 → 0.8.80
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-CgnoxDoD.js +464 -0
- package/dist/assets/{index-CX_4MYbk.js → index-CsG24Jub.js} +190 -187
- package/dist/assets/{index-C_n__Uh3.js → index-DZGVvqDT.js} +1 -1
- package/dist/assets/{index-DdpuHaGN.js → index-J9HXe7mU.js} +1 -1
- package/dist/{chunk-5VANA2TG.js → chunk-I7XNAIOF.js} +2 -1
- package/dist/{domEditingLayers-IGPLTI5E.js → domEditingLayers-MHFDKAQ7.js} +2 -2
- package/dist/index.d.ts +40 -16
- package/dist/index.html +1 -1
- package/dist/index.js +1918 -1408
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/App.tsx +6 -1
- package/src/components/editor/TimelineFxPopover.test.tsx +1 -1
- package/src/components/editor/TimelineFxPopover.tsx +1 -1
- package/src/components/editor/manualEdits.ts +1 -0
- package/src/components/editor/manualEditsParsing.ts +4 -0
- package/src/components/editor/useFxAudition.ts +3 -3
- package/src/components/nle/useTimelineEditCallbacks.ts +12 -2
- package/src/contexts/DomEditContext.tsx +4 -0
- package/src/contexts/TimelineEditContext.tsx +2 -0
- package/src/hooks/liveLanes.ts +147 -0
- package/src/hooks/timelineAudioGroupVolume.ts +97 -53
- package/src/hooks/timelineEditPermission.ts +55 -13
- package/src/hooks/timelineEditingHelpers.test.ts +16 -0
- package/src/hooks/timelineEditingHelpers.ts +69 -43
- package/src/hooks/timelineElementFxAttribute.ts +92 -40
- package/src/hooks/useAppHotkeys.ts +1 -1
- package/src/hooks/useAutomationSelectionKeyboard.test.tsx +125 -21
- package/src/hooks/useAutomationSelectionKeyboard.ts +21 -25
- package/src/hooks/useBlockHandlers.test.tsx +65 -0
- package/src/hooks/useBlockHandlers.ts +7 -4
- package/src/hooks/useClipboard.source.test.ts +340 -0
- package/src/hooks/useClipboard.ts +141 -62
- package/src/hooks/useDomEditAttributeCommits.ts +3 -17
- package/src/hooks/useDomEditSession.ts +1 -0
- package/src/hooks/useEditHistoryActions.ts +4 -3
- package/src/hooks/useElementPicker.test.ts +172 -0
- package/src/hooks/useElementPicker.ts +141 -93
- package/src/hooks/useExternalFileChangeCoordinator.test.tsx +49 -0
- package/src/hooks/useExternalFileChangeCoordinator.ts +15 -6
- package/src/hooks/useFrameCapture.test.tsx +67 -0
- package/src/hooks/useFrameCapture.ts +18 -9
- package/src/hooks/usePersistentEditHistory.test.ts +2 -2
- package/src/hooks/usePersistentEditHistory.ts +2 -2
- package/src/hooks/useTimelineEditing.test.tsx +59 -0
- package/src/hooks/useTimelineEditing.ts +59 -47
- package/src/hooks/useTimelineGroupEditing.ts +2 -1
- package/src/hooks/useToast.ts +2 -1
- package/src/index.ts +1 -0
- package/src/player/components/ImageThumbnail.tsx +3 -12
- package/src/player/components/Player.test.ts +28 -0
- package/src/player/components/Player.tsx +5 -2
- package/src/player/components/TimelineAutomationLane.test.tsx +38 -0
- package/src/player/components/TimelineAutomationLane.tsx +18 -13
- package/src/player/components/TimelineAutomationLaneSlot.test.tsx +0 -2
- package/src/player/components/TimelineClipFades.test.tsx +24 -3
- package/src/player/components/TimelineClipFades.tsx +5 -2
- package/src/player/components/TimelineFxButton.tsx +1 -1
- package/src/player/components/TimelineGroupHeader.tsx +1 -1
- package/src/player/components/TimelineGroupRow.test.tsx +0 -1
- package/src/player/components/TimelineGroupRow.tsx +17 -11
- package/src/player/components/TimelineTrackHeader.tsx +21 -6
- package/src/player/components/VideoThumbnail.test.tsx +13 -0
- package/src/player/components/VideoThumbnail.tsx +1 -1
- package/src/player/components/groupAutomationElement.ts +9 -1
- package/src/player/components/timelineCallbacks.ts +5 -2
- package/src/player/components/timelineClipDragPreview.test.ts +41 -0
- package/src/player/components/timelineClipDragPreview.ts +15 -24
- package/src/player/components/timelineEditing.test.ts +99 -1
- package/src/player/components/timelineEditing.ts +5 -8
- package/src/player/components/timelineGroupEditing.test.ts +27 -0
- package/src/player/components/timelineGroupEditing.ts +18 -15
- package/src/player/components/timelineStackingSync.ts +5 -5
- package/src/player/components/timelineZones.ts +2 -3
- package/src/player/components/useAutomationEdgeStretch.ts +4 -4
- package/src/player/components/useAutomationLaneGestures.ts +1 -1
- package/src/player/components/useAutomationLanes.save.test.tsx +1406 -0
- package/src/player/components/useAutomationLanes.ts +58 -74
- package/src/player/hooks/useTimelinePlayer.hiddenPause.test.ts +75 -0
- package/src/player/hooks/useTimelinePlayer.play.test.ts +36 -0
- package/src/player/hooks/useTimelinePlayer.ts +25 -28
- package/src/player/hooks/useTimelineSyncCallbacks.ts +12 -4
- package/src/player/lib/automationStoreSync.ts +30 -3
- package/src/player/lib/thumbnailImageDecoder.test.ts +82 -0
- package/src/player/lib/thumbnailImageDecoder.ts +73 -0
- package/src/player/lib/thumbnailVideoDecoder.test.ts +102 -6
- package/src/player/lib/thumbnailVideoDecoder.ts +20 -5
- package/src/player/lib/timelineDOM.test.ts +28 -8
- package/src/player/lib/timelineElementHelpers.ts +3 -4
- package/src/player/lib/timelineGroupInfo.ts +23 -23
- package/src/utils/authoredSource.ts +92 -0
- package/src/utils/gsapSoftReload.test.ts +29 -0
- package/src/utils/gsapSoftReload.ts +3 -7
- package/src/utils/studioFileHistory.ts +36 -30
- package/dist/assets/hyperframes-player-BdnQGwG5.js +0 -464
- package/src/player/components/automationGestureKeys.test.ts +0 -49
- package/src/player/components/automationGestureKeys.ts +0 -49
- /package/dist/{chunk-5VANA2TG.js.map → chunk-I7XNAIOF.js.map} +0 -0
- /package/dist/{domEditingLayers-IGPLTI5E.js.map → domEditingLayers-MHFDKAQ7.js.map} +0 -0
|
@@ -6,26 +6,68 @@ export type TimelineEditPermission = true | { blocked: true; reason: string };
|
|
|
6
6
|
|
|
7
7
|
export type CanEditTimelineElement = (element: TimelineElement) => TimelineEditPermission;
|
|
8
8
|
|
|
9
|
+
/** What an effect save did: landed, refused before writing, or failed while writing. */
|
|
10
|
+
export type TimelineEditOutcome =
|
|
11
|
+
| { status: "saved" }
|
|
12
|
+
| { status: "refused" | "failed"; reason: string };
|
|
13
|
+
|
|
14
|
+
/** The project to save into, or why the save stops before it writes. */
|
|
15
|
+
export function projectForTimelineSave(
|
|
16
|
+
isRecording: boolean | undefined,
|
|
17
|
+
projectId: string | null,
|
|
18
|
+
showToast: (message: string, tone?: "error" | "info") => void,
|
|
19
|
+
): string | TimelineEditOutcome {
|
|
20
|
+
if (isRecording) {
|
|
21
|
+
showToast("Cannot edit timeline while recording", "error");
|
|
22
|
+
return { status: "refused", reason: "Cannot edit timeline while recording" };
|
|
23
|
+
}
|
|
24
|
+
return projectId ?? failedTimelineSave("No project is open", showToast);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function failedTimelineSave(
|
|
28
|
+
reason: string,
|
|
29
|
+
showToast: (message: string, tone?: "error" | "info") => void,
|
|
30
|
+
): TimelineEditOutcome {
|
|
31
|
+
showToast(reason);
|
|
32
|
+
return { status: "failed", reason };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function timelineEditRefusal(
|
|
36
|
+
canEdit: CanEditTimelineElement | undefined,
|
|
37
|
+
targets: readonly TimelineElement[],
|
|
38
|
+
): string | null {
|
|
39
|
+
for (const element of targets) {
|
|
40
|
+
const verdict = canEdit?.(element) ?? true;
|
|
41
|
+
if (verdict !== true) return verdict.reason;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
9
46
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
47
|
+
* The host's reason to refuse a write when any target element is blocked, toasted,
|
|
48
|
+
* or null. Absent `canEdit` never refuses, so Studio itself is unchanged.
|
|
12
49
|
*/
|
|
13
|
-
export function
|
|
50
|
+
export function useTimelineEditRefusal(
|
|
14
51
|
canEdit: CanEditTimelineElement | undefined,
|
|
15
52
|
showToast: (message: string, tone?: "error" | "info") => void,
|
|
16
53
|
) {
|
|
17
54
|
return useCallback(
|
|
18
|
-
(targets: readonly TimelineElement[]):
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (verdict !== true) {
|
|
23
|
-
showToast(verdict.reason, "error");
|
|
24
|
-
return false;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
return true;
|
|
55
|
+
(targets: readonly TimelineElement[]): string | null => {
|
|
56
|
+
const reason = timelineEditRefusal(canEdit, targets);
|
|
57
|
+
if (reason !== null) showToast(reason, "error");
|
|
58
|
+
return reason;
|
|
28
59
|
},
|
|
29
60
|
[canEdit, showToast],
|
|
30
61
|
);
|
|
31
62
|
}
|
|
63
|
+
|
|
64
|
+
export function useTimelineEditGate(
|
|
65
|
+
canEdit: CanEditTimelineElement | undefined,
|
|
66
|
+
showToast: (message: string, tone?: "error" | "info") => void,
|
|
67
|
+
) {
|
|
68
|
+
const refuse = useTimelineEditRefusal(canEdit, showToast);
|
|
69
|
+
return useCallback(
|
|
70
|
+
(targets: readonly TimelineElement[]): boolean => refuse(targets) === null,
|
|
71
|
+
[refuse],
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -3,6 +3,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
3
3
|
import {
|
|
4
4
|
applyTimelineStackingReorder,
|
|
5
5
|
buildTimelineMoveTimingPatch,
|
|
6
|
+
buildTimelineResizeTimingPatch,
|
|
6
7
|
deleteSelectedKeyframes,
|
|
7
8
|
extendRootDurationIfNeeded,
|
|
8
9
|
patchIframeDomTiming,
|
|
@@ -230,6 +231,19 @@ describe("extendRootDurationIfNeeded", () => {
|
|
|
230
231
|
});
|
|
231
232
|
});
|
|
232
233
|
|
|
234
|
+
describe("buildTimelineResizeTimingPatch", () => {
|
|
235
|
+
it("moves a source-only in-point by the caller's own start change", () => {
|
|
236
|
+
const source = `<div id="root"><video id="a" class="clip" data-start="5" data-duration="3" data-media-start="0.337"></video></div>`;
|
|
237
|
+
const element = el({ id: "a", tag: "video", domId: "a", start: 5, duration: 3 });
|
|
238
|
+
const patched = buildTimelineResizeTimingPatch(source, { id: "a" }, element, {
|
|
239
|
+
start: 3,
|
|
240
|
+
duration: 5,
|
|
241
|
+
playbackStart: undefined,
|
|
242
|
+
});
|
|
243
|
+
expect(patched).toContain('data-media-start="0"');
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
233
247
|
describe("persistTimelineBatchEdit", () => {
|
|
234
248
|
const SOURCE = `<div id="root"><video id="a" class="clip" data-start="1" data-track-index="0"></video><video id="b" class="clip" data-start="2" data-track-index="1"></video></div>`;
|
|
235
249
|
|
|
@@ -452,6 +466,7 @@ describe("persistElementAttribute", () => {
|
|
|
452
466
|
writeProjectFile,
|
|
453
467
|
recordEdit: vi.fn(),
|
|
454
468
|
pendingTimelineEditPathRef: { current: new Set() },
|
|
469
|
+
onFileRead: vi.fn(),
|
|
455
470
|
patchLive,
|
|
456
471
|
}),
|
|
457
472
|
).rejects.toThrow("Unable to patch element in index.html");
|
|
@@ -495,6 +510,7 @@ describe("persistElementAttribute — unwind value", () => {
|
|
|
495
510
|
writeProjectFile,
|
|
496
511
|
recordEdit: vi.fn(),
|
|
497
512
|
pendingTimelineEditPathRef: { current: new Set() },
|
|
513
|
+
onFileRead: vi.fn(),
|
|
498
514
|
// The live DOM is ALREADY at the new value when the commit runs — that
|
|
499
515
|
// is what `setLive` does on every drag frame.
|
|
500
516
|
patchLive: (v) => patched.push(v),
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "../utils/sourcePatcher";
|
|
9
9
|
import {
|
|
10
10
|
formatTimelineAttributeNumber,
|
|
11
|
+
formatTimelineMediaOffset,
|
|
11
12
|
type TimelineStackingReorderIntent,
|
|
12
13
|
} from "../player/components/timelineEditing";
|
|
13
14
|
import { getElementZIndex } from "../player/lib/layerOrdering";
|
|
@@ -15,7 +16,12 @@ import {
|
|
|
15
16
|
furthestClipEndFromSource,
|
|
16
17
|
getTimelineElementIdentity,
|
|
17
18
|
} from "../player/lib/timelineElementHelpers";
|
|
18
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
saveProjectFilesWithHistory,
|
|
21
|
+
writeProjectFilesWithHistoryInQueue,
|
|
22
|
+
type RecordEditInput,
|
|
23
|
+
} from "../utils/studioFileHistory";
|
|
24
|
+
import { serializeStudioFileMutations } from "../utils/studioFileMutationCoordinator";
|
|
19
25
|
import type { TimelineZIndexReorderCommit } from "./useTimelineEditingTypes";
|
|
20
26
|
import { setCompositionDurationToContent } from "../utils/timelineAssetDrop";
|
|
21
27
|
import { readFileContent } from "./timelineTimingSync";
|
|
@@ -279,7 +285,7 @@ export function buildTimelineResizeTimingPatch(
|
|
|
279
285
|
patched = applyPatchByTarget(patched, target, {
|
|
280
286
|
type: "attribute",
|
|
281
287
|
property: pbs.attrName,
|
|
282
|
-
value:
|
|
288
|
+
value: formatTimelineMediaOffset(pbs.value),
|
|
283
289
|
});
|
|
284
290
|
}
|
|
285
291
|
// Content-driven duration from the PATCHED SOURCE (raw data-duration) —
|
|
@@ -396,7 +402,32 @@ export async function persistTimelineBatchEdit(
|
|
|
396
402
|
});
|
|
397
403
|
}
|
|
398
404
|
|
|
399
|
-
|
|
405
|
+
/** What the file holds for `attr` once queued writes to it land; undefined when unreadable. */
|
|
406
|
+
export async function readSavedAttribute(
|
|
407
|
+
projectId: string | null,
|
|
408
|
+
targetPath: string,
|
|
409
|
+
patchTarget: PatchTarget | null,
|
|
410
|
+
attr: string,
|
|
411
|
+
writeFile: (path: string, content: string, expectedContent?: string) => Promise<void>,
|
|
412
|
+
): Promise<string | null | undefined> {
|
|
413
|
+
if (!projectId || !patchTarget) return undefined;
|
|
414
|
+
const html = await serializeStudioFileMutations(writeFile, [targetPath], () =>
|
|
415
|
+
readFileContent(projectId, targetPath),
|
|
416
|
+
).catch(() => null);
|
|
417
|
+
return html === null ? undefined : readTargetAttribute(html, patchTarget, attr);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** What `html` holds for `attr` on the target; undefined when the target is not in it. */
|
|
421
|
+
export function readTargetAttribute(
|
|
422
|
+
html: string,
|
|
423
|
+
patchTarget: PatchTarget,
|
|
424
|
+
attr: string,
|
|
425
|
+
): string | null | undefined {
|
|
426
|
+
if (readTagSnippetByTarget(html, patchTarget) === undefined) return undefined;
|
|
427
|
+
return readAttributeByTarget(html, patchTarget, attr) ?? null;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export { applyPatchByTarget, formatTimelineAttributeNumber, formatTimelineMediaOffset };
|
|
400
431
|
|
|
401
432
|
export { patchDocumentRootDuration } from "./timelineEditingGsap";
|
|
402
433
|
|
|
@@ -412,6 +443,8 @@ export interface PersistElementAttributeInput {
|
|
|
412
443
|
pendingTimelineEditPathRef: { current: Set<string> };
|
|
413
444
|
/** Write the attribute directly on the live preview DOM node. */
|
|
414
445
|
patchLive: (value: string | null) => void;
|
|
446
|
+
/** What the file held for the attribute, read inside the queue before this write. */
|
|
447
|
+
onFileRead: (value: string | null) => void;
|
|
415
448
|
}
|
|
416
449
|
|
|
417
450
|
/**
|
|
@@ -433,47 +466,40 @@ export async function persistElementAttribute({
|
|
|
433
466
|
recordEdit,
|
|
434
467
|
pendingTimelineEditPathRef,
|
|
435
468
|
patchLive,
|
|
469
|
+
onFileRead,
|
|
436
470
|
}: PersistElementAttributeInput): Promise<string[]> {
|
|
437
|
-
//
|
|
438
|
-
//
|
|
439
|
-
//
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
// time this runs the live DOM already holds the in-progress value. Reading it
|
|
451
|
-
// here made `previousValue === value`, so the unwind below was a no-op, and
|
|
452
|
-
// `setQuiet`'s catch (which deliberately re-mirrors the store from the live
|
|
453
|
-
// DOM) then mirrored that same never-saved value. The group audibly had the
|
|
454
|
-
// preset, the panel agreed, and a reload dropped it — the failure class the
|
|
455
|
-
// target check above was added to close, still open on the live-write path.
|
|
456
|
-
const previousValue = readAttributeByTarget(before, patchTarget, attr) ?? null;
|
|
457
|
-
patchLive(value);
|
|
471
|
+
// Joins the file's mutation queue before reading, so saves land in the order they start
|
|
472
|
+
// and each patches what the save before it wrote. Resolve the target before patching
|
|
473
|
+
// the live DOM, so an unresolvable target never leaves an unsaved preview.
|
|
474
|
+
return serializeStudioFileMutations(writeProjectFile, [targetPath], async () => {
|
|
475
|
+
const before = await readFileContent(projectId, targetPath);
|
|
476
|
+
if (readTagSnippetByTarget(before, patchTarget) === undefined) {
|
|
477
|
+
throw new Error(`Unable to patch element in ${targetPath}`);
|
|
478
|
+
}
|
|
479
|
+
// Unwind to the file's value: live writers already patched the DOM, so `readLive()`
|
|
480
|
+
// would equal `value` and a failed save would keep a never-saved preview.
|
|
481
|
+
const previousValue = readAttributeByTarget(before, patchTarget, attr) ?? null;
|
|
482
|
+
onFileRead(previousValue);
|
|
483
|
+
patchLive(value);
|
|
458
484
|
|
|
459
|
-
|
|
460
|
-
|
|
485
|
+
const operation: PatchOperation = { type: "attribute", property: attr, value };
|
|
486
|
+
const patched = applyPatchByTarget(before, patchTarget, operation);
|
|
461
487
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
488
|
+
pendingTimelineEditPathRef.current.add(targetPath);
|
|
489
|
+
try {
|
|
490
|
+
return await writeProjectFilesWithHistoryInQueue({
|
|
491
|
+
projectId,
|
|
492
|
+
label,
|
|
493
|
+
files: { [targetPath]: patched },
|
|
494
|
+
readFile: async (path) => (path === targetPath ? before : readFileContent(projectId, path)),
|
|
495
|
+
writeFile: writeProjectFile,
|
|
496
|
+
recordEdit,
|
|
497
|
+
});
|
|
498
|
+
} catch (error) {
|
|
499
|
+
// The optimistic live write already ran; unwind it on a save failure so
|
|
500
|
+
// the preview doesn't show a value that never reached disk.
|
|
501
|
+
patchLive(previousValue);
|
|
502
|
+
throw error;
|
|
503
|
+
}
|
|
504
|
+
});
|
|
479
505
|
}
|
|
@@ -7,17 +7,25 @@
|
|
|
7
7
|
* shared core `setAudioGroupAttribute` also uses.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { useCallback
|
|
10
|
+
import { useCallback } from "react";
|
|
11
11
|
import type { TimelineElement } from "../player";
|
|
12
12
|
import {
|
|
13
13
|
buildPatchTarget,
|
|
14
14
|
findTimelineElementInIframe,
|
|
15
15
|
persistElementAttribute,
|
|
16
|
+
readSavedAttribute,
|
|
16
17
|
} from "./timelineEditingHelpers";
|
|
18
|
+
import { useLiveLanes, type LiveLaneRestore, type LiveLaneSource } from "./liveLanes";
|
|
17
19
|
import type {
|
|
18
20
|
MutableRef,
|
|
19
21
|
UseTimelineElementVisibilityEditingInput,
|
|
20
22
|
} from "./timelineTrackVisibility";
|
|
23
|
+
import {
|
|
24
|
+
failedTimelineSave,
|
|
25
|
+
projectForTimelineSave,
|
|
26
|
+
type TimelineEditOutcome,
|
|
27
|
+
} from "./timelineEditPermission";
|
|
28
|
+
import { syncStoredElementAttribute } from "../player/lib/automationStoreSync";
|
|
21
29
|
|
|
22
30
|
function patchLiveElementAttribute(
|
|
23
31
|
iframe: HTMLIFrameElement | null,
|
|
@@ -40,6 +48,22 @@ function elementAttributeLiveKey(
|
|
|
40
48
|
return `${element.sourceFile || activeCompPath || "index.html"}\0${element.key ?? element.domId ?? element.id}\0${attr}`;
|
|
41
49
|
}
|
|
42
50
|
|
|
51
|
+
function elementSaveTarget(element: TimelineElement, activeCompPath: string | null) {
|
|
52
|
+
return {
|
|
53
|
+
targetPath: element.sourceFile || activeCompPath || "index.html",
|
|
54
|
+
patchTarget: buildPatchTarget(element),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function elementLiveSource(
|
|
59
|
+
element: TimelineElement,
|
|
60
|
+
activeCompPath: string | null,
|
|
61
|
+
attr: string,
|
|
62
|
+
): LiveLaneSource {
|
|
63
|
+
const { targetPath, patchTarget } = elementSaveTarget(element, activeCompPath);
|
|
64
|
+
return { path: targetPath, target: patchTarget, attr };
|
|
65
|
+
}
|
|
66
|
+
|
|
43
67
|
interface SetElementAttributeInput {
|
|
44
68
|
projectId: string;
|
|
45
69
|
activeCompPath: string | null;
|
|
@@ -47,7 +71,8 @@ interface SetElementAttributeInput {
|
|
|
47
71
|
attr: string;
|
|
48
72
|
value: string | null;
|
|
49
73
|
label: string;
|
|
50
|
-
|
|
74
|
+
patchLive: (value: string | null) => void;
|
|
75
|
+
onFileRead: (value: string | null) => void;
|
|
51
76
|
writeProjectFile: (path: string, content: string) => Promise<void>;
|
|
52
77
|
recordEdit: Parameters<typeof persistElementAttribute>[0]["recordEdit"];
|
|
53
78
|
pendingTimelineEditPathRef: MutableRef<Set<string>>;
|
|
@@ -60,14 +85,14 @@ async function setElementAttribute({
|
|
|
60
85
|
attr,
|
|
61
86
|
value,
|
|
62
87
|
label,
|
|
63
|
-
|
|
88
|
+
patchLive,
|
|
89
|
+
onFileRead,
|
|
64
90
|
writeProjectFile,
|
|
65
91
|
recordEdit,
|
|
66
92
|
pendingTimelineEditPathRef,
|
|
67
|
-
}: SetElementAttributeInput): Promise<string[]> {
|
|
68
|
-
const targetPath = element
|
|
69
|
-
|
|
70
|
-
if (!patchTarget) return [];
|
|
93
|
+
}: SetElementAttributeInput): Promise<string[] | null> {
|
|
94
|
+
const { targetPath, patchTarget } = elementSaveTarget(element, activeCompPath);
|
|
95
|
+
if (!patchTarget) return null;
|
|
71
96
|
|
|
72
97
|
return persistElementAttribute({
|
|
73
98
|
projectId,
|
|
@@ -79,7 +104,8 @@ async function setElementAttribute({
|
|
|
79
104
|
writeProjectFile,
|
|
80
105
|
recordEdit,
|
|
81
106
|
pendingTimelineEditPathRef,
|
|
82
|
-
patchLive
|
|
107
|
+
patchLive,
|
|
108
|
+
onFileRead,
|
|
83
109
|
});
|
|
84
110
|
}
|
|
85
111
|
|
|
@@ -99,68 +125,94 @@ export function useSetElementAttribute({
|
|
|
99
125
|
attr: string,
|
|
100
126
|
value: string | null,
|
|
101
127
|
label: string,
|
|
102
|
-
) => Promise<
|
|
128
|
+
) => Promise<TimelineEditOutcome>;
|
|
103
129
|
revertLive: (element: TimelineElement, attr: string) => void;
|
|
130
|
+
restoreLive: (restore: LiveLaneRestore) => void;
|
|
104
131
|
} {
|
|
105
|
-
const
|
|
132
|
+
const liveLanes = useLiveLanes(projectIdRef, activeCompPath);
|
|
106
133
|
const setLive = useCallback(
|
|
107
134
|
(element: TimelineElement, attr: string, value: string | null) => {
|
|
108
135
|
const key = elementAttributeLiveKey(element, activeCompPath, attr);
|
|
109
136
|
const target = findTimelineElementInIframe(previewIframeRef.current, element, activeCompPath);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
137
|
+
liveLanes.preview(
|
|
138
|
+
key,
|
|
139
|
+
() => target?.getAttribute(attr) ?? null,
|
|
140
|
+
elementLiveSource(element, activeCompPath, attr),
|
|
141
|
+
);
|
|
113
142
|
patchLiveElementAttribute(previewIframeRef.current, element, attr, value, activeCompPath);
|
|
114
143
|
},
|
|
144
|
+
[liveLanes, previewIframeRef, activeCompPath],
|
|
145
|
+
);
|
|
146
|
+
const laneApply = useCallback(
|
|
147
|
+
(element: TimelineElement, attr: string) => ({
|
|
148
|
+
preview: (value: string | null) =>
|
|
149
|
+
patchLiveElementAttribute(previewIframeRef.current, element, attr, value, activeCompPath),
|
|
150
|
+
store: (value: string | null) => syncStoredElementAttribute(element, attr, value),
|
|
151
|
+
}),
|
|
115
152
|
[previewIframeRef, activeCompPath],
|
|
116
153
|
);
|
|
154
|
+
const claimLive = useCallback(
|
|
155
|
+
(element: TimelineElement, attr: string) =>
|
|
156
|
+
liveLanes.claim(
|
|
157
|
+
elementAttributeLiveKey(element, activeCompPath, attr),
|
|
158
|
+
laneApply(element, attr),
|
|
159
|
+
elementLiveSource(element, activeCompPath, attr),
|
|
160
|
+
),
|
|
161
|
+
[liveLanes, laneApply, activeCompPath],
|
|
162
|
+
);
|
|
117
163
|
const revertLive = useCallback(
|
|
118
|
-
(element: TimelineElement, attr: string) =>
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
attr,
|
|
125
|
-
liveBeforeRef.current.get(key) ?? null,
|
|
126
|
-
activeCompPath,
|
|
127
|
-
);
|
|
128
|
-
liveBeforeRef.current.delete(key);
|
|
129
|
-
},
|
|
130
|
-
[previewIframeRef, activeCompPath],
|
|
164
|
+
(element: TimelineElement, attr: string) =>
|
|
165
|
+
liveLanes.revert(
|
|
166
|
+
elementAttributeLiveKey(element, activeCompPath, attr),
|
|
167
|
+
laneApply(element, attr),
|
|
168
|
+
),
|
|
169
|
+
[liveLanes, laneApply, activeCompPath],
|
|
131
170
|
);
|
|
132
171
|
const setQuiet = useCallback(
|
|
133
|
-
async (
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
172
|
+
async (
|
|
173
|
+
element: TimelineElement,
|
|
174
|
+
attr: string,
|
|
175
|
+
value: string | null,
|
|
176
|
+
label: string,
|
|
177
|
+
): Promise<TimelineEditOutcome> => {
|
|
178
|
+
const project = projectIdRef.current;
|
|
179
|
+
const pid = projectForTimelineSave(isRecordingRef?.current, project, showToast);
|
|
180
|
+
const live = claimLive(element, attr);
|
|
181
|
+
const unsaved = async (outcome: TimelineEditOutcome): Promise<TimelineEditOutcome> => {
|
|
182
|
+
const { targetPath, patchTarget } = elementSaveTarget(element, activeCompPath);
|
|
183
|
+
live.settle(
|
|
184
|
+
await readSavedAttribute(project, targetPath, patchTarget, attr, writeProjectFile),
|
|
185
|
+
);
|
|
186
|
+
return outcome;
|
|
187
|
+
};
|
|
188
|
+
if (typeof pid !== "string") return unsaved(pid);
|
|
140
189
|
try {
|
|
141
|
-
await setElementAttribute({
|
|
190
|
+
const written = await setElementAttribute({
|
|
142
191
|
projectId: pid,
|
|
143
192
|
activeCompPath,
|
|
144
193
|
element,
|
|
145
194
|
attr,
|
|
146
195
|
value,
|
|
147
196
|
label,
|
|
148
|
-
|
|
197
|
+
patchLive: live.preview,
|
|
198
|
+
onFileRead: live.read,
|
|
149
199
|
writeProjectFile,
|
|
150
200
|
recordEdit,
|
|
151
201
|
pendingTimelineEditPathRef,
|
|
152
202
|
});
|
|
153
|
-
|
|
203
|
+
if (!written)
|
|
204
|
+
return unsaved(failedTimelineSave("This clip has no id to save it by", showToast));
|
|
205
|
+
live.settle(value);
|
|
206
|
+
return { status: "saved" };
|
|
154
207
|
} catch (error) {
|
|
155
208
|
console.error("[Timeline] Failed to set element attribute", error);
|
|
156
209
|
const message = error instanceof Error ? error.message : "Failed to update effect";
|
|
157
|
-
|
|
158
|
-
liveBeforeRef.current.delete(elementAttributeLiveKey(element, activeCompPath, attr));
|
|
210
|
+
return unsaved(failedTimelineSave(message, showToast));
|
|
159
211
|
}
|
|
160
212
|
},
|
|
161
213
|
[
|
|
214
|
+
claimLive,
|
|
162
215
|
activeCompPath,
|
|
163
|
-
previewIframeRef,
|
|
164
216
|
writeProjectFile,
|
|
165
217
|
recordEdit,
|
|
166
218
|
pendingTimelineEditPathRef,
|
|
@@ -169,5 +221,5 @@ export function useSetElementAttribute({
|
|
|
169
221
|
projectIdRef,
|
|
170
222
|
],
|
|
171
223
|
);
|
|
172
|
-
return { setLive, setQuiet, revertLive };
|
|
224
|
+
return { setLive, setQuiet, revertLive, restoreLive: liveLanes.restore };
|
|
173
225
|
}
|
|
@@ -93,7 +93,7 @@ interface UseAppHotkeysParams {
|
|
|
93
93
|
handleDuplicate: () => Promise<boolean>;
|
|
94
94
|
onResetKeyframes: () => boolean;
|
|
95
95
|
onDeleteSelectedKeyframes: () => void;
|
|
96
|
-
onAfterUndoRedo?:
|
|
96
|
+
onAfterUndoRedo?: UseEditHistoryActionsOptions["onAfterUndoRedo"];
|
|
97
97
|
onToggleRecording?: () => void;
|
|
98
98
|
/** Group the current multi-selection into a data-hf-group wrapper (⌘G). */
|
|
99
99
|
onGroupSelection?: () => void;
|