mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
we're getting there
This commit is contained in:
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -79,6 +79,7 @@ export function Timeline() {
|
||||
|
||||
const {
|
||||
dragState,
|
||||
dragDropTarget,
|
||||
handleElementMouseDown,
|
||||
handleElementClick,
|
||||
lastMouseXRef,
|
||||
@@ -312,6 +313,11 @@ export function Timeline() {
|
||||
tracks={timeline.getTracks()}
|
||||
isVisible={isDragOver}
|
||||
/>
|
||||
<DragLine
|
||||
dropTarget={dragDropTarget}
|
||||
tracks={timeline.getTracks()}
|
||||
isVisible={dragState.isDragging}
|
||||
/>
|
||||
|
||||
<ScrollArea className="h-full w-full" ref={tracksScrollRef}>
|
||||
<div
|
||||
|
||||
@@ -96,6 +96,10 @@ export function TimelineElement({
|
||||
);
|
||||
|
||||
const isBeingDragged = dragState.elementId === element.id;
|
||||
const dragOffsetY =
|
||||
isBeingDragged && dragState.isDragging
|
||||
? dragState.currentMouseY - dragState.startMouseY
|
||||
: 0;
|
||||
const elementStartTime =
|
||||
isBeingDragged && dragState.isDragging
|
||||
? dragState.currentTime
|
||||
@@ -130,9 +134,16 @@ export function TimelineElement({
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger asChild>
|
||||
<div
|
||||
className={`timeline-element absolute top-0 h-full select-none ${isBeingDragged ? "z-30" : "z-10"
|
||||
className={`absolute top-0 h-full select-none ${isBeingDragged ? "z-30" : "z-10"
|
||||
}`}
|
||||
style={{ left: `${elementLeft}px`, width: `${elementWidth}px` }}
|
||||
style={{
|
||||
left: `${elementLeft}px`,
|
||||
width: `${elementWidth}px`,
|
||||
transform:
|
||||
isBeingDragged && dragState.isDragging
|
||||
? `translate3d(0, ${dragOffsetY}px, 0)`
|
||||
: undefined,
|
||||
}}
|
||||
data-element-id={element.id}
|
||||
data-track-id={track.id}
|
||||
>
|
||||
|
||||
@@ -53,10 +53,10 @@ export function TimelineTrackContent({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="hover:bg-muted/20 size-full"
|
||||
className="size-full"
|
||||
onClick={clearElementSelection}
|
||||
>
|
||||
<div className="track-elements-container relative h-full min-w-full">
|
||||
<div className="relative h-full min-w-full">
|
||||
{track.elements.length === 0 ? (
|
||||
<div className="text-muted-foreground border-muted/30 flex size-full items-center justify-center rounded-sm border-2 border-dashed text-xs" />
|
||||
) : (
|
||||
|
||||
@@ -13,6 +13,7 @@ import { snapTimeToFrame } from "@/lib/time-utils";
|
||||
import { computeDropTarget } from "@/lib/timeline/drop-utils";
|
||||
import { generateUUID } from "@/lib/utils";
|
||||
import type {
|
||||
DropTarget,
|
||||
ElementDragState,
|
||||
TimelineElement,
|
||||
TimelineTrack,
|
||||
@@ -38,6 +39,7 @@ const initialDragState: ElementDragState = {
|
||||
startElementTime: 0,
|
||||
clickOffsetTime: 0,
|
||||
currentTime: 0,
|
||||
currentMouseY: 0,
|
||||
};
|
||||
|
||||
interface PendingDragState {
|
||||
@@ -84,9 +86,60 @@ function getElementDuration({ element }: { element: TimelineElement }): number {
|
||||
return element.duration - element.trimStart - element.trimEnd;
|
||||
}
|
||||
|
||||
function getDragDropTarget({
|
||||
clientX,
|
||||
clientY,
|
||||
elementId,
|
||||
trackId,
|
||||
tracks,
|
||||
tracksContainerRef,
|
||||
tracksScrollRef,
|
||||
zoomLevel,
|
||||
snappedTime,
|
||||
}: {
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
elementId: string;
|
||||
trackId: string;
|
||||
tracks: TimelineTrack[];
|
||||
tracksContainerRef: RefObject<HTMLDivElement | null>;
|
||||
tracksScrollRef: RefObject<HTMLDivElement | null>;
|
||||
zoomLevel: number;
|
||||
snappedTime: number;
|
||||
}): DropTarget | null {
|
||||
const containerRect = tracksContainerRef.current?.getBoundingClientRect();
|
||||
const scrollContainer = tracksScrollRef.current;
|
||||
if (!containerRect || !scrollContainer) return null;
|
||||
|
||||
const sourceTrack = tracks.find(({ id }) => id === trackId);
|
||||
const movingElement = sourceTrack?.elements.find(({ id }) => id === elementId);
|
||||
if (!movingElement) return null;
|
||||
|
||||
const elementDuration = getElementDuration({ element: movingElement });
|
||||
const scrollLeft = scrollContainer.scrollLeft;
|
||||
const scrollContainerRect = scrollContainer.getBoundingClientRect();
|
||||
const mouseX = clientX - scrollContainerRect.left + scrollLeft;
|
||||
const mouseY = clientY - containerRect.top;
|
||||
|
||||
return computeDropTarget({
|
||||
elementType: movingElement.type,
|
||||
mouseX,
|
||||
mouseY,
|
||||
tracks,
|
||||
playheadTime: snappedTime,
|
||||
isExternalDrop: false,
|
||||
elementDuration,
|
||||
pixelsPerSecond: TIMELINE_CONSTANTS.PIXELS_PER_SECOND,
|
||||
zoomLevel,
|
||||
startTimeOverride: snappedTime,
|
||||
excludeElementId: movingElement.id,
|
||||
});
|
||||
}
|
||||
|
||||
interface StartDragParams
|
||||
extends Omit<ElementDragState, "isDragging" | "currentTime"> {
|
||||
extends Omit<ElementDragState, "isDragging" | "currentTime" | "currentMouseY"> {
|
||||
initialCurrentTime: number;
|
||||
initialCurrentMouseY: number;
|
||||
}
|
||||
|
||||
export function useElementInteraction({
|
||||
@@ -106,6 +159,7 @@ export function useElementInteraction({
|
||||
|
||||
const [dragState, setDragState] =
|
||||
useState<ElementDragState>(initialDragState);
|
||||
const [dragDropTarget, setDragDropTarget] = useState<DropTarget | null>(null);
|
||||
const [isPendingDrag, setIsPendingDrag] = useState(false);
|
||||
const pendingDragRef = useRef<PendingDragState | null>(null);
|
||||
const lastMouseXRef = useRef(0);
|
||||
@@ -120,6 +174,7 @@ export function useElementInteraction({
|
||||
startElementTime,
|
||||
clickOffsetTime,
|
||||
initialCurrentTime,
|
||||
initialCurrentMouseY,
|
||||
}: StartDragParams) => {
|
||||
setDragState({
|
||||
isDragging: true,
|
||||
@@ -130,6 +185,7 @@ export function useElementInteraction({
|
||||
startElementTime,
|
||||
clickOffsetTime,
|
||||
currentTime: initialCurrentTime,
|
||||
currentMouseY: initialCurrentMouseY,
|
||||
});
|
||||
},
|
||||
[],
|
||||
@@ -137,6 +193,7 @@ export function useElementInteraction({
|
||||
|
||||
const endDrag = useCallback(() => {
|
||||
setDragState(initialDragState);
|
||||
setDragDropTarget(null);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -173,6 +230,7 @@ export function useElementInteraction({
|
||||
startDrag({
|
||||
...pendingDragRef.current,
|
||||
initialCurrentTime: snappedTime,
|
||||
initialCurrentMouseY: clientY,
|
||||
});
|
||||
startedDragThisEvent = true;
|
||||
pendingDragRef.current = null;
|
||||
@@ -215,7 +273,23 @@ export function useElementInteraction({
|
||||
setDragState((previousDragState) => ({
|
||||
...previousDragState,
|
||||
currentTime: snappedTime,
|
||||
currentMouseY: clientY,
|
||||
}));
|
||||
|
||||
if (dragState.elementId && dragState.trackId) {
|
||||
const dropTarget = getDragDropTarget({
|
||||
clientX,
|
||||
clientY,
|
||||
elementId: dragState.elementId,
|
||||
trackId: dragState.trackId,
|
||||
tracks,
|
||||
tracksContainerRef,
|
||||
tracksScrollRef,
|
||||
zoomLevel,
|
||||
snappedTime,
|
||||
});
|
||||
setDragDropTarget(dropTarget?.isNewTrack ? dropTarget : null);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
@@ -231,6 +305,8 @@ export function useElementInteraction({
|
||||
editor.project,
|
||||
timelineRef,
|
||||
tracksScrollRef,
|
||||
tracksContainerRef,
|
||||
tracks,
|
||||
isPendingDrag,
|
||||
startDrag,
|
||||
]);
|
||||
@@ -241,37 +317,6 @@ export function useElementInteraction({
|
||||
const handleMouseUp = ({ clientX, clientY }: MouseEvent) => {
|
||||
if (!dragState.elementId || !dragState.trackId) return;
|
||||
|
||||
const containerRect = tracksContainerRef.current?.getBoundingClientRect();
|
||||
if (!containerRect) {
|
||||
endDrag();
|
||||
onSnapPointChange?.(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceTrack = tracks.find(({ id }) => id === dragState.trackId);
|
||||
if (!sourceTrack) {
|
||||
endDrag();
|
||||
onSnapPointChange?.(null);
|
||||
return;
|
||||
}
|
||||
const movingElement = sourceTrack?.elements.find(
|
||||
({ id }) => id === dragState.elementId,
|
||||
);
|
||||
|
||||
if (!movingElement) {
|
||||
endDrag();
|
||||
onSnapPointChange?.(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const elementDuration = getElementDuration({ element: movingElement });
|
||||
const scrollLeft = tracksScrollRef.current?.scrollLeft ?? 0;
|
||||
const scrollContainerRect =
|
||||
tracksScrollRef.current?.getBoundingClientRect();
|
||||
const mouseX = scrollContainerRect
|
||||
? clientX - scrollContainerRect.left + scrollLeft
|
||||
: clientX - containerRect.left + scrollLeft;
|
||||
const mouseY = clientY - containerRect.top;
|
||||
if (mouseDownLocationRef.current) {
|
||||
const deltaX = Math.abs(clientX - mouseDownLocationRef.current.x);
|
||||
const deltaY = Math.abs(clientY - mouseDownLocationRef.current.y);
|
||||
@@ -283,23 +328,34 @@ export function useElementInteraction({
|
||||
}
|
||||
}
|
||||
|
||||
const dropTarget = computeDropTarget({
|
||||
elementType: movingElement.type,
|
||||
mouseX,
|
||||
mouseY,
|
||||
const dropTarget = getDragDropTarget({
|
||||
clientX,
|
||||
clientY,
|
||||
elementId: dragState.elementId,
|
||||
trackId: dragState.trackId,
|
||||
tracks,
|
||||
playheadTime: dragState.currentTime,
|
||||
isExternalDrop: false,
|
||||
elementDuration,
|
||||
pixelsPerSecond: TIMELINE_CONSTANTS.PIXELS_PER_SECOND,
|
||||
tracksContainerRef,
|
||||
tracksScrollRef,
|
||||
zoomLevel,
|
||||
excludeElementId: movingElement.id,
|
||||
snappedTime: dragState.currentTime,
|
||||
});
|
||||
if (!dropTarget) {
|
||||
endDrag();
|
||||
onSnapPointChange?.(null);
|
||||
return;
|
||||
}
|
||||
const snappedTime = dragState.currentTime;
|
||||
|
||||
const sourceTrack = tracks.find(({ id }) => id === dragState.trackId);
|
||||
if (!sourceTrack) {
|
||||
endDrag();
|
||||
onSnapPointChange?.(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (dropTarget.isNewTrack) {
|
||||
const newTrackId = generateUUID();
|
||||
|
||||
|
||||
editor.timeline.moveElement({
|
||||
sourceTrackId: dragState.trackId,
|
||||
targetTrackId: newTrackId,
|
||||
@@ -453,6 +509,7 @@ export function useElementInteraction({
|
||||
|
||||
return {
|
||||
dragState,
|
||||
dragDropTarget,
|
||||
handleElementMouseDown,
|
||||
handleElementClick,
|
||||
lastMouseXRef,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { EditorCore } from "@/core";
|
||||
import type { TimelineTrack, TimelineElement, TrackType } from "@/types/timeline";
|
||||
import {
|
||||
buildEmptyTrack,
|
||||
isMainTrack,
|
||||
validateElementTrackCompatibility,
|
||||
} from "@/lib/timeline/track-utils";
|
||||
|
||||
@@ -64,7 +65,7 @@ export class MoveElementCommand extends Command {
|
||||
|
||||
const isSameTrack = this.sourceTrackId === this.targetTrackId;
|
||||
|
||||
const updatedTracks = tracksToUpdate.map((track) => {
|
||||
let updatedTracks = tracksToUpdate.map((track) => {
|
||||
if (isSameTrack && track.id === this.sourceTrackId) {
|
||||
return {
|
||||
...track,
|
||||
@@ -91,6 +92,21 @@ export class MoveElementCommand extends Command {
|
||||
return track;
|
||||
}) as TimelineTrack[];
|
||||
|
||||
if (!isSameTrack) {
|
||||
const sourceTrackAfterMove = updatedTracks.find(
|
||||
(track) => track.id === this.sourceTrackId,
|
||||
);
|
||||
if (
|
||||
sourceTrackAfterMove &&
|
||||
sourceTrackAfterMove.elements.length === 0 &&
|
||||
!isMainTrack(sourceTrackAfterMove)
|
||||
) {
|
||||
updatedTracks = updatedTracks.filter(
|
||||
(track) => track.id !== this.sourceTrackId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
editor.timeline.updateTracks(updatedTracks);
|
||||
}
|
||||
|
||||
|
||||
@@ -57,14 +57,3 @@ export const verifications = pgTable("verifications", {
|
||||
() => /* @__PURE__ */ new Date()
|
||||
),
|
||||
}).enableRLS();
|
||||
|
||||
export const exportWaitlist = pgTable("export_waitlist", {
|
||||
id: text("id").primaryKey(),
|
||||
email: text("email").notNull().unique(),
|
||||
createdAt: timestamp("created_at")
|
||||
.$defaultFn(() => /* @__PURE__ */ new Date())
|
||||
.notNull(),
|
||||
updatedAt: timestamp("updated_at")
|
||||
.$defaultFn(() => /* @__PURE__ */ new Date())
|
||||
.notNull(),
|
||||
}).enableRLS();
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const exportWaitlistSchema = z.object({
|
||||
email: z.string().email().max(320),
|
||||
});
|
||||
|
||||
export const exportWaitlistResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
alreadySubscribed: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type ExportWaitlistInput = z.infer<typeof exportWaitlistSchema>;
|
||||
export type ExportWaitlistResponse = z.infer<
|
||||
typeof exportWaitlistResponseSchema
|
||||
>;
|
||||
@@ -64,7 +64,7 @@ function findInsertIndex({
|
||||
}): { index: number; position: "above" | "below" } {
|
||||
const mainTrackIndex = getMainTrackIndex({ tracks });
|
||||
|
||||
if (elementType === "audio") {
|
||||
if (elementType === "audio") {
|
||||
if (preferredIndex <= mainTrackIndex) {
|
||||
return { index: mainTrackIndex + 1, position: "below" };
|
||||
}
|
||||
@@ -74,12 +74,14 @@ function findInsertIndex({
|
||||
};
|
||||
}
|
||||
|
||||
if (preferredIndex > mainTrackIndex && mainTrackIndex >= 0) {
|
||||
const overlayInsertIndex = insertAbove ? preferredIndex : preferredIndex + 1;
|
||||
|
||||
if (mainTrackIndex >= 0 && overlayInsertIndex > mainTrackIndex) {
|
||||
return { index: mainTrackIndex, position: "above" };
|
||||
}
|
||||
|
||||
return {
|
||||
index: insertAbove ? preferredIndex : preferredIndex + 1,
|
||||
index: overlayInsertIndex,
|
||||
position: insertAbove ? "above" : "below",
|
||||
};
|
||||
}
|
||||
@@ -94,11 +96,15 @@ export function computeDropTarget({
|
||||
elementDuration,
|
||||
pixelsPerSecond,
|
||||
zoomLevel,
|
||||
startTimeOverride,
|
||||
excludeElementId,
|
||||
}: ComputeDropTargetParams): DropTarget {
|
||||
const xPosition = isExternalDrop
|
||||
? playheadTime
|
||||
: Math.max(0, mouseX / (pixelsPerSecond * zoomLevel));
|
||||
const xPosition =
|
||||
typeof startTimeOverride === "number"
|
||||
? startTimeOverride
|
||||
: isExternalDrop
|
||||
? playheadTime
|
||||
: Math.max(0, mouseX / (pixelsPerSecond * zoomLevel));
|
||||
|
||||
const mainTrackIndex = getMainTrackIndex({ tracks });
|
||||
|
||||
|
||||
@@ -158,6 +158,7 @@ export interface ElementDragState {
|
||||
startElementTime: number;
|
||||
clickOffsetTime: number;
|
||||
currentTime: number;
|
||||
currentMouseY: number;
|
||||
}
|
||||
|
||||
export interface DropTarget {
|
||||
@@ -177,6 +178,7 @@ export interface ComputeDropTargetParams {
|
||||
elementDuration: number;
|
||||
pixelsPerSecond: number;
|
||||
zoomLevel: number;
|
||||
startTimeOverride?: number;
|
||||
excludeElementId?: string;
|
||||
}
|
||||
|
||||
|
||||
+17
-6
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
@@ -11,7 +15,7 @@
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
@@ -19,7 +23,9 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
},
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
@@ -29,7 +35,12 @@
|
||||
"apps/web/.next/types/**/*.ts",
|
||||
"next-env.d.ts",
|
||||
".next/types/**/*.ts",
|
||||
"src/types/**/*.d.ts"
|
||||
, "../../use-frame-cache.ts", "src/stores/timeline-store.ts" ],
|
||||
"exclude": ["node_modules"]
|
||||
"src/types/**/*.d.ts",
|
||||
"../../use-frame-cache.ts",
|
||||
"src/stores/timeline-store.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user