mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
feat: add snapping functionality and controls to timeline component
This commit is contained in:
@@ -15,6 +15,10 @@ import {
|
|||||||
Video,
|
Video,
|
||||||
Music,
|
Music,
|
||||||
TypeIcon,
|
TypeIcon,
|
||||||
|
Magnet,
|
||||||
|
Grid3X3,
|
||||||
|
Move,
|
||||||
|
Target,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -50,6 +54,8 @@ import {
|
|||||||
} from "./timeline-playhead";
|
} from "./timeline-playhead";
|
||||||
import { SelectionBox } from "./selection-box";
|
import { SelectionBox } from "./selection-box";
|
||||||
import { useSelectionBox } from "@/hooks/use-selection-box";
|
import { useSelectionBox } from "@/hooks/use-selection-box";
|
||||||
|
import { SnapIndicator } from "./snap-indicator";
|
||||||
|
import { useTimelineSnapping } from "@/hooks/use-timeline-snapping";
|
||||||
import type { DragData, TimelineTrack } from "@/types/timeline";
|
import type { DragData, TimelineTrack } from "@/types/timeline";
|
||||||
import {
|
import {
|
||||||
getTrackHeight,
|
getTrackHeight,
|
||||||
@@ -78,6 +84,17 @@ export function Timeline() {
|
|||||||
separateAudio,
|
separateAudio,
|
||||||
undo,
|
undo,
|
||||||
redo,
|
redo,
|
||||||
|
snappingEnabled,
|
||||||
|
gridSnappingEnabled,
|
||||||
|
elementSnappingEnabled,
|
||||||
|
playheadSnappingEnabled,
|
||||||
|
snapThreshold,
|
||||||
|
gridInterval,
|
||||||
|
toggleSnapping,
|
||||||
|
toggleGridSnapping,
|
||||||
|
toggleElementSnapping,
|
||||||
|
togglePlayheadSnapping,
|
||||||
|
dragState,
|
||||||
} = useTimelineStore();
|
} = useTimelineStore();
|
||||||
const { mediaItems, addMediaItem } = useMediaStore();
|
const { mediaItems, addMediaItem } = useMediaStore();
|
||||||
const { activeProject } = useProjectStore();
|
const { activeProject } = useProjectStore();
|
||||||
@@ -145,6 +162,20 @@ export function Timeline() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Initialize snapping functionality
|
||||||
|
const { snapElementPosition } = useTimelineSnapping({
|
||||||
|
snapThreshold,
|
||||||
|
gridInterval,
|
||||||
|
enableGridSnapping: snappingEnabled && gridSnappingEnabled,
|
||||||
|
enableElementSnapping: snappingEnabled && elementSnappingEnabled,
|
||||||
|
enablePlayheadSnapping: snappingEnabled && playheadSnappingEnabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate snap indicator state
|
||||||
|
const [currentSnapPoint, setCurrentSnapPoint] = useState<any>(null);
|
||||||
|
const showSnapIndicator =
|
||||||
|
dragState.isDragging && snappingEnabled && currentSnapPoint;
|
||||||
|
|
||||||
// Timeline content click to seek handler
|
// Timeline content click to seek handler
|
||||||
const handleTimelineContentClick = useCallback(
|
const handleTimelineContentClick = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
@@ -310,6 +341,34 @@ export function Timeline() {
|
|||||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||||
}, [redo]);
|
}, [redo]);
|
||||||
|
|
||||||
|
// Keyboard shortcuts for snapping
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
// Don't trigger when typing in input fields or textareas
|
||||||
|
if (
|
||||||
|
e.target instanceof HTMLInputElement ||
|
||||||
|
e.target instanceof HTMLTextAreaElement
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only trigger when timeline is focused or mouse is over timeline
|
||||||
|
if (
|
||||||
|
!isInTimeline &&
|
||||||
|
!timelineRef.current?.contains(document.activeElement)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === "s" || e.key === "S") {
|
||||||
|
e.preventDefault();
|
||||||
|
toggleSnapping();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}, [toggleSnapping, isInTimeline]);
|
||||||
|
|
||||||
// Old marquee system removed - using new SelectionBox component instead
|
// Old marquee system removed - using new SelectionBox component instead
|
||||||
|
|
||||||
const handleDragEnter = (e: React.DragEvent) => {
|
const handleDragEnter = (e: React.DragEvent) => {
|
||||||
@@ -815,6 +874,72 @@ export function Timeline() {
|
|||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>Delete element (Delete)</TooltipContent>
|
<TooltipContent>Delete element (Delete)</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
<div className="w-px h-6 bg-border mx-1" />
|
||||||
|
|
||||||
|
{/* Snapping Controls */}
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant={snappingEnabled ? "default" : "text"}
|
||||||
|
size="icon"
|
||||||
|
onClick={toggleSnapping}
|
||||||
|
>
|
||||||
|
<Magnet className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Toggle snapping (S)</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant={
|
||||||
|
gridSnappingEnabled && snappingEnabled ? "default" : "text"
|
||||||
|
}
|
||||||
|
size="icon"
|
||||||
|
onClick={toggleGridSnapping}
|
||||||
|
disabled={!snappingEnabled}
|
||||||
|
>
|
||||||
|
<Grid3X3 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Toggle grid snapping</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant={
|
||||||
|
elementSnappingEnabled && snappingEnabled ? "default" : "text"
|
||||||
|
}
|
||||||
|
size="icon"
|
||||||
|
onClick={toggleElementSnapping}
|
||||||
|
disabled={!snappingEnabled}
|
||||||
|
>
|
||||||
|
<Move className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Toggle element snapping</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant={
|
||||||
|
playheadSnappingEnabled && snappingEnabled
|
||||||
|
? "default"
|
||||||
|
: "text"
|
||||||
|
}
|
||||||
|
size="icon"
|
||||||
|
onClick={togglePlayheadSnapping}
|
||||||
|
disabled={!snappingEnabled}
|
||||||
|
>
|
||||||
|
<Target className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Toggle playhead snapping</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1033,6 +1158,17 @@ export function Timeline() {
|
|||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Snap Indicator */}
|
||||||
|
<SnapIndicator
|
||||||
|
snapPoint={currentSnapPoint}
|
||||||
|
zoomLevel={zoomLevel}
|
||||||
|
timelineHeight={Math.max(
|
||||||
|
200,
|
||||||
|
Math.min(800, getTotalTracksHeight(tracks))
|
||||||
|
)}
|
||||||
|
isVisible={showSnapIndicator}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user