mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
150 lines
3.9 KiB
TypeScript
150 lines
3.9 KiB
TypeScript
import { useCallback, useRef } from "react";
|
|
import { useProjectStore } from "@/stores/project-store";
|
|
import type { RefObject } from "react";
|
|
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
|
import { snapTimeToFrame } from "@/lib/time-utils";
|
|
|
|
interface UseTimelineInteractionsProps {
|
|
playheadRef: RefObject<HTMLDivElement>;
|
|
tracksContainerRef: RefObject<HTMLDivElement>;
|
|
rulerScrollRef: RefObject<HTMLDivElement>;
|
|
tracksScrollRef: RefObject<HTMLDivElement>;
|
|
zoomLevel: number;
|
|
duration: number;
|
|
isSelecting: boolean;
|
|
justFinishedSelecting: boolean;
|
|
clearSelectedElements: () => void;
|
|
seek: (time: number) => void;
|
|
}
|
|
|
|
export function useTimelineInteractions({
|
|
playheadRef,
|
|
tracksContainerRef,
|
|
rulerScrollRef,
|
|
tracksScrollRef,
|
|
zoomLevel,
|
|
duration,
|
|
isSelecting,
|
|
justFinishedSelecting,
|
|
clearSelectedElements,
|
|
seek,
|
|
}: UseTimelineInteractionsProps) {
|
|
const { activeProject } = useProjectStore();
|
|
const mouseTrackingRef = useRef({
|
|
isMouseDown: false,
|
|
downX: 0,
|
|
downY: 0,
|
|
downTime: 0,
|
|
});
|
|
|
|
const handleTimelineMouseDown = useCallback(
|
|
(e: React.MouseEvent) => {
|
|
const target = e.target as HTMLElement;
|
|
|
|
const isTimelineBackground =
|
|
!target.closest(".timeline-element") &&
|
|
!playheadRef.current?.contains(target) &&
|
|
!target.closest("[data-track-labels]");
|
|
|
|
if (isTimelineBackground) {
|
|
mouseTrackingRef.current = {
|
|
isMouseDown: true,
|
|
downX: e.clientX,
|
|
downY: e.clientY,
|
|
downTime: e.timeStamp,
|
|
};
|
|
}
|
|
},
|
|
[playheadRef],
|
|
);
|
|
|
|
const shouldProcessTimelineClick = useCallback(
|
|
(e: React.MouseEvent) => {
|
|
const target = e.target as HTMLElement;
|
|
const { isMouseDown, downX, downY, downTime } = mouseTrackingRef.current;
|
|
|
|
if (!isMouseDown) return false;
|
|
|
|
const deltaX = Math.abs(e.clientX - downX);
|
|
const deltaY = Math.abs(e.clientY - downY);
|
|
const deltaTime = e.timeStamp - downTime;
|
|
|
|
if (deltaX > 5 || deltaY > 5 || deltaTime > 500) return false;
|
|
|
|
if (isSelecting || justFinishedSelecting) return false;
|
|
|
|
if (target.closest(".timeline-element")) return false;
|
|
|
|
if (playheadRef.current?.contains(target)) return false;
|
|
|
|
if (target.closest("[data-track-labels]")) {
|
|
clearSelectedElements();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
[isSelecting, justFinishedSelecting, clearSelectedElements, playheadRef],
|
|
);
|
|
|
|
const handleTimelineSeek = useCallback(
|
|
(e: React.MouseEvent) => {
|
|
const isRulerClick = (e.target as HTMLElement).closest(
|
|
"[data-ruler-area]",
|
|
);
|
|
const scrollContainer = isRulerClick
|
|
? rulerScrollRef.current
|
|
: tracksScrollRef.current;
|
|
|
|
if (!scrollContainer) return;
|
|
|
|
const rect = scrollContainer.getBoundingClientRect();
|
|
const mouseX = e.clientX - rect.left;
|
|
const scrollLeft = scrollContainer.scrollLeft;
|
|
|
|
const rawTime = Math.max(
|
|
0,
|
|
Math.min(
|
|
duration,
|
|
(mouseX + scrollLeft) /
|
|
(TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel),
|
|
),
|
|
);
|
|
|
|
const projectFps = activeProject?.fps || 30;
|
|
const time = snapTimeToFrame({ time: rawTime, fps: projectFps });
|
|
seek(time);
|
|
},
|
|
[
|
|
duration,
|
|
zoomLevel,
|
|
seek,
|
|
rulerScrollRef,
|
|
tracksScrollRef,
|
|
activeProject?.fps,
|
|
],
|
|
);
|
|
|
|
const handleTimelineContentClick = useCallback(
|
|
(e: React.MouseEvent) => {
|
|
mouseTrackingRef.current = {
|
|
isMouseDown: false,
|
|
downX: 0,
|
|
downY: 0,
|
|
downTime: 0,
|
|
};
|
|
|
|
if (shouldProcessTimelineClick(e)) {
|
|
clearSelectedElements();
|
|
handleTimelineSeek(e);
|
|
}
|
|
},
|
|
[shouldProcessTimelineClick, handleTimelineSeek, clearSelectedElements],
|
|
);
|
|
|
|
return {
|
|
handleTimelineMouseDown,
|
|
handleTimelineContentClick,
|
|
};
|
|
}
|