mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
feat: Clip effects, asset sorting, and timeline improvements
Major features and improvements: * **Clip Effects**: * Added UI in Properties Panel to manage effects on video/image clips (add, remove, toggle, reorder). * Implemented dynamic parameter fields for effects. * Added support for keyframing effect parameters. * **Assets Panel**: * Added sorting options: Name, Type, Duration, and File Size. * Persisted view preferences (grid/list mode, sort order) to local storage. * Refactored media item rendering and drag interactions. * **Timeline & Interaction**: * **Keyframe Dragging**: Added ability to drag keyframes directly on the timeline element. * **Resizing**: Improved resize logic to respect neighboring clips (prevents overlaps). * **Visuals**: Implemented tiled background rendering for video/image clips on the timeline. * **Shortcuts**: Added "Deselect All" action bound to the `Escape` key. * **Fixes**: Corrected drag-and-drop coordinate calculations when the timeline track area is scrolled. * **Text Elements**: * Refactored text background storage to use an explicit `enabled` flag. * Added `V8toV9` storage migration to update existing projects. * **Architecture**: * Moved export state management to `ProjectManager` for better lifecycle handling. * Refactored `PropertiesPanel` sections to be more composable (custom headers, borders).
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import { Command } from "@/lib/commands/base-command";
|
||||
import { EditorCore } from "@/core";
|
||||
import { isVisualElement, updateElementInTracks } from "@/lib/timeline";
|
||||
import type { TimelineTrack, VisualElement } from "@/types/timeline";
|
||||
import { buildDefaultEffectInstance } from "@/lib/effects";
|
||||
|
||||
function addEffectToElement({
|
||||
element,
|
||||
effectType,
|
||||
}: {
|
||||
element: VisualElement;
|
||||
effectType: string;
|
||||
}): VisualElement {
|
||||
const instance = buildDefaultEffectInstance({ effectType });
|
||||
const currentEffects = element.effects ?? [];
|
||||
return { ...element, effects: [...currentEffects, instance] };
|
||||
}
|
||||
|
||||
export class AddClipEffectCommand extends Command {
|
||||
private savedState: TimelineTrack[] | null = null;
|
||||
private effectId: string | null = null;
|
||||
private readonly trackId: string;
|
||||
private readonly elementId: string;
|
||||
private readonly effectType: string;
|
||||
|
||||
constructor({
|
||||
trackId,
|
||||
elementId,
|
||||
effectType,
|
||||
}: {
|
||||
trackId: string;
|
||||
elementId: string;
|
||||
effectType: string;
|
||||
}) {
|
||||
super();
|
||||
this.trackId = trackId;
|
||||
this.elementId = elementId;
|
||||
this.effectType = effectType;
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const editor = EditorCore.getInstance();
|
||||
this.savedState = editor.timeline.getTracks();
|
||||
|
||||
const updatedTracks = updateElementInTracks({
|
||||
tracks: this.savedState,
|
||||
trackId: this.trackId,
|
||||
elementId: this.elementId,
|
||||
elementPredicate: isVisualElement,
|
||||
update: (element) => {
|
||||
const updated = addEffectToElement({
|
||||
element: element as VisualElement,
|
||||
effectType: this.effectType,
|
||||
});
|
||||
const effects = updated.effects ?? [];
|
||||
this.effectId = effects[effects.length - 1]?.id ?? null;
|
||||
return updated;
|
||||
},
|
||||
});
|
||||
|
||||
editor.timeline.updateTracks(updatedTracks);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (this.savedState) {
|
||||
const editor = EditorCore.getInstance();
|
||||
editor.timeline.updateTracks(this.savedState);
|
||||
}
|
||||
}
|
||||
|
||||
getEffectId(): string | null {
|
||||
return this.effectId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export { AddClipEffectCommand } from "./add-effect";
|
||||
export { RemoveClipEffectCommand } from "./remove-effect";
|
||||
export { ToggleClipEffectCommand } from "./toggle-effect";
|
||||
export { UpdateClipEffectParamsCommand } from "./update-effect-params";
|
||||
export { ReorderClipEffectsCommand } from "./reorder-effect";
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Command } from "@/lib/commands/base-command";
|
||||
import { EditorCore } from "@/core";
|
||||
import { isVisualElement, updateElementInTracks } from "@/lib/timeline";
|
||||
import type { TimelineTrack, VisualElement } from "@/types/timeline";
|
||||
|
||||
function removeEffectFromElement({
|
||||
element,
|
||||
effectId,
|
||||
}: {
|
||||
element: VisualElement;
|
||||
effectId: string;
|
||||
}): VisualElement {
|
||||
const currentEffects = element.effects ?? [];
|
||||
const filtered = currentEffects.filter((effect) => effect.id !== effectId);
|
||||
return { ...element, effects: filtered };
|
||||
}
|
||||
|
||||
export class RemoveClipEffectCommand extends Command {
|
||||
private savedState: TimelineTrack[] | null = null;
|
||||
private readonly trackId: string;
|
||||
private readonly elementId: string;
|
||||
private readonly effectId: string;
|
||||
|
||||
constructor({
|
||||
trackId,
|
||||
elementId,
|
||||
effectId,
|
||||
}: {
|
||||
trackId: string;
|
||||
elementId: string;
|
||||
effectId: string;
|
||||
}) {
|
||||
super();
|
||||
this.trackId = trackId;
|
||||
this.elementId = elementId;
|
||||
this.effectId = effectId;
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const editor = EditorCore.getInstance();
|
||||
this.savedState = editor.timeline.getTracks();
|
||||
|
||||
const updatedTracks = updateElementInTracks({
|
||||
tracks: this.savedState,
|
||||
trackId: this.trackId,
|
||||
elementId: this.elementId,
|
||||
elementPredicate: isVisualElement,
|
||||
update: (element) => {
|
||||
return removeEffectFromElement({
|
||||
element: element as VisualElement,
|
||||
effectId: this.effectId,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
editor.timeline.updateTracks(updatedTracks);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (this.savedState) {
|
||||
const editor = EditorCore.getInstance();
|
||||
editor.timeline.updateTracks(this.savedState);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Command } from "@/lib/commands/base-command";
|
||||
import { EditorCore } from "@/core";
|
||||
import { isVisualElement, updateElementInTracks } from "@/lib/timeline";
|
||||
import type { TimelineTrack, VisualElement } from "@/types/timeline";
|
||||
|
||||
function reorderEffectsOnElement({
|
||||
element,
|
||||
fromIndex,
|
||||
toIndex,
|
||||
}: {
|
||||
element: VisualElement;
|
||||
fromIndex: number;
|
||||
toIndex: number;
|
||||
}): VisualElement {
|
||||
const effects = [...(element.effects ?? [])];
|
||||
const [moved] = effects.splice(fromIndex, 1);
|
||||
effects.splice(toIndex, 0, moved);
|
||||
return { ...element, effects };
|
||||
}
|
||||
|
||||
export class ReorderClipEffectsCommand extends Command {
|
||||
private savedState: TimelineTrack[] | null = null;
|
||||
private readonly trackId: string;
|
||||
private readonly elementId: string;
|
||||
private readonly fromIndex: number;
|
||||
private readonly toIndex: number;
|
||||
|
||||
constructor({
|
||||
trackId,
|
||||
elementId,
|
||||
fromIndex,
|
||||
toIndex,
|
||||
}: {
|
||||
trackId: string;
|
||||
elementId: string;
|
||||
fromIndex: number;
|
||||
toIndex: number;
|
||||
}) {
|
||||
super();
|
||||
this.trackId = trackId;
|
||||
this.elementId = elementId;
|
||||
this.fromIndex = fromIndex;
|
||||
this.toIndex = toIndex;
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const editor = EditorCore.getInstance();
|
||||
this.savedState = editor.timeline.getTracks();
|
||||
|
||||
const updatedTracks = updateElementInTracks({
|
||||
tracks: this.savedState,
|
||||
trackId: this.trackId,
|
||||
elementId: this.elementId,
|
||||
elementPredicate: isVisualElement,
|
||||
update: (element) => {
|
||||
return reorderEffectsOnElement({
|
||||
element: element as VisualElement,
|
||||
fromIndex: this.fromIndex,
|
||||
toIndex: this.toIndex,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
editor.timeline.updateTracks(updatedTracks);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (this.savedState) {
|
||||
const editor = EditorCore.getInstance();
|
||||
editor.timeline.updateTracks(this.savedState);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Command } from "@/lib/commands/base-command";
|
||||
import { EditorCore } from "@/core";
|
||||
import { isVisualElement, updateElementInTracks } from "@/lib/timeline";
|
||||
import type { TimelineTrack, VisualElement } from "@/types/timeline";
|
||||
|
||||
export function toggleEffectOnElement({
|
||||
element,
|
||||
effectId,
|
||||
}: {
|
||||
element: VisualElement;
|
||||
effectId: string;
|
||||
}): VisualElement {
|
||||
const currentEffects = element.effects ?? [];
|
||||
const updated = currentEffects.map((effect) =>
|
||||
effect.id === effectId ? { ...effect, enabled: !effect.enabled } : effect,
|
||||
);
|
||||
return { ...element, effects: updated };
|
||||
}
|
||||
|
||||
export class ToggleClipEffectCommand extends Command {
|
||||
private savedState: TimelineTrack[] | null = null;
|
||||
private readonly trackId: string;
|
||||
private readonly elementId: string;
|
||||
private readonly effectId: string;
|
||||
|
||||
constructor({
|
||||
trackId,
|
||||
elementId,
|
||||
effectId,
|
||||
}: {
|
||||
trackId: string;
|
||||
elementId: string;
|
||||
effectId: string;
|
||||
}) {
|
||||
super();
|
||||
this.trackId = trackId;
|
||||
this.elementId = elementId;
|
||||
this.effectId = effectId;
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const editor = EditorCore.getInstance();
|
||||
this.savedState = editor.timeline.getTracks();
|
||||
|
||||
const updatedTracks = updateElementInTracks({
|
||||
tracks: this.savedState,
|
||||
trackId: this.trackId,
|
||||
elementId: this.elementId,
|
||||
elementPredicate: isVisualElement,
|
||||
update: (element) => {
|
||||
return toggleEffectOnElement({
|
||||
element: element as VisualElement,
|
||||
effectId: this.effectId,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
editor.timeline.updateTracks(updatedTracks);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (this.savedState) {
|
||||
const editor = EditorCore.getInstance();
|
||||
editor.timeline.updateTracks(this.savedState);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Command } from "@/lib/commands/base-command";
|
||||
import { EditorCore } from "@/core";
|
||||
import { isVisualElement, updateElementInTracks } from "@/lib/timeline";
|
||||
import type { EffectParamValues } from "@/types/effects";
|
||||
import type { TimelineTrack, VisualElement } from "@/types/timeline";
|
||||
|
||||
function updateEffectParamsOnElement({
|
||||
element,
|
||||
effectId,
|
||||
params,
|
||||
}: {
|
||||
element: VisualElement;
|
||||
effectId: string;
|
||||
params: Partial<EffectParamValues>;
|
||||
}): VisualElement {
|
||||
const currentEffects = element.effects ?? [];
|
||||
const updated = currentEffects.map((effect) => {
|
||||
if (effect.id !== effectId) {
|
||||
return effect;
|
||||
}
|
||||
|
||||
const nextParams = { ...effect.params };
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value !== undefined) {
|
||||
nextParams[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return { ...effect, params: nextParams };
|
||||
});
|
||||
return { ...element, effects: updated };
|
||||
}
|
||||
|
||||
export class UpdateClipEffectParamsCommand extends Command {
|
||||
private savedState: TimelineTrack[] | null = null;
|
||||
private readonly trackId: string;
|
||||
private readonly elementId: string;
|
||||
private readonly effectId: string;
|
||||
private readonly params: Partial<EffectParamValues>;
|
||||
|
||||
constructor({
|
||||
trackId,
|
||||
elementId,
|
||||
effectId,
|
||||
params,
|
||||
}: {
|
||||
trackId: string;
|
||||
elementId: string;
|
||||
effectId: string;
|
||||
params: Partial<EffectParamValues>;
|
||||
}) {
|
||||
super();
|
||||
this.trackId = trackId;
|
||||
this.elementId = elementId;
|
||||
this.effectId = effectId;
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const editor = EditorCore.getInstance();
|
||||
this.savedState = editor.timeline.getTracks();
|
||||
|
||||
const updatedTracks = updateElementInTracks({
|
||||
tracks: this.savedState,
|
||||
trackId: this.trackId,
|
||||
elementId: this.elementId,
|
||||
elementPredicate: isVisualElement,
|
||||
update: (element) => {
|
||||
return updateEffectParamsOnElement({
|
||||
element: element as VisualElement,
|
||||
effectId: this.effectId,
|
||||
params: this.params,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
editor.timeline.updateTracks(updatedTracks);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (this.savedState) {
|
||||
const editor = EditorCore.getInstance();
|
||||
editor.timeline.updateTracks(this.savedState);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,3 +10,4 @@ export { ToggleElementsVisibilityCommand } from "./toggle-elements-visibility";
|
||||
export { ToggleElementsMutedCommand } from "./toggle-elements-muted";
|
||||
export { MoveElementCommand } from "./move-elements";
|
||||
export * from "./keyframes";
|
||||
export * from "./effects";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from "./remove-effect-param-keyframe";
|
||||
export * from "./remove-keyframe";
|
||||
export * from "./retime-keyframe";
|
||||
export * from "./upsert-effect-param-keyframe";
|
||||
export * from "./upsert-keyframe";
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { EditorCore } from "@/core";
|
||||
import { Command } from "@/lib/commands/base-command";
|
||||
import { removeEffectParamKeyframe } from "@/lib/animation/effect-param-channel";
|
||||
import { updateElementInTracks } from "@/lib/timeline";
|
||||
import { isVisualElement } from "@/lib/timeline/element-utils";
|
||||
import type { TimelineTrack } from "@/types/timeline";
|
||||
|
||||
export class RemoveEffectParamKeyframeCommand extends Command {
|
||||
private savedState: TimelineTrack[] | null = null;
|
||||
private readonly trackId: string;
|
||||
private readonly elementId: string;
|
||||
private readonly effectId: string;
|
||||
private readonly paramKey: string;
|
||||
private readonly keyframeId: string;
|
||||
|
||||
constructor({
|
||||
trackId,
|
||||
elementId,
|
||||
effectId,
|
||||
paramKey,
|
||||
keyframeId,
|
||||
}: {
|
||||
trackId: string;
|
||||
elementId: string;
|
||||
effectId: string;
|
||||
paramKey: string;
|
||||
keyframeId: string;
|
||||
}) {
|
||||
super();
|
||||
this.trackId = trackId;
|
||||
this.elementId = elementId;
|
||||
this.effectId = effectId;
|
||||
this.paramKey = paramKey;
|
||||
this.keyframeId = keyframeId;
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const editor = EditorCore.getInstance();
|
||||
this.savedState = editor.timeline.getTracks();
|
||||
|
||||
const updatedTracks = updateElementInTracks({
|
||||
tracks: this.savedState,
|
||||
trackId: this.trackId,
|
||||
elementId: this.elementId,
|
||||
elementPredicate: isVisualElement,
|
||||
update: (element) => {
|
||||
const animations = removeEffectParamKeyframe({
|
||||
animations: element.animations,
|
||||
effectId: this.effectId,
|
||||
paramKey: this.paramKey,
|
||||
keyframeId: this.keyframeId,
|
||||
});
|
||||
return { ...element, animations };
|
||||
},
|
||||
});
|
||||
|
||||
editor.timeline.updateTracks(updatedTracks);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (!this.savedState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const editor = EditorCore.getInstance();
|
||||
editor.timeline.updateTracks(this.savedState);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { EditorCore } from "@/core";
|
||||
import { Command } from "@/lib/commands/base-command";
|
||||
import { upsertEffectParamKeyframe } from "@/lib/animation/effect-param-channel";
|
||||
import { updateElementInTracks } from "@/lib/timeline";
|
||||
import { isVisualElement } from "@/lib/timeline/element-utils";
|
||||
import type { TimelineTrack } from "@/types/timeline";
|
||||
|
||||
export class UpsertEffectParamKeyframeCommand extends Command {
|
||||
private savedState: TimelineTrack[] | null = null;
|
||||
private readonly trackId: string;
|
||||
private readonly elementId: string;
|
||||
private readonly effectId: string;
|
||||
private readonly paramKey: string;
|
||||
private readonly time: number;
|
||||
private readonly value: number;
|
||||
private readonly interpolation: "linear" | "hold" | undefined;
|
||||
private readonly keyframeId: string | undefined;
|
||||
|
||||
constructor({
|
||||
trackId,
|
||||
elementId,
|
||||
effectId,
|
||||
paramKey,
|
||||
time,
|
||||
value,
|
||||
interpolation,
|
||||
keyframeId,
|
||||
}: {
|
||||
trackId: string;
|
||||
elementId: string;
|
||||
effectId: string;
|
||||
paramKey: string;
|
||||
time: number;
|
||||
value: number;
|
||||
interpolation?: "linear" | "hold";
|
||||
keyframeId?: string;
|
||||
}) {
|
||||
super();
|
||||
this.trackId = trackId;
|
||||
this.elementId = elementId;
|
||||
this.effectId = effectId;
|
||||
this.paramKey = paramKey;
|
||||
this.time = time;
|
||||
this.value = value;
|
||||
this.interpolation = interpolation;
|
||||
this.keyframeId = keyframeId;
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const editor = EditorCore.getInstance();
|
||||
this.savedState = editor.timeline.getTracks();
|
||||
|
||||
const updatedTracks = updateElementInTracks({
|
||||
tracks: this.savedState,
|
||||
trackId: this.trackId,
|
||||
elementId: this.elementId,
|
||||
elementPredicate: isVisualElement,
|
||||
update: (element) => {
|
||||
const boundedTime = Math.max(0, Math.min(this.time, element.duration));
|
||||
const animations = upsertEffectParamKeyframe({
|
||||
animations: element.animations,
|
||||
effectId: this.effectId,
|
||||
paramKey: this.paramKey,
|
||||
time: boundedTime,
|
||||
value: this.value,
|
||||
interpolation: this.interpolation,
|
||||
keyframeId: this.keyframeId,
|
||||
});
|
||||
return { ...element, animations };
|
||||
},
|
||||
});
|
||||
|
||||
editor.timeline.updateTracks(updatedTracks);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (!this.savedState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const editor = EditorCore.getInstance();
|
||||
editor.timeline.updateTracks(this.savedState);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
validateElementTrackCompatibility,
|
||||
enforceMainTrackStart,
|
||||
} from "@/lib/timeline/track-utils";
|
||||
import { rippleShiftElements } from "@/lib/timeline/ripple-utils";
|
||||
|
||||
export class MoveElementCommand extends Command {
|
||||
private savedState: TimelineTrack[] | null = null;
|
||||
@@ -19,6 +20,7 @@ export class MoveElementCommand extends Command {
|
||||
private readonly elementId: string;
|
||||
private readonly newStartTime: number;
|
||||
private readonly createTrack: { type: TrackType; index: number } | undefined;
|
||||
private readonly rippleEnabled: boolean;
|
||||
|
||||
constructor({
|
||||
sourceTrackId,
|
||||
@@ -26,12 +28,14 @@ export class MoveElementCommand extends Command {
|
||||
elementId,
|
||||
newStartTime,
|
||||
createTrack,
|
||||
rippleEnabled = false,
|
||||
}: {
|
||||
sourceTrackId: string;
|
||||
targetTrackId: string;
|
||||
elementId: string;
|
||||
newStartTime: number;
|
||||
createTrack?: { type: TrackType; index: number };
|
||||
rippleEnabled?: boolean;
|
||||
}) {
|
||||
super();
|
||||
this.sourceTrackId = sourceTrackId;
|
||||
@@ -39,6 +43,7 @@ export class MoveElementCommand extends Command {
|
||||
this.elementId = elementId;
|
||||
this.newStartTime = newStartTime;
|
||||
this.createTrack = createTrack;
|
||||
this.rippleEnabled = rippleEnabled;
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
@@ -53,8 +58,7 @@ export class MoveElementCommand extends Command {
|
||||
);
|
||||
|
||||
if (!sourceTrack || !element) {
|
||||
console.error("Source track or element not found");
|
||||
return;
|
||||
throw new Error("Source track or element not found");
|
||||
}
|
||||
|
||||
let targetTrack = this.savedState.find((track) => track.id === this.targetTrackId);
|
||||
@@ -69,8 +73,7 @@ export class MoveElementCommand extends Command {
|
||||
targetTrack = newTrack;
|
||||
}
|
||||
if (!targetTrack) {
|
||||
console.error("Target track not found");
|
||||
return;
|
||||
throw new Error("Target track not found");
|
||||
}
|
||||
|
||||
const validation = validateElementTrackCompatibility({
|
||||
@@ -79,8 +82,7 @@ export class MoveElementCommand extends Command {
|
||||
});
|
||||
|
||||
if (!validation.isValid) {
|
||||
console.error(validation.errorMessage);
|
||||
return;
|
||||
throw new Error(validation.errorMessage);
|
||||
}
|
||||
|
||||
const adjustedStartTime = enforceMainTrackStart({
|
||||
@@ -105,27 +107,32 @@ export class MoveElementCommand extends Command {
|
||||
elements: track.elements.map((trackElement) =>
|
||||
trackElement.id === this.elementId ? movedElement : trackElement,
|
||||
),
|
||||
};
|
||||
} as typeof track;
|
||||
}
|
||||
|
||||
if (track.id === this.sourceTrackId) {
|
||||
return {
|
||||
...track,
|
||||
elements: track.elements.filter(
|
||||
(trackElement) => trackElement.id !== this.elementId,
|
||||
),
|
||||
};
|
||||
const remainingElements = track.elements.filter(
|
||||
(trackElement) => trackElement.id !== this.elementId,
|
||||
);
|
||||
const shiftedElements = this.rippleEnabled
|
||||
? rippleShiftElements({
|
||||
elements: remainingElements,
|
||||
afterTime: element.startTime,
|
||||
shiftAmount: element.duration,
|
||||
})
|
||||
: remainingElements;
|
||||
return { ...track, elements: shiftedElements } as typeof track;
|
||||
}
|
||||
|
||||
if (track.id === this.targetTrackId) {
|
||||
return {
|
||||
...track,
|
||||
elements: [...track.elements, movedElement],
|
||||
};
|
||||
} as typeof track;
|
||||
}
|
||||
|
||||
return track;
|
||||
});
|
||||
return track;
|
||||
});
|
||||
|
||||
if (!isSameTrack) {
|
||||
const sourceTrackAfterMove = updatedTracks.find(
|
||||
|
||||
Reference in New Issue
Block a user