@hyperframes/studio 0.6.27 → 0.6.28
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-EdfhuQ5T.js +362 -0
- package/dist/index.html +1 -1
- package/package.json +5 -4
- package/src/App.tsx +13 -13
- package/src/hooks/{useManifestPersistence.ts → usePreviewPersistence.ts} +13 -6
- package/src/hooks/useTimelineEditing.ts +170 -99
- package/src/player/components/TimelineCanvas.tsx +2 -0
- package/src/player/components/TimelineClip.tsx +4 -2
- package/src/player/components/timelineEditing.test.ts +54 -42
- package/src/player/components/timelineEditing.ts +2 -14
- package/src/player/hooks/useTimelinePlayer.ts +28 -1
- package/src/player/lib/mediaProbe.ts +68 -0
- package/dist/assets/index-DYjmgXgg.js +0 -115
package/dist/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
7
7
|
<title>HyperFrames Studio</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-EdfhuQ5T.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-DVpLGNHi.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.28",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -30,8 +30,9 @@
|
|
|
30
30
|
"@codemirror/theme-one-dark": "^6.1.2",
|
|
31
31
|
"@codemirror/view": "6.40.0",
|
|
32
32
|
"@phosphor-icons/react": "^2.1.10",
|
|
33
|
-
"
|
|
34
|
-
"@hyperframes/
|
|
33
|
+
"mediabunny": "^1.45.3",
|
|
34
|
+
"@hyperframes/core": "0.6.28",
|
|
35
|
+
"@hyperframes/player": "0.6.28"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
37
38
|
"@types/react": "19",
|
|
@@ -45,7 +46,7 @@
|
|
|
45
46
|
"vite": "^6.4.2",
|
|
46
47
|
"vitest": "^3.2.4",
|
|
47
48
|
"zustand": "^5.0.0",
|
|
48
|
-
"@hyperframes/producer": "0.6.
|
|
49
|
+
"@hyperframes/producer": "0.6.28"
|
|
49
50
|
},
|
|
50
51
|
"peerDependencies": {
|
|
51
52
|
"react": "19",
|
package/src/App.tsx
CHANGED
|
@@ -8,7 +8,7 @@ import { useCaptionSync } from "./captions/hooks/useCaptionSync";
|
|
|
8
8
|
import { usePersistentEditHistory } from "./hooks/usePersistentEditHistory";
|
|
9
9
|
import { usePanelLayout } from "./hooks/usePanelLayout";
|
|
10
10
|
import { useFileManager } from "./hooks/useFileManager";
|
|
11
|
-
import {
|
|
11
|
+
import { usePreviewPersistence } from "./hooks/usePreviewPersistence";
|
|
12
12
|
import { useTimelineEditing } from "./hooks/useTimelineEditing";
|
|
13
13
|
import { addBlockToProject } from "./utils/blockInstaller";
|
|
14
14
|
import type { BlockParam } from "@hyperframes/core/registry";
|
|
@@ -117,12 +117,9 @@ export function StudioApp() {
|
|
|
117
117
|
});
|
|
118
118
|
const editHistory = usePersistentEditHistory({ projectId });
|
|
119
119
|
const domEditSaveTimestampRef = useRef(0);
|
|
120
|
+
const pendingTimelineEditPathRef = useRef(new Set<string>());
|
|
120
121
|
const reloadPreview = useCallback(() => {
|
|
121
|
-
|
|
122
|
-
previewIframeRef.current?.contentWindow?.location.reload();
|
|
123
|
-
} catch {
|
|
124
|
-
setRefreshKey((k) => k + 1);
|
|
125
|
-
}
|
|
122
|
+
setRefreshKey((k) => k + 1);
|
|
126
123
|
}, []);
|
|
127
124
|
|
|
128
125
|
const fileManager = useFileManager({
|
|
@@ -145,7 +142,7 @@ export function StudioApp() {
|
|
|
145
142
|
setActiveCompPathHydrated(true);
|
|
146
143
|
}, [activeCompPathHydrated, fileManager.fileTree, fileManager.fileTreeLoaded]);
|
|
147
144
|
|
|
148
|
-
const
|
|
145
|
+
const previewPersistence = usePreviewPersistence({
|
|
149
146
|
projectId,
|
|
150
147
|
showToast,
|
|
151
148
|
readOptionalProjectFile: fileManager.readOptionalProjectFile,
|
|
@@ -155,6 +152,7 @@ export function StudioApp() {
|
|
|
155
152
|
activeCompPathRef,
|
|
156
153
|
domEditSaveTimestampRef,
|
|
157
154
|
reloadPreview: () => setRefreshKey((k) => k + 1),
|
|
155
|
+
pendingTimelineEditPathRef,
|
|
158
156
|
});
|
|
159
157
|
|
|
160
158
|
const timelineEditing = useTimelineEditing({
|
|
@@ -166,6 +164,8 @@ export function StudioApp() {
|
|
|
166
164
|
recordEdit: editHistory.recordEdit,
|
|
167
165
|
domEditSaveTimestampRef,
|
|
168
166
|
reloadPreview,
|
|
167
|
+
previewIframeRef,
|
|
168
|
+
pendingTimelineEditPathRef,
|
|
169
169
|
uploadProjectFiles: fileManager.uploadProjectFiles,
|
|
170
170
|
});
|
|
171
171
|
|
|
@@ -274,8 +274,8 @@ export function StudioApp() {
|
|
|
274
274
|
writeProjectFile: fileManager.writeProjectFile,
|
|
275
275
|
domEditSaveTimestampRef,
|
|
276
276
|
showToast,
|
|
277
|
-
syncHistoryPreviewAfterApply:
|
|
278
|
-
waitForPendingDomEditSaves:
|
|
277
|
+
syncHistoryPreviewAfterApply: previewPersistence.syncHistoryPreviewAfterApply,
|
|
278
|
+
waitForPendingDomEditSaves: previewPersistence.waitForPendingDomEditSaves,
|
|
279
279
|
leftSidebarRef,
|
|
280
280
|
handleCopy,
|
|
281
281
|
handlePaste,
|
|
@@ -297,7 +297,7 @@ export function StudioApp() {
|
|
|
297
297
|
setRightPanelTab: panelLayout.setRightPanelTab,
|
|
298
298
|
showToast,
|
|
299
299
|
refreshPreviewDocumentVersion,
|
|
300
|
-
queueDomEditSave:
|
|
300
|
+
queueDomEditSave: previewPersistence.queueDomEditSave,
|
|
301
301
|
readProjectFile: fileManager.readProjectFile,
|
|
302
302
|
writeProjectFile: fileManager.writeProjectFile,
|
|
303
303
|
domEditSaveTimestampRef,
|
|
@@ -309,7 +309,7 @@ export function StudioApp() {
|
|
|
309
309
|
previewIframe,
|
|
310
310
|
refreshKey,
|
|
311
311
|
rightPanelTab: panelLayout.rightPanelTab,
|
|
312
|
-
applyStudioManualEditsToPreviewRef:
|
|
312
|
+
applyStudioManualEditsToPreviewRef: previewPersistence.applyStudioManualEditsToPreviewRef,
|
|
313
313
|
syncPreviewHistoryHotkey: appHotkeys.syncPreviewHistoryHotkey,
|
|
314
314
|
reloadPreview,
|
|
315
315
|
setRefreshKey,
|
|
@@ -345,7 +345,7 @@ export function StudioApp() {
|
|
|
345
345
|
projectId,
|
|
346
346
|
activeCompPath,
|
|
347
347
|
showToast,
|
|
348
|
-
waitForPendingDomEditSaves:
|
|
348
|
+
waitForPendingDomEditSaves: previewPersistence.waitForPendingDomEditSaves,
|
|
349
349
|
});
|
|
350
350
|
const {
|
|
351
351
|
consoleErrors,
|
|
@@ -453,7 +453,7 @@ export function StudioApp() {
|
|
|
453
453
|
startRender: renderQueue.startRender as (options: unknown) => Promise<void>,
|
|
454
454
|
},
|
|
455
455
|
compositionDimensions,
|
|
456
|
-
waitForPendingDomEditSaves:
|
|
456
|
+
waitForPendingDomEditSaves: previewPersistence.waitForPendingDomEditSaves,
|
|
457
457
|
handlePreviewIframeRef,
|
|
458
458
|
refreshPreviewDocumentVersion,
|
|
459
459
|
timelineVisible,
|
|
@@ -17,7 +17,7 @@ interface RecordEditInput {
|
|
|
17
17
|
files: Record<string, { before: string; after: string }>;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
interface
|
|
20
|
+
interface UsePreviewPersistenceParams {
|
|
21
21
|
projectId: string | null;
|
|
22
22
|
showToast: (message: string, tone?: "error" | "info") => void;
|
|
23
23
|
readOptionalProjectFile: (path: string) => Promise<string>;
|
|
@@ -26,15 +26,18 @@ interface UseManifestPersistenceParams {
|
|
|
26
26
|
previewIframeRef: React.MutableRefObject<HTMLIFrameElement | null>;
|
|
27
27
|
activeCompPathRef: React.MutableRefObject<string | null>;
|
|
28
28
|
/** Shared timestamp ref — written by any studio save (code tab, timeline, DOM edits).
|
|
29
|
-
* Used to suppress
|
|
29
|
+
* Used to suppress file-change echoes so we don't reload after our own saves. */
|
|
30
30
|
domEditSaveTimestampRef: React.MutableRefObject<number>;
|
|
31
|
+
/** Tracks in-flight timeline edits that patch the iframe DOM directly. File-change
|
|
32
|
+
* events for these paths are always suppressed since the preview is already up-to-date. */
|
|
33
|
+
pendingTimelineEditPathRef?: React.MutableRefObject<Set<string>>;
|
|
31
34
|
/** Called to reload the preview after undo/redo or external file changes. */
|
|
32
35
|
reloadPreview: () => void;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
// ── Hook ──
|
|
36
39
|
|
|
37
|
-
export function
|
|
40
|
+
export function usePreviewPersistence({
|
|
38
41
|
projectId,
|
|
39
42
|
showToast: _showToast,
|
|
40
43
|
readOptionalProjectFile: _readOptionalProjectFile,
|
|
@@ -44,7 +47,8 @@ export function useManifestPersistence({
|
|
|
44
47
|
activeCompPathRef: _activeCompPathRef,
|
|
45
48
|
domEditSaveTimestampRef,
|
|
46
49
|
reloadPreview,
|
|
47
|
-
|
|
50
|
+
pendingTimelineEditPathRef,
|
|
51
|
+
}: UsePreviewPersistenceParams) {
|
|
48
52
|
void _showToast;
|
|
49
53
|
void _recordEdit;
|
|
50
54
|
void _activeCompPathRef;
|
|
@@ -162,8 +166,11 @@ export function useManifestPersistence({
|
|
|
162
166
|
const handler = (payload?: unknown) => {
|
|
163
167
|
const changedPath = readStudioFileChangePath(payload);
|
|
164
168
|
if (!changedPath) return;
|
|
165
|
-
const recentDomEditSave = Date.now() - domEditSaveTimestampRef.current <
|
|
166
|
-
|
|
169
|
+
const recentDomEditSave = Date.now() - domEditSaveTimestampRef.current < 4000;
|
|
170
|
+
if (pendingTimelineEditPathRef?.current.has(changedPath)) {
|
|
171
|
+
pendingTimelineEditPathRef.current.delete(changedPath);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
167
174
|
if (!recentDomEditSave) {
|
|
168
175
|
reloadPreview();
|
|
169
176
|
}
|
|
@@ -38,6 +38,8 @@ interface UseTimelineEditingOptions {
|
|
|
38
38
|
recordEdit: (input: RecordEditInput) => Promise<void>;
|
|
39
39
|
domEditSaveTimestampRef: React.MutableRefObject<number>;
|
|
40
40
|
reloadPreview: () => void;
|
|
41
|
+
previewIframeRef: React.RefObject<HTMLIFrameElement | null>;
|
|
42
|
+
pendingTimelineEditPathRef: React.MutableRefObject<Set<string>>;
|
|
41
43
|
uploadProjectFiles: (files: Iterable<File>, dir?: string) => Promise<string[]>;
|
|
42
44
|
}
|
|
43
45
|
|
|
@@ -53,6 +55,97 @@ function buildPatchTarget(element: { domId?: string; selector?: string; selector
|
|
|
53
55
|
return null;
|
|
54
56
|
}
|
|
55
57
|
|
|
58
|
+
// The runtime re-reads data-start/data-duration from the DOM on each sync tick
|
|
59
|
+
// (packages/core/src/runtime/init.ts:1324-1368), so attribute mutations here are
|
|
60
|
+
// picked up automatically on the next frame without a rebind call.
|
|
61
|
+
function patchIframeDomTiming(
|
|
62
|
+
iframe: HTMLIFrameElement | null,
|
|
63
|
+
element: TimelineElement,
|
|
64
|
+
attrs: Array<[string, string]>,
|
|
65
|
+
): void {
|
|
66
|
+
try {
|
|
67
|
+
const doc = iframe?.contentDocument;
|
|
68
|
+
if (!doc) return;
|
|
69
|
+
const el = element.domId
|
|
70
|
+
? doc.getElementById(element.domId)
|
|
71
|
+
: element.selector
|
|
72
|
+
? (doc.querySelectorAll(element.selector)[element.selectorIndex ?? 0] ?? null)
|
|
73
|
+
: null;
|
|
74
|
+
if (!el) return;
|
|
75
|
+
for (const [name, value] of attrs) el.setAttribute(name, value);
|
|
76
|
+
} catch {
|
|
77
|
+
// Cross-origin or mid-navigation — file save is enqueued; iframe patch is best-effort.
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolveResizePlaybackStart(
|
|
82
|
+
original: string,
|
|
83
|
+
target: PatchTarget,
|
|
84
|
+
element: TimelineElement,
|
|
85
|
+
updates: Pick<TimelineElement, "start" | "playbackStart">,
|
|
86
|
+
): { attrName: string; value: number } | null {
|
|
87
|
+
if (updates.playbackStart != null) {
|
|
88
|
+
const attrName =
|
|
89
|
+
element.playbackStartAttr === "playback-start" ? "playback-start" : "media-start";
|
|
90
|
+
return { attrName, value: updates.playbackStart };
|
|
91
|
+
}
|
|
92
|
+
const trimDelta = updates.start - element.start;
|
|
93
|
+
if (trimDelta === 0) return null;
|
|
94
|
+
const raw =
|
|
95
|
+
readAttributeByTarget(original, target, "playback-start") ??
|
|
96
|
+
readAttributeByTarget(original, target, "media-start");
|
|
97
|
+
const current = raw != null ? parseFloat(raw) : undefined;
|
|
98
|
+
if (current == null || !Number.isFinite(current)) return null;
|
|
99
|
+
const attrName =
|
|
100
|
+
element.playbackStartAttr === "playback-start" ? "playback-start" : "media-start";
|
|
101
|
+
return {
|
|
102
|
+
attrName,
|
|
103
|
+
value: Math.max(0, current + trimDelta * Math.max(element.playbackRate ?? 1, 0.1)),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
type PatchTarget = NonNullable<ReturnType<typeof buildPatchTarget>>;
|
|
108
|
+
|
|
109
|
+
interface PersistTimelineEditInput {
|
|
110
|
+
projectId: string;
|
|
111
|
+
element: TimelineElement;
|
|
112
|
+
activeCompPath: string | null;
|
|
113
|
+
label: string;
|
|
114
|
+
buildPatches: (original: string, target: PatchTarget) => string;
|
|
115
|
+
writeProjectFile: (path: string, content: string) => Promise<void>;
|
|
116
|
+
recordEdit: (input: RecordEditInput) => Promise<void>;
|
|
117
|
+
domEditSaveTimestampRef: React.MutableRefObject<number>;
|
|
118
|
+
pendingTimelineEditPathRef: React.MutableRefObject<Set<string>>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function persistTimelineEdit(input: PersistTimelineEditInput): Promise<void> {
|
|
122
|
+
const targetPath = input.element.sourceFile || input.activeCompPath || "index.html";
|
|
123
|
+
const originalContent = await readFileContent(input.projectId, targetPath);
|
|
124
|
+
|
|
125
|
+
const patchTarget = buildPatchTarget(input.element);
|
|
126
|
+
if (!patchTarget) {
|
|
127
|
+
throw new Error(`Timeline element ${input.element.id} is missing a patchable target`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const patchedContent = input.buildPatches(originalContent, patchTarget);
|
|
131
|
+
if (patchedContent === originalContent) {
|
|
132
|
+
throw new Error(`Unable to patch timeline element ${input.element.id} in ${targetPath}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
input.pendingTimelineEditPathRef.current.add(targetPath);
|
|
136
|
+
input.domEditSaveTimestampRef.current = Date.now();
|
|
137
|
+
await saveProjectFilesWithHistory({
|
|
138
|
+
projectId: input.projectId,
|
|
139
|
+
label: input.label,
|
|
140
|
+
kind: "timeline",
|
|
141
|
+
files: { [targetPath]: patchedContent },
|
|
142
|
+
readFile: async () => originalContent,
|
|
143
|
+
writeFile: input.writeProjectFile,
|
|
144
|
+
recordEdit: input.recordEdit,
|
|
145
|
+
});
|
|
146
|
+
input.domEditSaveTimestampRef.current = Date.now();
|
|
147
|
+
}
|
|
148
|
+
|
|
56
149
|
async function readFileContent(projectId: string, targetPath: string): Promise<string> {
|
|
57
150
|
const response = await fetch(
|
|
58
151
|
`/api/projects/${projectId}/files/${encodeURIComponent(targetPath)}`,
|
|
@@ -78,127 +171,105 @@ export function useTimelineEditing({
|
|
|
78
171
|
recordEdit,
|
|
79
172
|
domEditSaveTimestampRef,
|
|
80
173
|
reloadPreview,
|
|
174
|
+
previewIframeRef,
|
|
175
|
+
pendingTimelineEditPathRef,
|
|
81
176
|
uploadProjectFiles,
|
|
82
177
|
}: UseTimelineEditingOptions) {
|
|
83
178
|
const projectIdRef = useRef(projectId);
|
|
84
179
|
projectIdRef.current = projectId;
|
|
85
180
|
|
|
181
|
+
const editQueueRef = useRef(Promise.resolve());
|
|
86
182
|
const lastBlockedTimelineToastAtRef = useRef(0);
|
|
87
183
|
|
|
88
|
-
const
|
|
89
|
-
|
|
184
|
+
const enqueueEdit = useCallback(
|
|
185
|
+
(
|
|
186
|
+
element: TimelineElement,
|
|
187
|
+
label: string,
|
|
188
|
+
buildPatches: PersistTimelineEditInput["buildPatches"],
|
|
189
|
+
): Promise<void> => {
|
|
90
190
|
const pid = projectIdRef.current;
|
|
91
|
-
if (!pid)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
type: "attribute",
|
|
108
|
-
property: "track-index",
|
|
109
|
-
value: String(updates.track),
|
|
191
|
+
if (!pid) return Promise.resolve();
|
|
192
|
+
const queued = editQueueRef.current.then(() =>
|
|
193
|
+
persistTimelineEdit({
|
|
194
|
+
projectId: pid,
|
|
195
|
+
element,
|
|
196
|
+
activeCompPath,
|
|
197
|
+
label,
|
|
198
|
+
buildPatches,
|
|
199
|
+
writeProjectFile,
|
|
200
|
+
recordEdit,
|
|
201
|
+
domEditSaveTimestampRef,
|
|
202
|
+
pendingTimelineEditPathRef,
|
|
203
|
+
}),
|
|
204
|
+
);
|
|
205
|
+
editQueueRef.current = queued.catch((error) => {
|
|
206
|
+
console.error(`[Timeline] Failed to persist: ${label}`, error);
|
|
110
207
|
});
|
|
208
|
+
return queued;
|
|
209
|
+
},
|
|
210
|
+
[
|
|
211
|
+
activeCompPath,
|
|
212
|
+
recordEdit,
|
|
213
|
+
writeProjectFile,
|
|
214
|
+
domEditSaveTimestampRef,
|
|
215
|
+
pendingTimelineEditPathRef,
|
|
216
|
+
],
|
|
217
|
+
);
|
|
111
218
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
219
|
+
const handleTimelineElementMove = useCallback(
|
|
220
|
+
(element: TimelineElement, updates: Pick<TimelineElement, "start" | "track">) => {
|
|
221
|
+
patchIframeDomTiming(previewIframeRef.current, element, [
|
|
222
|
+
["data-start", formatTimelineAttributeNumber(updates.start)],
|
|
223
|
+
["data-track-index", String(updates.track)],
|
|
224
|
+
]);
|
|
225
|
+
return enqueueEdit(element, "Move timeline clip", (original, target) => {
|
|
226
|
+
let patched = applyPatchByTarget(original, target, {
|
|
227
|
+
type: "attribute",
|
|
228
|
+
property: "start",
|
|
229
|
+
value: formatTimelineAttributeNumber(updates.start),
|
|
230
|
+
});
|
|
231
|
+
return applyPatchByTarget(patched, target, {
|
|
232
|
+
type: "attribute",
|
|
233
|
+
property: "track-index",
|
|
234
|
+
value: String(updates.track),
|
|
235
|
+
});
|
|
125
236
|
});
|
|
126
|
-
|
|
127
|
-
reloadPreview();
|
|
128
237
|
},
|
|
129
|
-
[
|
|
238
|
+
[previewIframeRef, enqueueEdit],
|
|
130
239
|
);
|
|
131
240
|
|
|
132
241
|
const handleTimelineElementResize = useCallback(
|
|
133
|
-
|
|
242
|
+
(
|
|
134
243
|
element: TimelineElement,
|
|
135
244
|
updates: Pick<TimelineElement, "start" | "duration" | "playbackStart">,
|
|
136
245
|
) => {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (!patchTarget) {
|
|
145
|
-
throw new Error(`Timeline element ${element.id} is missing a patchable target`);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const playbackStartAttrName =
|
|
149
|
-
element.playbackStartAttr === "playback-start" ? "playback-start" : "media-start";
|
|
150
|
-
const currentPlaybackStartValue =
|
|
151
|
-
readAttributeByTarget(originalContent, patchTarget, "playback-start") ??
|
|
152
|
-
readAttributeByTarget(originalContent, patchTarget, "media-start");
|
|
153
|
-
const currentPlaybackStart =
|
|
154
|
-
currentPlaybackStartValue != null ? parseFloat(currentPlaybackStartValue) : undefined;
|
|
155
|
-
const trimDelta = updates.start - element.start;
|
|
156
|
-
const fallbackPlaybackStart =
|
|
157
|
-
updates.playbackStart == null &&
|
|
158
|
-
trimDelta !== 0 &&
|
|
159
|
-
Number.isFinite(currentPlaybackStart) &&
|
|
160
|
-
currentPlaybackStart != null
|
|
161
|
-
? Math.max(0, currentPlaybackStart + trimDelta * Math.max(element.playbackRate ?? 1, 0.1))
|
|
162
|
-
: undefined;
|
|
163
|
-
const nextPlaybackStart = updates.playbackStart ?? fallbackPlaybackStart;
|
|
164
|
-
|
|
165
|
-
let patchedContent = originalContent;
|
|
166
|
-
patchedContent = applyPatchByTarget(patchedContent, patchTarget, {
|
|
167
|
-
type: "attribute",
|
|
168
|
-
property: "start",
|
|
169
|
-
value: formatTimelineAttributeNumber(updates.start),
|
|
170
|
-
});
|
|
171
|
-
patchedContent = applyPatchByTarget(patchedContent, patchTarget, {
|
|
172
|
-
type: "attribute",
|
|
173
|
-
property: "duration",
|
|
174
|
-
value: formatTimelineAttributeNumber(updates.duration),
|
|
175
|
-
});
|
|
176
|
-
if (nextPlaybackStart != null) {
|
|
177
|
-
patchedContent = applyPatchByTarget(patchedContent, patchTarget, {
|
|
246
|
+
patchIframeDomTiming(previewIframeRef.current, element, [
|
|
247
|
+
["data-start", formatTimelineAttributeNumber(updates.start)],
|
|
248
|
+
["data-duration", formatTimelineAttributeNumber(updates.duration)],
|
|
249
|
+
]);
|
|
250
|
+
return enqueueEdit(element, "Resize timeline clip", (original, target) => {
|
|
251
|
+
const pbs = resolveResizePlaybackStart(original, target, element, updates);
|
|
252
|
+
let patched = applyPatchByTarget(original, target, {
|
|
178
253
|
type: "attribute",
|
|
179
|
-
property:
|
|
180
|
-
value: formatTimelineAttributeNumber(
|
|
254
|
+
property: "start",
|
|
255
|
+
value: formatTimelineAttributeNumber(updates.start),
|
|
181
256
|
});
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
writeFile: writeProjectFile,
|
|
196
|
-
recordEdit,
|
|
257
|
+
patched = applyPatchByTarget(patched, target, {
|
|
258
|
+
type: "attribute",
|
|
259
|
+
property: "duration",
|
|
260
|
+
value: formatTimelineAttributeNumber(updates.duration),
|
|
261
|
+
});
|
|
262
|
+
if (pbs) {
|
|
263
|
+
patched = applyPatchByTarget(patched, target, {
|
|
264
|
+
type: "attribute",
|
|
265
|
+
property: pbs.attrName,
|
|
266
|
+
value: formatTimelineAttributeNumber(pbs.value),
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
return patched;
|
|
197
270
|
});
|
|
198
|
-
|
|
199
|
-
reloadPreview();
|
|
200
271
|
},
|
|
201
|
-
[
|
|
272
|
+
[previewIframeRef, enqueueEdit],
|
|
202
273
|
);
|
|
203
274
|
|
|
204
275
|
const handleTimelineElementDelete = useCallback(
|
|
@@ -252,6 +252,7 @@ export const TimelineCanvas = memo(function TimelineCanvas({
|
|
|
252
252
|
isHovered={hoveredClip === clipKey}
|
|
253
253
|
isDragging={false}
|
|
254
254
|
hasCustomContent={!!renderClipContent}
|
|
255
|
+
capabilities={capabilities}
|
|
255
256
|
theme={theme}
|
|
256
257
|
trackStyle={clipStyle}
|
|
257
258
|
isComposition={isComposition}
|
|
@@ -369,6 +370,7 @@ export const TimelineCanvas = memo(function TimelineCanvas({
|
|
|
369
370
|
isHovered={false}
|
|
370
371
|
isDragging={true}
|
|
371
372
|
hasCustomContent={!!renderClipContent}
|
|
373
|
+
capabilities={getTimelineEditCapabilities(activeDraggedElement)}
|
|
372
374
|
theme={theme}
|
|
373
375
|
trackStyle={getTrackStyle(activeDraggedElement.tag)}
|
|
374
376
|
isComposition={!!activeDraggedElement.compositionSrc}
|
|
@@ -3,7 +3,7 @@ import type { TimelineTrackStyle } from "./timelineTheme";
|
|
|
3
3
|
import { memo, type ReactNode } from "react";
|
|
4
4
|
import type { TimelineElement } from "../store/playerStore";
|
|
5
5
|
import { defaultTimelineTheme, getClipHandleOpacity, type TimelineTheme } from "./timelineTheme";
|
|
6
|
-
import {
|
|
6
|
+
import type { TimelineEditCapabilities } from "./timelineEditing";
|
|
7
7
|
|
|
8
8
|
interface TimelineClipProps {
|
|
9
9
|
el: TimelineElement;
|
|
@@ -13,6 +13,7 @@ interface TimelineClipProps {
|
|
|
13
13
|
isHovered: boolean;
|
|
14
14
|
isDragging?: boolean;
|
|
15
15
|
hasCustomContent: boolean;
|
|
16
|
+
capabilities: TimelineEditCapabilities;
|
|
16
17
|
theme?: TimelineTheme;
|
|
17
18
|
trackStyle: TimelineTrackStyle;
|
|
18
19
|
isComposition: boolean;
|
|
@@ -33,6 +34,7 @@ export const TimelineClip = memo(function TimelineClip({
|
|
|
33
34
|
isHovered,
|
|
34
35
|
isDragging = false,
|
|
35
36
|
hasCustomContent,
|
|
37
|
+
capabilities,
|
|
36
38
|
theme = defaultTimelineTheme,
|
|
37
39
|
trackStyle,
|
|
38
40
|
isComposition,
|
|
@@ -47,6 +49,7 @@ export const TimelineClip = memo(function TimelineClip({
|
|
|
47
49
|
const leftPx = el.start * pps;
|
|
48
50
|
const widthPx = Math.max(el.duration * pps, 4);
|
|
49
51
|
const handleOpacity = getClipHandleOpacity({ isHovered, isSelected, isDragging });
|
|
52
|
+
|
|
50
53
|
const borderColor = isSelected
|
|
51
54
|
? theme.clipBorderActive
|
|
52
55
|
: isHovered
|
|
@@ -59,7 +62,6 @@ export const TimelineClip = memo(function TimelineClip({
|
|
|
59
62
|
: isHovered
|
|
60
63
|
? theme.clipShadowHover
|
|
61
64
|
: theme.clipShadow;
|
|
62
|
-
const capabilities = getTimelineEditCapabilities(el);
|
|
63
65
|
const displayLabel = el.label || el.id || el.tag;
|
|
64
66
|
const showHandles = handleOpacity > 0.01;
|
|
65
67
|
|