mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
shipping this slop
This commit is contained in:
@@ -3,8 +3,6 @@
|
||||
import { useTimelineStore } from "@/stores/timeline-store";
|
||||
import { useActionHandler } from "@/hooks/actions/use-action-handler";
|
||||
import { useEditor } from "../use-editor";
|
||||
import { PasteCommand } from "@/lib/commands/timeline/clipboard/paste";
|
||||
import { toast } from "sonner";
|
||||
import { useElementSelection } from "../timeline/element/use-element-selection";
|
||||
|
||||
export function useEditorActions() {
|
||||
@@ -126,14 +124,10 @@ export function useEditorActions() {
|
||||
useActionHandler(
|
||||
"split-selected",
|
||||
() => {
|
||||
const splitElementIds = editor.timeline.splitElements({
|
||||
editor.timeline.splitElements({
|
||||
elements: selectedElements,
|
||||
splitTime: editor.playback.getCurrentTime(),
|
||||
});
|
||||
|
||||
if (splitElementIds.length === 0) {
|
||||
toast.error("Playhead must be positioned over the selected element(s)");
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
@@ -141,15 +135,11 @@ export function useEditorActions() {
|
||||
useActionHandler(
|
||||
"split-selected-left",
|
||||
() => {
|
||||
const splitElementIds = editor.timeline.splitElements({
|
||||
editor.timeline.splitElements({
|
||||
elements: selectedElements,
|
||||
splitTime: editor.playback.getCurrentTime(),
|
||||
retainSide: "left",
|
||||
});
|
||||
|
||||
if (splitElementIds.length === 0) {
|
||||
toast.error("Playhead must be positioned over the selected element(s)");
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
@@ -157,15 +147,11 @@ export function useEditorActions() {
|
||||
useActionHandler(
|
||||
"split-selected-right",
|
||||
() => {
|
||||
const splitElementIds = editor.timeline.splitElements({
|
||||
editor.timeline.splitElements({
|
||||
elements: selectedElements,
|
||||
splitTime: editor.playback.getCurrentTime(),
|
||||
retainSide: "right",
|
||||
});
|
||||
|
||||
if (splitElementIds.length === 0) {
|
||||
toast.error("Playhead must be positioned over the selected element(s)");
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
@@ -240,6 +226,7 @@ export function useEditorActions() {
|
||||
const items = results.map(({ track, element }) => {
|
||||
const { id, ...elementWithoutId } = element;
|
||||
return {
|
||||
trackId: track.id,
|
||||
trackType: track.type,
|
||||
element: elementWithoutId,
|
||||
};
|
||||
@@ -255,9 +242,9 @@ export function useEditorActions() {
|
||||
() => {
|
||||
if (!clipboard?.items.length) return;
|
||||
|
||||
const currentTime = editor.playback.getCurrentTime();
|
||||
editor.command.execute({
|
||||
command: new PasteCommand(currentTime, clipboard.items),
|
||||
editor.timeline.pasteAtTime({
|
||||
time: editor.playback.getCurrentTime(),
|
||||
clipboardItems: clipboard.items,
|
||||
});
|
||||
},
|
||||
undefined,
|
||||
|
||||
@@ -12,6 +12,7 @@ import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
||||
import { snapTimeToFrame } from "@/lib/time-utils";
|
||||
import { computeDropTarget } from "@/lib/timeline/drop-utils";
|
||||
import { generateUUID } from "@/lib/utils";
|
||||
import { useTimelineSnapping } from "@/hooks/timeline/use-timeline-snapping";
|
||||
import type {
|
||||
DropTarget,
|
||||
ElementDragState,
|
||||
@@ -27,6 +28,7 @@ interface UseElementInteractionProps {
|
||||
timelineRef: RefObject<HTMLDivElement | null>;
|
||||
tracksContainerRef: RefObject<HTMLDivElement | null>;
|
||||
tracksScrollRef: RefObject<HTMLDivElement | null>;
|
||||
snappingEnabled: boolean;
|
||||
onSnapPointChange?: (snapPoint: SnapPoint | null) => void;
|
||||
}
|
||||
|
||||
@@ -82,8 +84,16 @@ function getClickOffsetTime({
|
||||
return clickOffsetX / (TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel);
|
||||
}
|
||||
|
||||
function getElementDuration({ element }: { element: TimelineElement }): number {
|
||||
return element.duration - element.trimStart - element.trimEnd;
|
||||
function getVerticalDragDirection({
|
||||
startMouseY,
|
||||
currentMouseY,
|
||||
}: {
|
||||
startMouseY: number;
|
||||
currentMouseY: number;
|
||||
}): "up" | "down" | null {
|
||||
if (currentMouseY < startMouseY) return "up";
|
||||
if (currentMouseY > startMouseY) return "down";
|
||||
return null;
|
||||
}
|
||||
|
||||
function getDragDropTarget({
|
||||
@@ -96,6 +106,7 @@ function getDragDropTarget({
|
||||
tracksScrollRef,
|
||||
zoomLevel,
|
||||
snappedTime,
|
||||
verticalDragDirection,
|
||||
}: {
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
@@ -106,6 +117,7 @@ function getDragDropTarget({
|
||||
tracksScrollRef: RefObject<HTMLDivElement | null>;
|
||||
zoomLevel: number;
|
||||
snappedTime: number;
|
||||
verticalDragDirection?: "up" | "down" | null;
|
||||
}): DropTarget | null {
|
||||
const containerRect = tracksContainerRef.current?.getBoundingClientRect();
|
||||
const scrollContainer = tracksScrollRef.current;
|
||||
@@ -115,7 +127,7 @@ function getDragDropTarget({
|
||||
const movingElement = sourceTrack?.elements.find(({ id }) => id === elementId);
|
||||
if (!movingElement) return null;
|
||||
|
||||
const elementDuration = getElementDuration({ element: movingElement });
|
||||
const elementDuration = movingElement.duration;
|
||||
const scrollLeft = scrollContainer.scrollLeft;
|
||||
const scrollContainerRect = scrollContainer.getBoundingClientRect();
|
||||
const mouseX = clientX - scrollContainerRect.left + scrollLeft;
|
||||
@@ -133,6 +145,7 @@ function getDragDropTarget({
|
||||
zoomLevel,
|
||||
startTimeOverride: snappedTime,
|
||||
excludeElementId: movingElement.id,
|
||||
verticalDragDirection,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -147,10 +160,12 @@ export function useElementInteraction({
|
||||
timelineRef,
|
||||
tracksContainerRef,
|
||||
tracksScrollRef,
|
||||
snappingEnabled,
|
||||
onSnapPointChange,
|
||||
}: UseElementInteractionProps) {
|
||||
const editor = useEditor();
|
||||
const tracks = editor.timeline.getTracks();
|
||||
const { snapElementEdge } = useTimelineSnapping();
|
||||
const {
|
||||
isElementSelected,
|
||||
selectElement,
|
||||
@@ -196,6 +211,55 @@ export function useElementInteraction({
|
||||
setDragDropTarget(null);
|
||||
}, []);
|
||||
|
||||
const getDragSnapResult = useCallback(
|
||||
({
|
||||
frameSnappedTime,
|
||||
movingElement,
|
||||
}: {
|
||||
frameSnappedTime: number;
|
||||
movingElement: TimelineElement | null | undefined;
|
||||
}) => {
|
||||
if (!snappingEnabled || !movingElement) {
|
||||
return { snappedTime: frameSnappedTime, snapPoint: null };
|
||||
}
|
||||
|
||||
const elementDuration = movingElement.duration;
|
||||
const playheadTime = editor.playback.getCurrentTime();
|
||||
|
||||
const startSnap = snapElementEdge({
|
||||
targetTime: frameSnappedTime,
|
||||
elementDuration,
|
||||
tracks,
|
||||
playheadTime,
|
||||
zoomLevel,
|
||||
excludeElementId: movingElement.id,
|
||||
snapToStart: true,
|
||||
});
|
||||
|
||||
const endSnap = snapElementEdge({
|
||||
targetTime: frameSnappedTime,
|
||||
elementDuration,
|
||||
tracks,
|
||||
playheadTime,
|
||||
zoomLevel,
|
||||
excludeElementId: movingElement.id,
|
||||
snapToStart: false,
|
||||
});
|
||||
|
||||
const snapResult =
|
||||
startSnap.snapDistance <= endSnap.snapDistance ? startSnap : endSnap;
|
||||
if (!snapResult.snapPoint) {
|
||||
return { snappedTime: frameSnappedTime, snapPoint: null };
|
||||
}
|
||||
|
||||
return {
|
||||
snappedTime: snapResult.snappedTime,
|
||||
snapPoint: snapResult.snapPoint,
|
||||
};
|
||||
},
|
||||
[snappingEnabled, editor.playback, snapElementEdge, tracks, zoomLevel],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dragState.isDragging && !isPendingDrag) return;
|
||||
|
||||
@@ -269,14 +333,28 @@ export function useElementInteraction({
|
||||
});
|
||||
const adjustedTime = Math.max(0, mouseTime - dragState.clickOffsetTime);
|
||||
const fps = activeProject.settings.fps;
|
||||
const snappedTime = snapTimeToFrame({ time: adjustedTime, fps });
|
||||
const frameSnappedTime = snapTimeToFrame({ time: adjustedTime, fps });
|
||||
|
||||
const sourceTrack = tracks.find(({ id }) => id === dragState.trackId);
|
||||
const movingElement = sourceTrack?.elements.find(
|
||||
({ id }) => id === dragState.elementId,
|
||||
);
|
||||
const { snappedTime, snapPoint } = getDragSnapResult({
|
||||
frameSnappedTime,
|
||||
movingElement,
|
||||
});
|
||||
setDragState((previousDragState) => ({
|
||||
...previousDragState,
|
||||
currentTime: snappedTime,
|
||||
currentMouseY: clientY,
|
||||
}));
|
||||
onSnapPointChange?.(snapPoint);
|
||||
|
||||
if (dragState.elementId && dragState.trackId) {
|
||||
const verticalDragDirection = getVerticalDragDirection({
|
||||
startMouseY: dragState.startMouseY,
|
||||
currentMouseY: clientY,
|
||||
});
|
||||
const dropTarget = getDragDropTarget({
|
||||
clientX,
|
||||
clientY,
|
||||
@@ -287,6 +365,7 @@ export function useElementInteraction({
|
||||
tracksScrollRef,
|
||||
zoomLevel,
|
||||
snappedTime,
|
||||
verticalDragDirection,
|
||||
});
|
||||
setDragDropTarget(dropTarget?.isNewTrack ? dropTarget : null);
|
||||
}
|
||||
@@ -303,12 +382,15 @@ export function useElementInteraction({
|
||||
isElementSelected,
|
||||
selectElement,
|
||||
editor.project,
|
||||
editor.playback,
|
||||
timelineRef,
|
||||
tracksScrollRef,
|
||||
tracksContainerRef,
|
||||
tracks,
|
||||
isPendingDrag,
|
||||
startDrag,
|
||||
getDragSnapResult,
|
||||
onSnapPointChange,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -338,6 +420,10 @@ export function useElementInteraction({
|
||||
tracksScrollRef,
|
||||
zoomLevel,
|
||||
snappedTime: dragState.currentTime,
|
||||
verticalDragDirection: getVerticalDragDirection({
|
||||
startMouseY: dragState.startMouseY,
|
||||
currentMouseY: clientY,
|
||||
}),
|
||||
});
|
||||
if (!dropTarget) {
|
||||
endDrag();
|
||||
|
||||
@@ -2,6 +2,11 @@ import { useState, useEffect, useRef } from "react";
|
||||
import { TimelineElement, TimelineTrack } from "@/types/timeline";
|
||||
import { snapTimeToFrame } from "@/lib/time-utils";
|
||||
import { EditorCore } from "@/core";
|
||||
import {
|
||||
useTimelineSnapping,
|
||||
type SnapPoint,
|
||||
} from "@/hooks/timeline/use-timeline-snapping";
|
||||
import { useTimelineStore } from "@/stores/timeline-store";
|
||||
|
||||
export interface ResizeState {
|
||||
elementId: string;
|
||||
@@ -17,15 +22,21 @@ interface UseTimelineElementResizeProps {
|
||||
element: TimelineElement;
|
||||
track: TimelineTrack;
|
||||
zoomLevel: number;
|
||||
onSnapPointChange?: (snapPoint: SnapPoint | null) => void;
|
||||
onResizeStateChange?: (params: { isResizing: boolean }) => void;
|
||||
}
|
||||
|
||||
export function useTimelineElementResize({
|
||||
element,
|
||||
track,
|
||||
zoomLevel,
|
||||
onSnapPointChange,
|
||||
onResizeStateChange,
|
||||
}: UseTimelineElementResizeProps) {
|
||||
const editor = EditorCore.getInstance();
|
||||
const activeProject = editor.project.getActive();
|
||||
const snappingEnabled = useTimelineStore((state) => state.snappingEnabled);
|
||||
const { findSnapPoints, snapToNearestPoint } = useTimelineSnapping();
|
||||
|
||||
const [resizing, setResizing] = useState<ResizeState | null>(null);
|
||||
const [currentTrimStart, setCurrentTrimStart] = useState(element.trimStart);
|
||||
@@ -87,6 +98,7 @@ export function useTimelineElementResize({
|
||||
currentTrimEndRef.current = element.trimEnd;
|
||||
currentStartTimeRef.current = element.startTime;
|
||||
currentDurationRef.current = element.duration;
|
||||
onResizeStateChange?.({ isResizing: true });
|
||||
};
|
||||
|
||||
const canExtendElementDuration = () => {
|
||||
@@ -101,16 +113,55 @@ export function useTimelineElementResize({
|
||||
if (!resizing) return;
|
||||
|
||||
const deltaX = clientX - resizing.startX;
|
||||
const deltaTime = deltaX / (50 * zoomLevel);
|
||||
let deltaTime = deltaX / (50 * zoomLevel);
|
||||
let resizeSnapPoint: SnapPoint | null = null;
|
||||
|
||||
const projectFps = activeProject.settings.fps;
|
||||
const minDurationSeconds = 1 / projectFps;
|
||||
const canSnap = snappingEnabled;
|
||||
if (canSnap) {
|
||||
const tracks = editor.timeline.getTracks();
|
||||
const playheadTime = editor.playback.getCurrentTime();
|
||||
const snapPoints = findSnapPoints({
|
||||
tracks,
|
||||
playheadTime,
|
||||
excludeElementId: element.id,
|
||||
});
|
||||
if (resizing.side === "left") {
|
||||
const targetStartTime = resizing.initialStartTime + deltaTime;
|
||||
const snapResult = snapToNearestPoint({
|
||||
targetTime: targetStartTime,
|
||||
snapPoints,
|
||||
zoomLevel,
|
||||
});
|
||||
resizeSnapPoint = snapResult.snapPoint;
|
||||
if (snapResult.snapPoint) {
|
||||
deltaTime = snapResult.snappedTime - resizing.initialStartTime;
|
||||
}
|
||||
} else {
|
||||
const baseEndTime =
|
||||
resizing.initialStartTime + resizing.initialDuration;
|
||||
const targetEndTime = baseEndTime + deltaTime;
|
||||
const snapResult = snapToNearestPoint({
|
||||
targetTime: targetEndTime,
|
||||
snapPoints,
|
||||
zoomLevel,
|
||||
});
|
||||
resizeSnapPoint = snapResult.snapPoint;
|
||||
if (snapResult.snapPoint) {
|
||||
deltaTime = snapResult.snappedTime - baseEndTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
onSnapPointChange?.(resizeSnapPoint);
|
||||
|
||||
if (resizing.side === "left") {
|
||||
const sourceDuration =
|
||||
resizing.initialTrimStart +
|
||||
resizing.initialDuration +
|
||||
resizing.initialTrimEnd;
|
||||
const maxAllowed = sourceDuration - resizing.initialTrimEnd - 0.1;
|
||||
const maxAllowed =
|
||||
sourceDuration - resizing.initialTrimEnd - minDurationSeconds;
|
||||
const calculated = resizing.initialTrimStart + deltaTime;
|
||||
|
||||
if (calculated >= 0 && calculated <= maxAllowed) {
|
||||
@@ -183,8 +234,9 @@ export function useTimelineElementResize({
|
||||
if (newTrimEnd < 0) {
|
||||
if (canExtendElementDuration()) {
|
||||
const extensionNeeded = Math.abs(newTrimEnd);
|
||||
const baseDuration = resizing.initialDuration + resizing.initialTrimEnd;
|
||||
const newDuration = snapTimeToFrame({
|
||||
time: resizing.initialDuration + extensionNeeded,
|
||||
time: baseDuration + extensionNeeded,
|
||||
fps: projectFps,
|
||||
});
|
||||
|
||||
@@ -205,7 +257,8 @@ export function useTimelineElementResize({
|
||||
currentTrimEndRef.current = 0;
|
||||
}
|
||||
} else {
|
||||
const maxTrimEnd = sourceDuration - resizing.initialTrimStart - 0.1;
|
||||
const maxTrimEnd =
|
||||
sourceDuration - resizing.initialTrimStart - minDurationSeconds;
|
||||
const clampedTrimEnd = Math.min(maxTrimEnd, Math.max(0, newTrimEnd));
|
||||
const finalTrimEnd = snapTimeToFrame({
|
||||
time: clampedTrimEnd,
|
||||
@@ -261,6 +314,8 @@ export function useTimelineElementResize({
|
||||
}
|
||||
|
||||
setResizing(null);
|
||||
onResizeStateChange?.({ isResizing: false });
|
||||
onSnapPointChange?.(null);
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -208,7 +208,7 @@ export function useSelectionBox({
|
||||
}, [selectionBox, selectElementsInBox]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectionBox?.isActive) return;
|
||||
if (!selectionBox) return;
|
||||
|
||||
const previousBodyUserSelect = document.body.style.userSelect;
|
||||
const container = containerRef.current;
|
||||
@@ -221,7 +221,7 @@ export function useSelectionBox({
|
||||
document.body.style.userSelect = previousBodyUserSelect;
|
||||
if (container) container.style.userSelect = previousContainerUserSelect;
|
||||
};
|
||||
}, [selectionBox?.isActive, containerRef]);
|
||||
}, [selectionBox, containerRef]);
|
||||
|
||||
return {
|
||||
selectionBox,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useCallback, type RefObject } from "react";
|
||||
import { useEditor } from "@/hooks/use-editor";
|
||||
import { processMediaAssets } from "@/lib/media-processing-utils";
|
||||
import { processMediaAssets } from "@/lib/media/processing";
|
||||
import { toast } from "sonner";
|
||||
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
||||
import { snapTimeToFrame } from "@/lib/time-utils";
|
||||
@@ -98,9 +98,10 @@ export function useTimelineDragDrop({
|
||||
|
||||
let elementType = getElementType({ dataTransfer: e.dataTransfer });
|
||||
|
||||
// external drops default to video until determined on drop
|
||||
if (!elementType && hasFiles) {
|
||||
elementType = "video";
|
||||
if (!elementType && hasFiles && isExternal) {
|
||||
setDropTarget(null);
|
||||
setElementType(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!elementType) return;
|
||||
@@ -194,7 +195,10 @@ export function useTimelineDragDrop({
|
||||
startTime: target.xPosition,
|
||||
});
|
||||
|
||||
editor.timeline.addElementToTrack({ trackId, element });
|
||||
editor.timeline.insertElement({
|
||||
placement: { mode: "explicit", trackId },
|
||||
element,
|
||||
});
|
||||
},
|
||||
[editor.timeline, tracks],
|
||||
);
|
||||
@@ -225,7 +229,10 @@ export function useTimelineDragDrop({
|
||||
startTime: target.xPosition,
|
||||
});
|
||||
|
||||
editor.timeline.addElementToTrack({ trackId, element });
|
||||
editor.timeline.insertElement({
|
||||
placement: { mode: "explicit", trackId },
|
||||
element,
|
||||
});
|
||||
},
|
||||
[editor.timeline, tracks],
|
||||
);
|
||||
@@ -254,8 +261,8 @@ export function useTimelineDragDrop({
|
||||
mediaAsset.duration ?? TIMELINE_CONSTANTS.DEFAULT_ELEMENT_DURATION;
|
||||
|
||||
if (dragData.mediaType === "audio") {
|
||||
editor.timeline.addElementToTrack({
|
||||
trackId,
|
||||
editor.timeline.insertElement({
|
||||
placement: { mode: "explicit", trackId },
|
||||
element: {
|
||||
type: "audio",
|
||||
sourceType: "upload",
|
||||
@@ -266,13 +273,12 @@ export function useTimelineDragDrop({
|
||||
trimStart: 0,
|
||||
trimEnd: 0,
|
||||
volume: 1,
|
||||
buffer: new AudioBuffer({ length: 1, sampleRate: 44100 }),
|
||||
muted: false,
|
||||
},
|
||||
});
|
||||
} else if (dragData.mediaType === "video") {
|
||||
editor.timeline.addElementToTrack({
|
||||
trackId,
|
||||
editor.timeline.insertElement({
|
||||
placement: { mode: "explicit", trackId },
|
||||
element: {
|
||||
type: "video",
|
||||
mediaId: mediaAsset.id,
|
||||
@@ -293,8 +299,8 @@ export function useTimelineDragDrop({
|
||||
},
|
||||
});
|
||||
} else {
|
||||
editor.timeline.addElementToTrack({
|
||||
trackId,
|
||||
editor.timeline.insertElement({
|
||||
placement: { mode: "explicit", trackId },
|
||||
element: {
|
||||
type: "image",
|
||||
mediaId: mediaAsset.id,
|
||||
@@ -320,7 +326,15 @@ export function useTimelineDragDrop({
|
||||
);
|
||||
|
||||
const executeFileDrop = useCallback(
|
||||
async ({ files }: { files: File[] }) => {
|
||||
async ({
|
||||
files,
|
||||
mouseX,
|
||||
mouseY,
|
||||
}: {
|
||||
files: File[];
|
||||
mouseX: number;
|
||||
mouseY: number;
|
||||
}) => {
|
||||
if (!activeProject) return;
|
||||
|
||||
const processedAssets = await processMediaAssets({ files });
|
||||
@@ -336,26 +350,42 @@ export function useTimelineDragDrop({
|
||||
.find((m) => m.name === asset.name && m.url === asset.url);
|
||||
|
||||
if (added) {
|
||||
const trackType: TrackType =
|
||||
added.type === "audio" ? "audio" : "video";
|
||||
const trackId = editor.timeline.addTrack({
|
||||
type: trackType,
|
||||
index: 0,
|
||||
});
|
||||
|
||||
const duration =
|
||||
added.duration ?? TIMELINE_CONSTANTS.DEFAULT_ELEMENT_DURATION;
|
||||
const currentTracks = editor.timeline.getTracks();
|
||||
const dropTarget = computeDropTarget({
|
||||
elementType: added.type,
|
||||
mouseX,
|
||||
mouseY,
|
||||
tracks: currentTracks,
|
||||
playheadTime: currentTime,
|
||||
isExternalDrop: true,
|
||||
elementDuration: duration,
|
||||
pixelsPerSecond: TIMELINE_CONSTANTS.PIXELS_PER_SECOND,
|
||||
zoomLevel,
|
||||
});
|
||||
|
||||
const trackType: TrackType =
|
||||
added.type === "audio" ? "audio" : "video";
|
||||
const trackId = dropTarget.isNewTrack
|
||||
? editor.timeline.addTrack({
|
||||
type: trackType,
|
||||
index: dropTarget.trackIndex,
|
||||
})
|
||||
: currentTracks[dropTarget.trackIndex]?.id;
|
||||
|
||||
if (!trackId) return;
|
||||
|
||||
if (added.type === "audio") {
|
||||
editor.timeline.addElementToTrack({
|
||||
trackId,
|
||||
editor.timeline.insertElement({
|
||||
placement: { mode: "explicit", trackId },
|
||||
element: {
|
||||
type: "audio",
|
||||
sourceType: "upload",
|
||||
mediaId: added.id,
|
||||
name: added.name,
|
||||
duration,
|
||||
startTime: currentTime,
|
||||
startTime: dropTarget.xPosition,
|
||||
trimStart: 0,
|
||||
trimEnd: 0,
|
||||
volume: 1,
|
||||
@@ -364,14 +394,14 @@ export function useTimelineDragDrop({
|
||||
},
|
||||
});
|
||||
} else if (added.type === "video") {
|
||||
editor.timeline.addElementToTrack({
|
||||
trackId,
|
||||
editor.timeline.insertElement({
|
||||
placement: { mode: "explicit", trackId },
|
||||
element: {
|
||||
type: "video",
|
||||
mediaId: added.id,
|
||||
name: added.name,
|
||||
duration,
|
||||
startTime: currentTime,
|
||||
startTime: dropTarget.xPosition,
|
||||
trimStart: 0,
|
||||
trimEnd: 0,
|
||||
transform: {
|
||||
@@ -386,14 +416,14 @@ export function useTimelineDragDrop({
|
||||
},
|
||||
});
|
||||
} else {
|
||||
editor.timeline.addElementToTrack({
|
||||
trackId,
|
||||
editor.timeline.insertElement({
|
||||
placement: { mode: "explicit", trackId },
|
||||
element: {
|
||||
type: "image",
|
||||
mediaId: added.id,
|
||||
name: added.name,
|
||||
duration,
|
||||
startTime: currentTime,
|
||||
startTime: dropTarget.xPosition,
|
||||
trimStart: 0,
|
||||
trimEnd: 0,
|
||||
transform: {
|
||||
@@ -411,27 +441,32 @@ export function useTimelineDragDrop({
|
||||
}
|
||||
}
|
||||
},
|
||||
[activeProject, editor.media, editor.timeline, currentTime],
|
||||
[
|
||||
activeProject,
|
||||
editor.media,
|
||||
editor.timeline,
|
||||
currentTime,
|
||||
zoomLevel,
|
||||
],
|
||||
);
|
||||
|
||||
const handleDrop = useCallback(
|
||||
async (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const currentTarget = dropTarget;
|
||||
setIsDragOver(false);
|
||||
setDropTarget(null);
|
||||
setElementType(null);
|
||||
|
||||
if (!currentTarget) return;
|
||||
|
||||
const hasAsset = hasDragData({ dataTransfer: e.dataTransfer });
|
||||
const hasFiles = e.dataTransfer.files?.length > 0;
|
||||
|
||||
if (!hasAsset && !hasFiles) return;
|
||||
|
||||
const currentTarget = dropTarget;
|
||||
setIsDragOver(false);
|
||||
setDropTarget(null);
|
||||
setElementType(null);
|
||||
|
||||
try {
|
||||
if (hasAsset) {
|
||||
if (!currentTarget) return;
|
||||
const dragData = getDragData({ dataTransfer: e.dataTransfer });
|
||||
if (!dragData) return;
|
||||
|
||||
@@ -443,7 +478,15 @@ export function useTimelineDragDrop({
|
||||
executeMediaDrop({ target: currentTarget, dragData });
|
||||
}
|
||||
} else if (hasFiles) {
|
||||
await executeFileDrop({ files: Array.from(e.dataTransfer.files) });
|
||||
const rect = containerRef.current?.getBoundingClientRect();
|
||||
if (!rect) return;
|
||||
const mouseX = e.clientX - rect.left;
|
||||
const mouseY = e.clientY - rect.top;
|
||||
await executeFileDrop({
|
||||
files: Array.from(e.dataTransfer.files),
|
||||
mouseX,
|
||||
mouseY,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to process drop:", err);
|
||||
|
||||
@@ -89,20 +89,18 @@ export function useTimelineInteractions({
|
||||
({ event }: { event: React.MouseEvent }) => {
|
||||
const target = event.target as HTMLElement;
|
||||
const { isMouseDown, downX, downY, downTime } = mouseTrackingRef.current;
|
||||
|
||||
if (!isMouseDown) return false;
|
||||
|
||||
const deltaX = Math.abs(event.clientX - downX);
|
||||
const deltaY = Math.abs(event.clientY - downY);
|
||||
const deltaTime = event.timeStamp - downTime;
|
||||
const isPlayhead = !!playheadRef.current?.contains(target);
|
||||
const isTrackLabels = !!trackLabelsRef.current?.contains(target);
|
||||
const shouldBlockForDrag = deltaX > 5 || deltaY > 5 || deltaTime > 500;
|
||||
|
||||
if (deltaX > 5 || deltaY > 5 || deltaTime > 500) return false;
|
||||
|
||||
if (!isMouseDown) return false;
|
||||
if (shouldBlockForDrag) return false;
|
||||
if (isSelecting) return false;
|
||||
|
||||
if (playheadRef.current?.contains(target)) return false;
|
||||
|
||||
if (trackLabelsRef.current?.contains(target)) {
|
||||
if (isPlayhead) return false;
|
||||
if (isTrackLabels) {
|
||||
clearSelectedElements();
|
||||
return false;
|
||||
}
|
||||
@@ -134,7 +132,7 @@ export function useTimelineInteractions({
|
||||
Math.min(
|
||||
duration,
|
||||
(mouseX + scrollLeft) /
|
||||
(TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel),
|
||||
(TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -158,9 +156,10 @@ export function useTimelineInteractions({
|
||||
|
||||
const handleTracksClick = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
const shouldProcess = shouldProcessTimelineClick({ event });
|
||||
resetMouseTracking({ mouseTrackingRef });
|
||||
|
||||
if (shouldProcessTimelineClick({ event })) {
|
||||
if (shouldProcess) {
|
||||
clearSelectedElements();
|
||||
handleTimelineSeek({ event, source: "tracks" });
|
||||
}
|
||||
@@ -170,9 +169,10 @@ export function useTimelineInteractions({
|
||||
|
||||
const handleRulerClick = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
const shouldProcess = shouldProcessTimelineClick({ event });
|
||||
resetMouseTracking({ mouseTrackingRef });
|
||||
|
||||
if (shouldProcessTimelineClick({ event })) {
|
||||
if (shouldProcess) {
|
||||
clearSelectedElements();
|
||||
handleTimelineSeek({ event, source: "ruler" });
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
||||
interface UseTimelineZoomProps {
|
||||
containerRef: RefObject<HTMLDivElement>;
|
||||
isInTimeline?: boolean;
|
||||
minZoom?: number;
|
||||
}
|
||||
|
||||
interface UseTimelineZoomReturn {
|
||||
@@ -21,6 +22,7 @@ interface UseTimelineZoomReturn {
|
||||
export function useTimelineZoom({
|
||||
containerRef,
|
||||
isInTimeline = false,
|
||||
minZoom = TIMELINE_CONSTANTS.ZOOM_MIN,
|
||||
}: UseTimelineZoomProps): UseTimelineZoomReturn {
|
||||
const [zoomLevel, setZoomLevel] = useState(1);
|
||||
|
||||
@@ -38,7 +40,7 @@ export function useTimelineZoom({
|
||||
const zoomMultiplier = event.deltaY > 0 ? 1 / 1.1 : 1.1;
|
||||
setZoomLevel((prev) => {
|
||||
const nextZoom = Math.max(
|
||||
TIMELINE_CONSTANTS.ZOOM_MIN,
|
||||
minZoom,
|
||||
Math.min(TIMELINE_CONSTANTS.ZOOM_MAX, prev * zoomMultiplier),
|
||||
);
|
||||
return nextZoom;
|
||||
@@ -47,7 +49,11 @@ export function useTimelineZoom({
|
||||
// let the event bubble up to allow ScrollArea to handle it
|
||||
return;
|
||||
}
|
||||
}, []);
|
||||
}, [minZoom]);
|
||||
|
||||
useEffect(() => {
|
||||
setZoomLevel((prev) => (prev < minZoom ? minZoom : prev));
|
||||
}, [minZoom]);
|
||||
|
||||
// prevent browser zoom in the timeline
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user