@remotion/studio 4.0.470 → 4.0.471

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.
Files changed (35) hide show
  1. package/dist/components/ContextMenu.d.ts +7 -2
  2. package/dist/components/ContextMenu.js +50 -10
  3. package/dist/components/Preview.js +2 -1
  4. package/dist/components/SelectedOutlineOverlay.d.ts +31 -0
  5. package/dist/components/SelectedOutlineOverlay.js +100 -35
  6. package/dist/components/Timeline/Timeline.js +76 -1
  7. package/dist/components/Timeline/TimelineDeleteKeybindings.js +15 -0
  8. package/dist/components/Timeline/TimelineDragHandler.js +1 -0
  9. package/dist/components/Timeline/TimelineEffectItem.js +157 -4
  10. package/dist/components/Timeline/TimelineKeyframeControls.js +16 -11
  11. package/dist/components/Timeline/TimelineRowChrome.d.ts +3 -0
  12. package/dist/components/Timeline/TimelineRowChrome.js +3 -3
  13. package/dist/components/Timeline/TimelineSequenceItem.js +91 -1
  14. package/dist/components/Timeline/call-delete-keyframe.d.ts +16 -0
  15. package/dist/components/Timeline/call-delete-keyframe.js +86 -14
  16. package/dist/components/Timeline/delete-selected-keyframe.d.ts +10 -0
  17. package/dist/components/Timeline/delete-selected-keyframe.js +48 -7
  18. package/dist/components/Timeline/delete-selected-timeline-item.js +6 -11
  19. package/dist/components/Timeline/reset-selected-timeline-props.d.ts +38 -0
  20. package/dist/components/Timeline/reset-selected-timeline-props.js +143 -0
  21. package/dist/components/Timeline/sequence-props-subscription-store.d.ts +3 -2
  22. package/dist/components/Timeline/sequence-props-subscription-store.js +2 -1
  23. package/dist/components/Timeline/timeline-scroll-logic.js +3 -3
  24. package/dist/components/Timeline/use-sequence-props-subscription.js +2 -1
  25. package/dist/esm/{chunk-dny42qnq.js → chunk-z0z9d4r0.js} +1704 -962
  26. package/dist/esm/internals.mjs +1704 -962
  27. package/dist/esm/previewEntry.mjs +1711 -967
  28. package/dist/esm/renderEntry.mjs +1 -1
  29. package/dist/helpers/get-left-of-timeline-slider.js +1 -1
  30. package/dist/helpers/get-timeline-sequence-layout.js +10 -11
  31. package/dist/helpers/open-in-editor.d.ts +19 -1
  32. package/dist/helpers/open-in-editor.js +42 -4
  33. package/dist/helpers/use-menu-structure.js +0 -1
  34. package/dist/state/z-index.js +5 -2
  35. package/package.json +10 -10
@@ -14,11 +14,55 @@ const TimelineExpandArrowButton_1 = require("./TimelineExpandArrowButton");
14
14
  const TimelineLayerEye_1 = require("./TimelineLayerEye");
15
15
  const TimelineRowChrome_1 = require("./TimelineRowChrome");
16
16
  const TimelineSelection_1 = require("./TimelineSelection");
17
+ const EFFECT_REORDER_MIME_TYPE = 'application/remotion-effect-reorder';
18
+ let currentEffectDrag = null;
17
19
  const rowLabel = {
18
20
  fontSize: 12,
19
21
  color: 'rgba(255, 255, 255, 0.8)',
20
22
  userSelect: 'none',
21
23
  };
24
+ const rowStyle = {
25
+ height: timeline_layout_1.TREE_GROUP_ROW_HEIGHT,
26
+ cursor: 'default',
27
+ };
28
+ const reorderWrapper = {
29
+ position: 'relative',
30
+ };
31
+ const reorderLineBase = {
32
+ backgroundColor: '#0b84ff',
33
+ height: 2,
34
+ left: 0,
35
+ pointerEvents: 'none',
36
+ position: 'absolute',
37
+ right: 0,
38
+ zIndex: 1,
39
+ };
40
+ const hasEffectReorderDragType = (dataTransfer) => {
41
+ return Array.from(dataTransfer.types).includes(EFFECT_REORDER_MIME_TYPE);
42
+ };
43
+ const getEffectReorderDragData = (dataTransfer) => {
44
+ if (currentEffectDrag) {
45
+ return currentEffectDrag;
46
+ }
47
+ const value = dataTransfer.getData(EFFECT_REORDER_MIME_TYPE);
48
+ if (!value) {
49
+ return null;
50
+ }
51
+ try {
52
+ const parsed = JSON.parse(value);
53
+ if (typeof parsed.nodePathKey === 'string' &&
54
+ typeof parsed.effectIndex === 'number') {
55
+ return parsed;
56
+ }
57
+ }
58
+ catch (_a) {
59
+ return null;
60
+ }
61
+ return null;
62
+ };
63
+ const getDestinationIndex = ({ fromIndex, insertionIndex, }) => {
64
+ return insertionIndex > fromIndex ? insertionIndex - 1 : insertionIndex;
65
+ };
22
66
  const TimelineEffectItem = ({ label, nodePathInfo, effectIndex, effectSchema, documentationLink, nodePath, validatedLocation, rowDepth, getIsExpanded, toggleTrack, }) => {
23
67
  var _a;
24
68
  var _b;
@@ -27,6 +71,7 @@ const TimelineEffectItem = ({ label, nodePathInfo, effectIndex, effectSchema, do
27
71
  const { codeValues } = (0, react_1.useContext)(remotion_1.Internals.VisualModeCodeValuesContext);
28
72
  const { setCodeValues } = (0, react_1.useContext)(remotion_1.Internals.VisualModeSettersContext);
29
73
  const selection = (0, TimelineSelection_1.useTimelineRowSelection)(nodePathInfo);
74
+ const [dropIndicator, setDropIndicator] = (0, react_1.useState)(null);
30
75
  const effectStatus = (0, react_1.useMemo)(() => remotion_1.Internals.getEffectCodeValuesCtx({
31
76
  codeValues,
32
77
  nodePath,
@@ -45,6 +90,11 @@ const TimelineEffectItem = ({ label, nodePathInfo, effectIndex, effectSchema, do
45
90
  const deleteDisabled = !previewConnected ||
46
91
  effectStatus.type !== 'can-update-effect' ||
47
92
  !validatedLocation.source;
93
+ const canReorder = TimelineSelection_1.SELECTION_ENABLED &&
94
+ previewConnected &&
95
+ effectStatus.type === 'can-update-effect' &&
96
+ Boolean(validatedLocation.source);
97
+ const nodePathKey = (0, react_1.useMemo)(() => remotion_1.Internals.makeSequencePropsSubscriptionKey(nodePath), [nodePath]);
48
98
  const onDeleteEffectFromSource = (0, react_1.useCallback)(async () => {
49
99
  if (deleteDisabled) {
50
100
  return;
@@ -148,9 +198,6 @@ const TimelineEffectItem = ({ label, nodePathInfo, effectIndex, effectSchema, do
148
198
  validatedLocation.source,
149
199
  ]);
150
200
  const isExpanded = getIsExpanded(nodePathInfo);
151
- const rowStyle = (0, react_1.useMemo)(() => ({
152
- height: timeline_layout_1.TREE_GROUP_ROW_HEIGHT,
153
- }), []);
154
201
  const labelStyle = (0, react_1.useMemo)(() => {
155
202
  return {
156
203
  ...rowLabel,
@@ -164,7 +211,113 @@ const TimelineEffectItem = ({ label, nodePathInfo, effectIndex, effectSchema, do
164
211
  paddingRight: timeline_layout_1.EXPANDED_SECTION_PADDING_RIGHT,
165
212
  };
166
213
  }, [selection.selected]);
214
+ const getDropTarget = (0, react_1.useCallback)((e) => {
215
+ const dragData = getEffectReorderDragData(e.dataTransfer);
216
+ if (!dragData || dragData.nodePathKey !== nodePathKey) {
217
+ return null;
218
+ }
219
+ const rect = e.currentTarget.getBoundingClientRect();
220
+ const before = e.clientY < rect.top + rect.height / 2;
221
+ const insertionIndex = before ? effectIndex : effectIndex + 1;
222
+ const toIndex = getDestinationIndex({
223
+ fromIndex: dragData.effectIndex,
224
+ insertionIndex,
225
+ });
226
+ if (toIndex === dragData.effectIndex) {
227
+ return null;
228
+ }
229
+ return {
230
+ dragData,
231
+ toIndex,
232
+ indicator: before ? 'before' : 'after',
233
+ };
234
+ }, [effectIndex, nodePathKey]);
235
+ const onDragStart = (0, react_1.useCallback)((e) => {
236
+ if (!canReorder) {
237
+ e.preventDefault();
238
+ return;
239
+ }
240
+ const dragData = { nodePathKey, effectIndex };
241
+ currentEffectDrag = dragData;
242
+ e.dataTransfer.effectAllowed = 'move';
243
+ e.dataTransfer.setData(EFFECT_REORDER_MIME_TYPE, JSON.stringify(dragData));
244
+ e.stopPropagation();
245
+ }, [canReorder, effectIndex, nodePathKey]);
246
+ const onDragEnd = (0, react_1.useCallback)(() => {
247
+ currentEffectDrag = null;
248
+ setDropIndicator(null);
249
+ }, []);
250
+ const onDragOver = (0, react_1.useCallback)((e) => {
251
+ if (!canReorder || !hasEffectReorderDragType(e.dataTransfer)) {
252
+ return;
253
+ }
254
+ const dropTarget = getDropTarget(e);
255
+ if (!dropTarget) {
256
+ setDropIndicator(null);
257
+ return;
258
+ }
259
+ e.preventDefault();
260
+ e.stopPropagation();
261
+ e.dataTransfer.dropEffect = 'move';
262
+ setDropIndicator(dropTarget.indicator);
263
+ }, [canReorder, getDropTarget]);
264
+ const onDragLeave = (0, react_1.useCallback)((e) => {
265
+ if (e.currentTarget.contains(e.relatedTarget)) {
266
+ return;
267
+ }
268
+ setDropIndicator(null);
269
+ }, []);
270
+ const onDrop = (0, react_1.useCallback)(async (e) => {
271
+ if (!canReorder ||
272
+ previewServerState.type !== 'connected' ||
273
+ !validatedLocation.source) {
274
+ return;
275
+ }
276
+ const dropTarget = getDropTarget(e);
277
+ if (!dropTarget) {
278
+ setDropIndicator(null);
279
+ return;
280
+ }
281
+ e.preventDefault();
282
+ e.stopPropagation();
283
+ setDropIndicator(null);
284
+ currentEffectDrag = null;
285
+ try {
286
+ const result = await (0, call_api_1.callApi)('/api/reorder-effect', {
287
+ fileName: validatedLocation.source,
288
+ sequenceNodePath: nodePath,
289
+ fromIndex: dropTarget.dragData.effectIndex,
290
+ toIndex: dropTarget.toIndex,
291
+ clientId: previewServerState.clientId,
292
+ });
293
+ if (result.success) {
294
+ (0, NotificationCenter_1.showNotification)('Reordered effect', 2000);
295
+ }
296
+ else {
297
+ (0, NotificationCenter_1.showNotification)(result.reason, 4000);
298
+ }
299
+ }
300
+ catch (err) {
301
+ (0, NotificationCenter_1.showNotification)(err.message, 4000);
302
+ }
303
+ }, [
304
+ canReorder,
305
+ getDropTarget,
306
+ nodePath,
307
+ previewServerState,
308
+ validatedLocation.source,
309
+ ]);
310
+ const reorderLineStyle = (0, react_1.useMemo)(() => {
311
+ if (!dropIndicator) {
312
+ return null;
313
+ }
314
+ return {
315
+ ...reorderLineBase,
316
+ ...(dropIndicator === 'before' ? { top: -1 } : { bottom: -1 }),
317
+ };
318
+ }, [dropIndicator]);
167
319
  const row = (jsx_runtime_1.jsx(TimelineRowChrome_1.TimelineRowChrome, { depth: rowDepth, eye: canToggle ? (jsx_runtime_1.jsx(TimelineLayerEye_1.TimelineLayerEye, { type: "effect", hidden: isDisabled, onInvoked: onToggle })) : (jsx_runtime_1.jsx(TimelineLayerEye_1.TimelineLayerEyeSpacer, {})), arrow: jsx_runtime_1.jsx(TimelineExpandArrowButton_1.TimelineExpandArrowButton, { isExpanded: isExpanded, onClick: () => toggleTrack(nodePathInfo), label: `${label} section`, disabled: false }), style: rowStyle, selected: selection.selected, selectable: selection.selectable, onSelect: selection.onSelect, showSelectedBackground: true, containsSelection: false, outerHeight: null, children: jsx_runtime_1.jsx("span", { title: label, style: labelStyle, children: label }) }));
168
- return previewConnected ? (jsx_runtime_1.jsx(ContextMenu_1.ContextMenu, { values: contextMenuValues, onOpen: selection.selectable ? selection.onSelect : null, children: row })) : (row);
320
+ const draggableRow = canReorder ? (jsx_runtime_1.jsxs("div", { draggable: true, onDragStart: onDragStart, onDragEnd: onDragEnd, onDragOver: onDragOver, onDragLeave: onDragLeave, onDrop: onDrop, style: reorderWrapper, children: [reorderLineStyle ? jsx_runtime_1.jsx("div", { style: reorderLineStyle }) : null, row] })) : (row);
321
+ return previewConnected ? (jsx_runtime_1.jsx(ContextMenu_1.ContextMenu, { values: contextMenuValues, onOpen: selection.selectable ? selection.onSelect : null, children: draggableRow })) : (draggableRow);
169
322
  };
170
323
  exports.TimelineEffectItem = TimelineEffectItem;
@@ -15,7 +15,7 @@ const controlsContainerStyle = {
15
15
  alignItems: 'center',
16
16
  display: 'flex',
17
17
  flexShrink: 0,
18
- gap: 2,
18
+ gap: 1,
19
19
  marginRight: 4,
20
20
  };
21
21
  const navButtonStyle = {
@@ -26,19 +26,24 @@ const navButtonStyle = {
26
26
  cursor: 'pointer',
27
27
  display: 'flex',
28
28
  flexShrink: 0,
29
- fontSize: 8,
30
29
  height: 12,
31
30
  justifyContent: 'center',
32
31
  lineHeight: 1,
33
32
  outline: 'none',
34
33
  padding: 0,
35
34
  userSelect: 'none',
36
- width: 10,
35
+ width: 12,
37
36
  };
38
37
  const diamondButtonStyle = {
39
38
  ...navButtonStyle,
40
- height: 10,
41
- width: 10,
39
+ background: 'none',
40
+ };
41
+ const diamondIconStyle = {
42
+ borderRadius: 1,
43
+ boxShadow: '0 0 0 1px rgba(0, 0, 0, 0.4)',
44
+ height: 7,
45
+ transform: 'rotate(45deg)',
46
+ width: 7,
42
47
  };
43
48
  const svgStyle = { display: 'block' };
44
49
  const getCurrentKeyframeValue = ({ propStatus, jsxFrame, defaultValue, dragOverrideValue, }) => {
@@ -203,15 +208,15 @@ const TimelineKeyframeControls = ({ fieldKey, propStatus, nodePath, fileName, ke
203
208
  }), [nextDisabled]);
204
209
  const diamondStyle = (0, react_1.useMemo)(() => ({
205
210
  ...diamondButtonStyle,
206
- backgroundColor: hasKeyframeAtCurrentFrame ? colors_1.BLUE : colors_1.LIGHT_TEXT,
207
- borderRadius: 1,
208
- boxShadow: '0 0 0 1px rgba(0, 0, 0, 0.4)',
209
211
  cursor: canToggleKeyframe && clientId ? 'pointer' : 'default',
210
212
  opacity: canToggleKeyframe && clientId ? 1 : 0.35,
211
- transform: 'rotate(45deg)',
212
- }), [canToggleKeyframe, clientId, hasKeyframeAtCurrentFrame]);
213
+ }), [canToggleKeyframe, clientId]);
214
+ const diamondIcon = (0, react_1.useMemo)(() => ({
215
+ ...diamondIconStyle,
216
+ backgroundColor: hasKeyframeAtCurrentFrame ? colors_1.BLUE : colors_1.LIGHT_TEXT,
217
+ }), [hasKeyframeAtCurrentFrame]);
213
218
  return (jsx_runtime_1.jsxs("div", { style: controlsContainerStyle, children: [
214
- jsx_runtime_1.jsx("button", { type: "button", style: previousStyle, disabled: previousDisabled, onPointerDown: previousDisabled ? undefined : onPrevious, "aria-label": "Go to previous keyframe", title: "Previous keyframe", children: jsx_runtime_1.jsx("svg", { width: "8", height: "8", viewBox: "0 0 8 8", style: svgStyle, children: jsx_runtime_1.jsx("path", { d: "M5 1L2 4L5 7Z", fill: "#ccc" }) }) }), jsx_runtime_1.jsx("button", { type: "button", style: diamondStyle, disabled: !canToggleKeyframe || !clientId, onPointerDown: canToggleKeyframe && clientId ? onToggleKeyframe : undefined, "aria-label": hasKeyframeAtCurrentFrame ? 'Remove keyframe' : 'Add keyframe', title: hasKeyframeAtCurrentFrame ? 'Remove keyframe' : 'Add keyframe' }), jsx_runtime_1.jsx("button", { type: "button", style: nextStyle, disabled: nextDisabled, onPointerDown: nextDisabled ? undefined : onNext, "aria-label": "Go to next keyframe", title: "Next keyframe", children: jsx_runtime_1.jsx("svg", { width: "8", height: "8", viewBox: "0 0 8 8", style: svgStyle, children: jsx_runtime_1.jsx("path", { d: "M3 1L6 4L3 7Z", fill: "#ccc" }) }) })
219
+ jsx_runtime_1.jsx("button", { type: "button", style: previousStyle, disabled: previousDisabled, onPointerDown: previousDisabled ? undefined : onPrevious, "aria-label": "Go to previous keyframe", title: "Previous keyframe", children: jsx_runtime_1.jsx("svg", { width: "14", height: "14", viewBox: "0 0 10 10", style: svgStyle, children: jsx_runtime_1.jsx("path", { d: "M7 1.5L3 5L7 8.5Z", fill: "#ccc" }) }) }), jsx_runtime_1.jsx("button", { type: "button", style: diamondStyle, disabled: !canToggleKeyframe || !clientId, onPointerDown: canToggleKeyframe && clientId ? onToggleKeyframe : undefined, "aria-label": hasKeyframeAtCurrentFrame ? 'Remove keyframe' : 'Add keyframe', title: hasKeyframeAtCurrentFrame ? 'Remove keyframe' : 'Add keyframe', children: jsx_runtime_1.jsx("span", { style: diamondIcon }) }), jsx_runtime_1.jsx("button", { type: "button", style: nextStyle, disabled: nextDisabled, onPointerDown: nextDisabled ? undefined : onNext, "aria-label": "Go to next keyframe", title: "Next keyframe", children: jsx_runtime_1.jsx("svg", { width: "14", height: "14", viewBox: "0 0 10 10", style: svgStyle, children: jsx_runtime_1.jsx("path", { d: "M3 1.5L7 5L3 8.5Z", fill: "#ccc" }) }) })
215
220
  ] }));
216
221
  };
217
222
  exports.TimelineKeyframeControls = TimelineKeyframeControls;
@@ -13,5 +13,8 @@ export declare const TimelineRowChrome: React.FC<{
13
13
  readonly showSelectedBackground: boolean;
14
14
  readonly containsSelection: boolean;
15
15
  readonly outerHeight: number | null;
16
+ readonly onDragLeave?: (e: React.DragEvent<HTMLDivElement>) => void;
17
+ readonly onDragOver?: (e: React.DragEvent<HTMLDivElement>) => void;
18
+ readonly onDrop?: (e: React.DragEvent<HTMLDivElement>) => void;
16
19
  readonly onDoubleClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
17
20
  }>;
@@ -25,7 +25,7 @@ const keyframeControlsColumnStyle = {
25
25
  marginRight: -(timeline_row_layout_1.TIMELINE_KEYFRAME_CONTROLS_WIDTH - timeline_row_layout_1.TIMELINE_ROW_BASE_PADDING),
26
26
  width: timeline_row_layout_1.TIMELINE_KEYFRAME_CONTROLS_WIDTH,
27
27
  };
28
- const TimelineRowChrome = ({ depth, eye, keyframeControls, arrow, children, style, selected, selectable, onSelect, showSelectedBackground, containsSelection, outerHeight, onDoubleClick, }) => {
28
+ const TimelineRowChrome = ({ depth, eye, keyframeControls, arrow, children, style, selected, selectable, onSelect, showSelectedBackground, containsSelection, outerHeight, onDragLeave, onDragOver, onDrop, onDoubleClick, }) => {
29
29
  const indentWidth = (0, timeline_row_layout_1.getTimelineRowIndentWidth)(depth);
30
30
  const chromeColumnStyle = (0, react_1.useMemo)(() => ({
31
31
  alignItems: 'center',
@@ -72,8 +72,8 @@ const TimelineRowChrome = ({ depth, eye, keyframeControls, arrow, children, styl
72
72
  jsx_runtime_1.jsxs("div", { style: leftChromeStyle, children: [keyframeControls ? (jsx_runtime_1.jsx("div", { style: keyframeControlsColumnStyle, children: keyframeControls })) : null, jsx_runtime_1.jsxs("div", { style: chromeColumnStyle, children: [eye, indentWidth > 0 ? jsx_runtime_1.jsx(Padder_1.Padder, { depth: depth }) : null, arrow] })
73
73
  ] }), children] }));
74
74
  if (outerStyle) {
75
- return (jsx_runtime_1.jsx("div", { style: outerStyle, onPointerDown: selectable ? onPointerDown : undefined, onContextMenu: selectable ? onContextMenu : undefined, onDoubleClick: onDoubleClick, children: jsx_runtime_1.jsx("div", { style: innerRowStyle, children: chrome }) }));
75
+ return (jsx_runtime_1.jsx("div", { style: outerStyle, onDragLeave: onDragLeave, onDragOver: onDragOver, onDrop: onDrop, onPointerDown: selectable ? onPointerDown : undefined, onContextMenu: selectable ? onContextMenu : undefined, onDoubleClick: onDoubleClick, children: jsx_runtime_1.jsx("div", { style: innerRowStyle, children: chrome }) }));
76
76
  }
77
- return (jsx_runtime_1.jsx("div", { onPointerDown: selectable ? onPointerDown : undefined, onContextMenu: selectable ? onContextMenu : undefined, onDoubleClick: onDoubleClick, style: innerRowStyle, children: chrome }));
77
+ return (jsx_runtime_1.jsx("div", { onDragLeave: onDragLeave, onDragOver: onDragOver, onDrop: onDrop, onPointerDown: selectable ? onPointerDown : undefined, onContextMenu: selectable ? onContextMenu : undefined, onDoubleClick: onDoubleClick, style: innerRowStyle, children: chrome }));
78
78
  };
79
79
  exports.TimelineRowChrome = TimelineRowChrome;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TimelineSequenceItem = void 0;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const studio_shared_1 = require("@remotion/studio-shared");
5
6
  const react_1 = require("react");
6
7
  const remotion_1 = require("remotion");
7
8
  const no_react_1 = require("remotion/no-react");
@@ -31,6 +32,33 @@ const labelContainerStyle = {
31
32
  minWidth: 0,
32
33
  gap: 4,
33
34
  };
35
+ const effectDropHighlight = {
36
+ backgroundColor: 'rgba(0, 155, 255, 0.16)',
37
+ outline: '1px solid rgba(0, 155, 255, 0.75)',
38
+ outlineOffset: -1,
39
+ };
40
+ const hasEffectDragType = (dataTransfer) => {
41
+ return Array.from(dataTransfer.types).some((type) => type === studio_shared_1.EFFECT_DRAG_MIME_TYPE ||
42
+ type === 'application/json' ||
43
+ type === 'text/plain');
44
+ };
45
+ const getEffectDragData = (dataTransfer) => {
46
+ for (const type of [
47
+ studio_shared_1.EFFECT_DRAG_MIME_TYPE,
48
+ 'application/json',
49
+ 'text/plain',
50
+ ]) {
51
+ const value = dataTransfer.getData(type);
52
+ if (!value) {
53
+ continue;
54
+ }
55
+ const parsed = (0, studio_shared_1.parseEffectDragData)(value);
56
+ if (parsed) {
57
+ return parsed;
58
+ }
59
+ }
60
+ return null;
61
+ };
34
62
  const TimelineSequenceItem = ({ nestedDepth, sequence, nodePathInfo, keyframeDisplayOffset }) => {
35
63
  var _a;
36
64
  var _b;
@@ -44,6 +72,7 @@ const TimelineSequenceItem = ({ nestedDepth, sequence, nodePathInfo, keyframeDis
44
72
  const { setCanvasContent } = (0, react_1.useContext)(remotion_1.Internals.CompositionSetters);
45
73
  const { onSelect, selectable, selected } = (0, TimelineSelection_1.useTimelineRowSelection)(nodePathInfo);
46
74
  const containsSelection = (0, TimelineSelection_1.useTimelineRowContainsSelection)(nodePathInfo);
75
+ const [effectDropHovered, setEffectDropHovered] = (0, react_1.useState)(false);
47
76
  const { canOpenInEditor, openInEditor, originalLocation } = (0, use_open_sequence_in_editor_1.useOpenSequenceInEditor)(sequence);
48
77
  const fileLocation = (0, react_1.useMemo)(() => (0, format_file_location_1.formatFileLocation)({
49
78
  location: originalLocation,
@@ -322,6 +351,14 @@ const TimelineSequenceItem = ({ nestedDepth, sequence, nodePathInfo, keyframeDis
322
351
  flexShrink: 0,
323
352
  };
324
353
  }, []);
354
+ const rowStyle = (0, react_1.useMemo)(() => {
355
+ return effectDropHovered
356
+ ? {
357
+ ...inner,
358
+ ...effectDropHighlight,
359
+ }
360
+ : inner;
361
+ }, [effectDropHovered, inner]);
325
362
  const hasExpandableContent = Boolean(sequence.controls) || sequence.effects.length > 0;
326
363
  const canToggleVisibility = previewConnected &&
327
364
  Boolean(sequence.controls) &&
@@ -330,7 +367,60 @@ const TimelineSequenceItem = ({ nestedDepth, sequence, nodePathInfo, keyframeDis
330
367
  codeHiddenStatus !== undefined &&
331
368
  codeHiddenStatus !== null &&
332
369
  codeHiddenStatus.canUpdate;
333
- const trackRow = (jsx_runtime_1.jsx(TimelineRowChrome_1.TimelineRowChrome, { depth: nestedDepth, eye: canToggleVisibility ? (jsx_runtime_1.jsx(TimelineLayerEye_1.TimelineLayerEye, { type: sequence.type === 'audio' ? 'speaker' : 'eye', hidden: isItemHidden, onInvoked: onToggleVisibility })) : (jsx_runtime_1.jsx(TimelineLayerEye_1.TimelineLayerEyeSpacer, {})), arrow: hasExpandableContent ? (jsx_runtime_1.jsx(TimelineExpandArrowButton_1.TimelineExpandArrowButton, { isExpanded: isExpanded, onClick: onToggleExpand, label: "track properties", disabled: !previewConnected || nodePathInfo === null })) : (jsx_runtime_1.jsx(TimelineExpandArrowButton_1.TimelineExpandArrowSpacer, {})), style: inner, selected: selected, selectable: selectable, onSelect: onSelect, showSelectedBackground: true, containsSelection: containsSelection, outerHeight: outerHeight, onDoubleClick: TimelineSelection_1.SELECTION_ENABLED && canOpenInEditor
370
+ const canDropEffect = previewServerState.type === 'connected' &&
371
+ nodePath !== null &&
372
+ validatedLocation !== null &&
373
+ sequence.type !== 'audio';
374
+ const onEffectDragOver = (0, react_1.useCallback)((e) => {
375
+ if (!canDropEffect || !hasEffectDragType(e.dataTransfer)) {
376
+ return;
377
+ }
378
+ e.preventDefault();
379
+ e.dataTransfer.dropEffect = 'copy';
380
+ setEffectDropHovered(true);
381
+ }, [canDropEffect]);
382
+ const onEffectDragLeave = (0, react_1.useCallback)((e) => {
383
+ if (e.currentTarget.contains(e.relatedTarget)) {
384
+ return;
385
+ }
386
+ setEffectDropHovered(false);
387
+ }, []);
388
+ const onEffectDrop = (0, react_1.useCallback)(async (e) => {
389
+ if (!canDropEffect ||
390
+ previewServerState.type !== 'connected' ||
391
+ nodePath === null ||
392
+ validatedLocation === null) {
393
+ return;
394
+ }
395
+ e.preventDefault();
396
+ e.stopPropagation();
397
+ setEffectDropHovered(false);
398
+ const dragData = getEffectDragData(e.dataTransfer);
399
+ if (!dragData) {
400
+ (0, NotificationCenter_1.showNotification)('Could not read effect drag data', 3000);
401
+ return;
402
+ }
403
+ try {
404
+ const result = await (0, call_api_1.callApi)('/api/add-effect', {
405
+ fileName: validatedLocation.source,
406
+ sequenceNodePath: nodePath,
407
+ effectName: dragData.effect.name,
408
+ effectImportPath: dragData.effect.importPath,
409
+ effectConfig: dragData.effect.config,
410
+ clientId: previewServerState.clientId,
411
+ });
412
+ if (result.success) {
413
+ (0, NotificationCenter_1.showNotification)(`Added ${dragData.effect.name}()`, 2000);
414
+ }
415
+ else {
416
+ (0, NotificationCenter_1.showNotification)(result.reason, 4000);
417
+ }
418
+ }
419
+ catch (err) {
420
+ (0, NotificationCenter_1.showNotification)(err.message, 4000);
421
+ }
422
+ }, [canDropEffect, nodePath, previewServerState, validatedLocation]);
423
+ const trackRow = (jsx_runtime_1.jsx(TimelineRowChrome_1.TimelineRowChrome, { depth: nestedDepth, eye: canToggleVisibility ? (jsx_runtime_1.jsx(TimelineLayerEye_1.TimelineLayerEye, { type: sequence.type === 'audio' ? 'speaker' : 'eye', hidden: isItemHidden, onInvoked: onToggleVisibility })) : (jsx_runtime_1.jsx(TimelineLayerEye_1.TimelineLayerEyeSpacer, {})), arrow: hasExpandableContent ? (jsx_runtime_1.jsx(TimelineExpandArrowButton_1.TimelineExpandArrowButton, { isExpanded: isExpanded, onClick: onToggleExpand, label: "track properties", disabled: !previewConnected || nodePathInfo === null })) : (jsx_runtime_1.jsx(TimelineExpandArrowButton_1.TimelineExpandArrowSpacer, {})), style: rowStyle, selected: selected, selectable: selectable, onSelect: onSelect, showSelectedBackground: true, containsSelection: containsSelection, outerHeight: outerHeight, onDragLeave: canDropEffect ? onEffectDragLeave : undefined, onDragOver: canDropEffect ? onEffectDragOver : undefined, onDrop: canDropEffect ? onEffectDrop : undefined, onDoubleClick: TimelineSelection_1.SELECTION_ENABLED && canOpenInEditor
334
424
  ? onShowInEditorDoubleClick
335
425
  : undefined, children: jsx_runtime_1.jsxs("div", { style: labelContainerStyle, children: [
336
426
  jsx_runtime_1.jsx(TimelineSequenceName_1.TimelineSequenceName, { sequence: sequence, selected: selected, containsSelection: containsSelection }), mediaSrc ? jsx_runtime_1.jsx(TimelineMediaInfo_1.TimelineMediaInfo, { src: mediaSrc }) : null, jsx_runtime_1.jsx(TimelineItemStack_1.TimelineItemStack, { originalLocation: originalLocation })
@@ -1,5 +1,15 @@
1
1
  import type { SequencePropsSubscriptionKey, SequenceSchema } from 'remotion';
2
2
  import type { SetCodeValues } from './save-sequence-prop';
3
+ export type DeleteSequenceKeyframeChange = {
4
+ fileName: string;
5
+ nodePath: SequencePropsSubscriptionKey;
6
+ fieldKey: string;
7
+ sourceFrame: number;
8
+ schema: SequenceSchema;
9
+ };
10
+ export type DeleteEffectKeyframeChange = DeleteSequenceKeyframeChange & {
11
+ effectIndex: number;
12
+ };
3
13
  export declare const callDeleteSequenceKeyframe: ({ fileName, nodePath, fieldKey, sourceFrame, schema, setCodeValues, clientId, }: {
4
14
  fileName: string;
5
15
  nodePath: SequencePropsSubscriptionKey;
@@ -19,3 +29,9 @@ export declare const callDeleteEffectKeyframe: ({ fileName, nodePath, effectInde
19
29
  setCodeValues: SetCodeValues;
20
30
  clientId: string;
21
31
  }) => Promise<void>;
32
+ export declare const callDeleteKeyframes: ({ sequenceKeyframes, effectKeyframes, setCodeValues, clientId, }: {
33
+ sequenceKeyframes: DeleteSequenceKeyframeChange[];
34
+ effectKeyframes: DeleteEffectKeyframeChange[];
35
+ setCodeValues: SetCodeValues;
36
+ clientId: string;
37
+ }) => Promise<void>;
@@ -1,9 +1,20 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.callDeleteEffectKeyframe = exports.callDeleteSequenceKeyframe = void 0;
3
+ exports.callDeleteKeyframes = exports.callDeleteEffectKeyframe = exports.callDeleteSequenceKeyframe = void 0;
4
4
  const studio_shared_1 = require("@remotion/studio-shared");
5
5
  const call_api_1 = require("../call-api");
6
6
  const save_prop_queue_1 = require("./save-prop-queue");
7
+ const groupByNodePath = (keyframes) => {
8
+ var _a;
9
+ const groups = new Map();
10
+ for (const keyframe of keyframes) {
11
+ const key = JSON.stringify(keyframe.nodePath);
12
+ const group = (_a = groups.get(key)) !== null && _a !== void 0 ? _a : [];
13
+ group.push(keyframe);
14
+ groups.set(key, group);
15
+ }
16
+ return [...groups.values()];
17
+ };
7
18
  const callDeleteSequenceKeyframe = ({ fileName, nodePath, fieldKey, sourceFrame, schema, setCodeValues, clientId, }) => {
8
19
  return (0, save_prop_queue_1.enqueueSavePropChange)({
9
20
  nodePath,
@@ -13,12 +24,17 @@ const callDeleteSequenceKeyframe = ({ fileName, nodePath, fieldKey, sourceFrame,
13
24
  fieldKey,
14
25
  frame: sourceFrame,
15
26
  }),
16
- apiCall: () => (0, call_api_1.callApi)('/api/delete-sequence-keyframe', {
17
- fileName,
18
- nodePath,
19
- key: fieldKey,
20
- frame: sourceFrame,
21
- schema,
27
+ apiCall: () => (0, call_api_1.callApi)('/api/delete-keyframes', {
28
+ sequenceKeyframes: [
29
+ {
30
+ fileName,
31
+ nodePath,
32
+ key: fieldKey,
33
+ frame: sourceFrame,
34
+ schema,
35
+ },
36
+ ],
37
+ effectKeyframes: [],
22
38
  clientId,
23
39
  }),
24
40
  errorLabel: 'Could not delete keyframe',
@@ -35,16 +51,72 @@ const callDeleteEffectKeyframe = ({ fileName, nodePath, effectIndex, fieldKey, s
35
51
  fieldKey,
36
52
  frame: sourceFrame,
37
53
  }),
38
- apiCall: () => (0, call_api_1.callApi)('/api/delete-effect-keyframe', {
39
- fileName,
40
- sequenceNodePath: nodePath,
41
- effectIndex,
42
- key: fieldKey,
43
- frame: sourceFrame,
44
- schema,
54
+ apiCall: () => (0, call_api_1.callApi)('/api/delete-keyframes', {
55
+ sequenceKeyframes: [],
56
+ effectKeyframes: [
57
+ {
58
+ fileName,
59
+ sequenceNodePath: nodePath,
60
+ effectIndex,
61
+ key: fieldKey,
62
+ frame: sourceFrame,
63
+ schema,
64
+ },
65
+ ],
45
66
  clientId,
46
67
  }),
47
68
  errorLabel: 'Could not delete keyframe',
48
69
  });
49
70
  };
50
71
  exports.callDeleteEffectKeyframe = callDeleteEffectKeyframe;
72
+ const callDeleteKeyframes = ({ sequenceKeyframes, effectKeyframes, setCodeValues, clientId, }) => {
73
+ if (sequenceKeyframes.length === 0 && effectKeyframes.length === 0) {
74
+ return Promise.resolve();
75
+ }
76
+ for (const keyframes of groupByNodePath(sequenceKeyframes)) {
77
+ const [firstKeyframe] = keyframes;
78
+ if (!firstKeyframe) {
79
+ continue;
80
+ }
81
+ setCodeValues(firstKeyframe.nodePath, (prev) => (0, studio_shared_1.optimisticDeleteSequenceKeyframes)({
82
+ previous: prev,
83
+ keyframes: keyframes.map((keyframe) => ({
84
+ fieldKey: keyframe.fieldKey,
85
+ frame: keyframe.sourceFrame,
86
+ })),
87
+ }));
88
+ }
89
+ for (const keyframes of groupByNodePath(effectKeyframes)) {
90
+ const [firstKeyframe] = keyframes;
91
+ if (!firstKeyframe) {
92
+ continue;
93
+ }
94
+ setCodeValues(firstKeyframe.nodePath, (prev) => (0, studio_shared_1.optimisticDeleteEffectKeyframes)({
95
+ previous: prev,
96
+ keyframes: keyframes.map((keyframe) => ({
97
+ effectIndex: keyframe.effectIndex,
98
+ fieldKey: keyframe.fieldKey,
99
+ frame: keyframe.sourceFrame,
100
+ })),
101
+ }));
102
+ }
103
+ return (0, call_api_1.callApi)('/api/delete-keyframes', {
104
+ sequenceKeyframes: sequenceKeyframes.map((keyframe) => ({
105
+ fileName: keyframe.fileName,
106
+ nodePath: keyframe.nodePath,
107
+ key: keyframe.fieldKey,
108
+ frame: keyframe.sourceFrame,
109
+ schema: keyframe.schema,
110
+ })),
111
+ effectKeyframes: effectKeyframes.map((keyframe) => ({
112
+ fileName: keyframe.fileName,
113
+ sequenceNodePath: keyframe.nodePath,
114
+ effectIndex: keyframe.effectIndex,
115
+ key: keyframe.fieldKey,
116
+ frame: keyframe.sourceFrame,
117
+ schema: keyframe.schema,
118
+ })),
119
+ clientId,
120
+ }).then(() => undefined);
121
+ };
122
+ exports.callDeleteKeyframes = callDeleteKeyframes;
@@ -9,3 +9,13 @@ export declare const deleteSelectedKeyframe: ({ nodePathInfo, frame, sequences,
9
9
  setCodeValues: SetCodeValues;
10
10
  clientId: string;
11
11
  }) => Promise<void> | null;
12
+ export declare const deleteSelectedKeyframes: ({ keyframes, sequences, overrideIdsToNodePaths, setCodeValues, clientId, }: {
13
+ keyframes: {
14
+ nodePathInfo: SequenceNodePathInfo;
15
+ frame: number;
16
+ }[];
17
+ sequences: TSequence[];
18
+ overrideIdsToNodePaths: OverrideIdToNodePaths;
19
+ setCodeValues: SetCodeValues;
20
+ clientId: string;
21
+ }) => Promise<void> | null;