mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
cleanup
This commit is contained in:
@@ -2,74 +2,53 @@
|
|||||||
|
|
||||||
import { SnapPoint } from "@/hooks/use-timeline-snapping";
|
import { SnapPoint } from "@/hooks/use-timeline-snapping";
|
||||||
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
||||||
|
import type { TimelineTrack } from "@/types/timeline";
|
||||||
|
|
||||||
interface SnapIndicatorProps {
|
interface SnapIndicatorProps {
|
||||||
snapPoint: SnapPoint | null;
|
snapPoint: SnapPoint | null;
|
||||||
zoomLevel: number;
|
zoomLevel: number;
|
||||||
timelineHeight: number;
|
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
|
tracks: TimelineTrack[];
|
||||||
|
timelineRef: React.RefObject<HTMLDivElement>;
|
||||||
|
trackLabelsRef?: React.RefObject<HTMLDivElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SnapIndicator({
|
export function SnapIndicator({
|
||||||
snapPoint,
|
snapPoint,
|
||||||
zoomLevel,
|
zoomLevel,
|
||||||
timelineHeight,
|
|
||||||
isVisible,
|
isVisible,
|
||||||
|
tracks,
|
||||||
|
timelineRef,
|
||||||
|
trackLabelsRef,
|
||||||
}: SnapIndicatorProps) {
|
}: SnapIndicatorProps) {
|
||||||
if (!isVisible || !snapPoint) {
|
if (!isVisible || !snapPoint) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const timelineContainerHeight = timelineRef.current?.offsetHeight || 400;
|
||||||
|
const totalHeight = timelineContainerHeight - 8; // 8px padding from edges
|
||||||
|
|
||||||
|
// Get dynamic track labels width, fallback to 0 if no tracks or no ref
|
||||||
|
const trackLabelsWidth =
|
||||||
|
tracks.length > 0 && trackLabelsRef?.current
|
||||||
|
? trackLabelsRef.current.offsetWidth
|
||||||
|
: 0;
|
||||||
|
|
||||||
const leftPosition =
|
const leftPosition =
|
||||||
|
trackLabelsWidth +
|
||||||
snapPoint.time * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
|
snapPoint.time * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
|
||||||
|
|
||||||
const getIndicatorColor = () => {
|
|
||||||
switch (snapPoint.type) {
|
|
||||||
case "grid":
|
|
||||||
return "bg-blue-400";
|
|
||||||
case "element-start":
|
|
||||||
case "element-end":
|
|
||||||
return "bg-green-400";
|
|
||||||
case "playhead":
|
|
||||||
return "bg-red-400";
|
|
||||||
default:
|
|
||||||
return "bg-gray-400";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getIndicatorLabel = () => {
|
|
||||||
switch (snapPoint.type) {
|
|
||||||
case "grid":
|
|
||||||
return "Grid";
|
|
||||||
case "element-start":
|
|
||||||
return "Start";
|
|
||||||
case "element-end":
|
|
||||||
return "End";
|
|
||||||
case "playhead":
|
|
||||||
return "Playhead";
|
|
||||||
default:
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="absolute top-0 pointer-events-none z-50"
|
className="absolute pointer-events-none z-[90]"
|
||||||
style={{
|
style={{
|
||||||
left: `${leftPosition}px`,
|
left: `${leftPosition}px`,
|
||||||
height: `${timelineHeight}px`,
|
top: 0,
|
||||||
|
height: `${totalHeight}px`,
|
||||||
|
width: "2px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Snap line */}
|
<div className={`w-0.5 h-full bg-primary/40 opacity-80`} />
|
||||||
<div className={`w-0.5 h-full ${getIndicatorColor()} opacity-80`} />
|
|
||||||
|
|
||||||
{/* Snap label */}
|
|
||||||
<div
|
|
||||||
className={`absolute top-0 left-1 px-1 py-0.5 text-xs text-white rounded ${getIndicatorColor()} opacity-90 whitespace-nowrap`}
|
|
||||||
style={{ transform: "translateY(-100%)" }}
|
|
||||||
>
|
|
||||||
{getIndicatorLabel()} ({snapPoint.time.toFixed(1)}s)
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ interface TimelinePlayheadProps {
|
|||||||
trackLabelsRef?: React.RefObject<HTMLDivElement>;
|
trackLabelsRef?: React.RefObject<HTMLDivElement>;
|
||||||
timelineRef: React.RefObject<HTMLDivElement>;
|
timelineRef: React.RefObject<HTMLDivElement>;
|
||||||
playheadRef?: React.RefObject<HTMLDivElement>;
|
playheadRef?: React.RefObject<HTMLDivElement>;
|
||||||
|
isSnappingToPlayhead?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TimelinePlayhead({
|
export function TimelinePlayhead({
|
||||||
@@ -34,6 +35,7 @@ export function TimelinePlayhead({
|
|||||||
trackLabelsRef,
|
trackLabelsRef,
|
||||||
timelineRef,
|
timelineRef,
|
||||||
playheadRef: externalPlayheadRef,
|
playheadRef: externalPlayheadRef,
|
||||||
|
isSnappingToPlayhead = false,
|
||||||
}: TimelinePlayheadProps) {
|
}: TimelinePlayheadProps) {
|
||||||
const internalPlayheadRef = useRef<HTMLDivElement>(null);
|
const internalPlayheadRef = useRef<HTMLDivElement>(null);
|
||||||
const playheadRef = externalPlayheadRef || internalPlayheadRef;
|
const playheadRef = externalPlayheadRef || internalPlayheadRef;
|
||||||
@@ -73,11 +75,15 @@ export function TimelinePlayhead({
|
|||||||
}}
|
}}
|
||||||
onMouseDown={handlePlayheadMouseDown}
|
onMouseDown={handlePlayheadMouseDown}
|
||||||
>
|
>
|
||||||
{/* The red line spanning full height */}
|
{/* The playhead line spanning full height */}
|
||||||
<div className="absolute left-0 w-0.5 bg-foreground cursor-col-resize h-full" />
|
<div
|
||||||
|
className={`absolute left-0 w-0.5 cursor-col-resize h-full ${isSnappingToPlayhead ? "bg-primary" : "bg-foreground"}`}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Red dot indicator at the top (in ruler area) */}
|
{/* Playhead dot indicator at the top (in ruler area) */}
|
||||||
<div className="absolute top-1 left-1/2 transform -translate-x-1/2 w-3 h-3 bg-foreground rounded-full border-2 border-foreground shadow-sm" />
|
<div
|
||||||
|
className={`absolute top-1 left-1/2 transform -translate-x-1/2 w-3 h-3 rounded-full border-2 shadow-sm ${isSnappingToPlayhead ? "bg-primary border-primary" : "bg-foreground border-foreground"}`}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,22 +49,15 @@ export function TimelineTrackContent({
|
|||||||
clearSelectedElements,
|
clearSelectedElements,
|
||||||
insertTrackAt,
|
insertTrackAt,
|
||||||
snappingEnabled,
|
snappingEnabled,
|
||||||
gridSnappingEnabled,
|
|
||||||
elementSnappingEnabled,
|
|
||||||
playheadSnappingEnabled,
|
|
||||||
snapThreshold,
|
|
||||||
gridInterval,
|
|
||||||
} = useTimelineStore();
|
} = useTimelineStore();
|
||||||
|
|
||||||
const { currentTime } = usePlaybackStore();
|
const { currentTime } = usePlaybackStore();
|
||||||
|
|
||||||
// Initialize snapping hook
|
// Initialize snapping hook
|
||||||
const { snapElementPosition } = useTimelineSnapping({
|
const { snapElementPosition } = useTimelineSnapping({
|
||||||
snapThreshold,
|
snapThreshold: 10,
|
||||||
gridInterval,
|
enableElementSnapping: snappingEnabled,
|
||||||
enableGridSnapping: snappingEnabled && gridSnappingEnabled,
|
enablePlayheadSnapping: snappingEnabled,
|
||||||
enableElementSnapping: snappingEnabled && elementSnappingEnabled,
|
|
||||||
enablePlayheadSnapping: snappingEnabled && playheadSnappingEnabled,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const timelineRef = useRef<HTMLDivElement>(null);
|
const timelineRef = useRef<HTMLDivElement>(null);
|
||||||
|
|||||||
@@ -16,9 +16,7 @@ import {
|
|||||||
Music,
|
Music,
|
||||||
TypeIcon,
|
TypeIcon,
|
||||||
Magnet,
|
Magnet,
|
||||||
Grid3X3,
|
Lock,
|
||||||
Move,
|
|
||||||
Target,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -40,13 +38,6 @@ import { useTimelineZoom } from "@/hooks/use-timeline-zoom";
|
|||||||
import { processMediaFiles } from "@/lib/media-processing";
|
import { processMediaFiles } from "@/lib/media-processing";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useState, useRef, useEffect, useCallback } from "react";
|
import { useState, useRef, useEffect, useCallback } from "react";
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "../ui/select";
|
|
||||||
import { TimelineTrackContent } from "./timeline-track";
|
import { TimelineTrackContent } from "./timeline-track";
|
||||||
import {
|
import {
|
||||||
TimelinePlayhead,
|
TimelinePlayhead,
|
||||||
@@ -55,7 +46,7 @@ import {
|
|||||||
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 { SnapIndicator } from "./snap-indicator";
|
||||||
import { useTimelineSnapping, SnapPoint } from "@/hooks/use-timeline-snapping";
|
import { SnapPoint } from "@/hooks/use-timeline-snapping";
|
||||||
import type { DragData, TimelineTrack } from "@/types/timeline";
|
import type { DragData, TimelineTrack } from "@/types/timeline";
|
||||||
import {
|
import {
|
||||||
getTrackHeight,
|
getTrackHeight,
|
||||||
@@ -85,15 +76,7 @@ export function Timeline() {
|
|||||||
undo,
|
undo,
|
||||||
redo,
|
redo,
|
||||||
snappingEnabled,
|
snappingEnabled,
|
||||||
gridSnappingEnabled,
|
|
||||||
elementSnappingEnabled,
|
|
||||||
playheadSnappingEnabled,
|
|
||||||
snapThreshold,
|
|
||||||
gridInterval,
|
|
||||||
toggleSnapping,
|
toggleSnapping,
|
||||||
toggleGridSnapping,
|
|
||||||
toggleElementSnapping,
|
|
||||||
togglePlayheadSnapping,
|
|
||||||
dragState,
|
dragState,
|
||||||
} = useTimelineStore();
|
} = useTimelineStore();
|
||||||
const { mediaItems, addMediaItem } = useMediaStore();
|
const { mediaItems, addMediaItem } = useMediaStore();
|
||||||
@@ -135,7 +118,7 @@ export function Timeline() {
|
|||||||
const lastVerticalSync = useRef(0);
|
const lastVerticalSync = useRef(0);
|
||||||
|
|
||||||
// Timeline playhead ruler handlers
|
// Timeline playhead ruler handlers
|
||||||
const { handleRulerMouseDown, isDraggingRuler } = useTimelinePlayheadRuler({
|
const { handleRulerMouseDown } = useTimelinePlayheadRuler({
|
||||||
currentTime,
|
currentTime,
|
||||||
duration,
|
duration,
|
||||||
zoomLevel,
|
zoomLevel,
|
||||||
@@ -162,15 +145,6 @@ export function Timeline() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize snapping functionality
|
|
||||||
const { snapElementPosition } = useTimelineSnapping({
|
|
||||||
snapThreshold,
|
|
||||||
gridInterval,
|
|
||||||
enableGridSnapping: snappingEnabled && gridSnappingEnabled,
|
|
||||||
enableElementSnapping: snappingEnabled && elementSnappingEnabled,
|
|
||||||
enablePlayheadSnapping: snappingEnabled && playheadSnappingEnabled,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Calculate snap indicator state
|
// Calculate snap indicator state
|
||||||
const [currentSnapPoint, setCurrentSnapPoint] = useState<SnapPoint | null>(
|
const [currentSnapPoint, setCurrentSnapPoint] = useState<SnapPoint | null>(
|
||||||
null
|
null
|
||||||
@@ -724,202 +698,168 @@ export function Timeline() {
|
|||||||
onMouseLeave={() => setIsInTimeline(false)}
|
onMouseLeave={() => setIsInTimeline(false)}
|
||||||
>
|
>
|
||||||
{/* Toolbar */}
|
{/* Toolbar */}
|
||||||
<div className="border-b flex items-center px-2 py-1 gap-1">
|
<div className="border-b flex items-center justify-between px-2 py-1">
|
||||||
<TooltipProvider delayDuration={500}>
|
<div className="flex items-center gap-1 w-full">
|
||||||
{/* Play/Pause Button */}
|
<TooltipProvider delayDuration={500}>
|
||||||
<Tooltip>
|
{/* Play/Pause Button */}
|
||||||
<TooltipTrigger asChild>
|
<Tooltip>
|
||||||
<Button
|
<TooltipTrigger asChild>
|
||||||
variant="text"
|
<Button
|
||||||
size="icon"
|
variant="text"
|
||||||
onClick={toggle}
|
size="icon"
|
||||||
className="mr-2"
|
onClick={toggle}
|
||||||
>
|
className="mr-2"
|
||||||
{isPlaying ? (
|
>
|
||||||
<Pause className="h-4 w-4" />
|
{isPlaying ? (
|
||||||
) : (
|
<Pause className="h-4 w-4" />
|
||||||
<Play className="h-4 w-4" />
|
) : (
|
||||||
)}
|
<Play className="h-4 w-4" />
|
||||||
</Button>
|
)}
|
||||||
</TooltipTrigger>
|
</Button>
|
||||||
<TooltipContent>
|
</TooltipTrigger>
|
||||||
{isPlaying ? "Pause (Space)" : "Play (Space)"}
|
<TooltipContent>
|
||||||
</TooltipContent>
|
{isPlaying ? "Pause (Space)" : "Play (Space)"}
|
||||||
</Tooltip>
|
</TooltipContent>
|
||||||
<div className="w-px h-6 bg-border mx-1" />
|
</Tooltip>
|
||||||
{/* Time Display */}
|
<div className="w-px h-6 bg-border mx-1" />
|
||||||
<div
|
{/* Time Display */}
|
||||||
className="text-xs text-muted-foreground font-mono px-2"
|
<div
|
||||||
style={{ minWidth: "18ch", textAlign: "center" }}
|
className="text-xs text-muted-foreground font-mono px-2"
|
||||||
>
|
style={{ minWidth: "18ch", textAlign: "center" }}
|
||||||
{currentTime.toFixed(1)}s / {duration.toFixed(1)}s
|
>
|
||||||
</div>
|
{currentTime.toFixed(1)}s / {duration.toFixed(1)}s
|
||||||
{/* Test Clip Button - for debugging */}
|
</div>
|
||||||
{tracks.length === 0 && (
|
{/* Test Clip Button - for debugging */}
|
||||||
<>
|
{tracks.length === 0 && (
|
||||||
<div className="w-px h-6 bg-border mx-1" />
|
<>
|
||||||
<Tooltip>
|
<div className="w-px h-6 bg-border mx-1" />
|
||||||
<TooltipTrigger asChild>
|
<Tooltip>
|
||||||
<Button
|
<TooltipTrigger asChild>
|
||||||
variant="outline"
|
<Button
|
||||||
size="sm"
|
variant="outline"
|
||||||
onClick={() => {
|
size="sm"
|
||||||
const trackId = addTrack("media");
|
onClick={() => {
|
||||||
addElementToTrack(trackId, {
|
const trackId = addTrack("media");
|
||||||
type: "media",
|
addElementToTrack(trackId, {
|
||||||
mediaId: "test",
|
type: "media",
|
||||||
name: "Test Clip",
|
mediaId: "test",
|
||||||
duration: TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION,
|
name: "Test Clip",
|
||||||
startTime: 0,
|
duration: TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION,
|
||||||
trimStart: 0,
|
startTime: 0,
|
||||||
trimEnd: 0,
|
trimStart: 0,
|
||||||
});
|
trimEnd: 0,
|
||||||
}}
|
});
|
||||||
className="text-xs"
|
}}
|
||||||
>
|
className="text-xs"
|
||||||
Add Test Clip
|
>
|
||||||
</Button>
|
Add Test Clip
|
||||||
</TooltipTrigger>
|
</Button>
|
||||||
<TooltipContent>Add a test clip to try playback</TooltipContent>
|
</TooltipTrigger>
|
||||||
</Tooltip>
|
<TooltipContent>
|
||||||
</>
|
Add a test clip to try playback
|
||||||
)}
|
</TooltipContent>
|
||||||
<div className="w-px h-6 bg-border mx-1" />
|
</Tooltip>
|
||||||
<Tooltip>
|
</>
|
||||||
<TooltipTrigger asChild>
|
)}
|
||||||
<Button variant="text" size="icon" onClick={handleSplitSelected}>
|
<div className="w-px h-6 bg-border mx-1" />
|
||||||
<Scissors className="h-4 w-4" />
|
<Tooltip>
|
||||||
</Button>
|
<TooltipTrigger asChild>
|
||||||
</TooltipTrigger>
|
<Button
|
||||||
<TooltipContent>Split element (Ctrl+S)</TooltipContent>
|
variant="text"
|
||||||
</Tooltip>
|
size="icon"
|
||||||
<Tooltip>
|
onClick={handleSplitSelected}
|
||||||
<TooltipTrigger asChild>
|
>
|
||||||
<Button
|
<Scissors className="h-4 w-4" />
|
||||||
variant="text"
|
</Button>
|
||||||
size="icon"
|
</TooltipTrigger>
|
||||||
onClick={handleSplitAndKeepLeft}
|
<TooltipContent>Split element (Ctrl+S)</TooltipContent>
|
||||||
>
|
</Tooltip>
|
||||||
<ArrowLeftToLine className="h-4 w-4" />
|
<Tooltip>
|
||||||
</Button>
|
<TooltipTrigger asChild>
|
||||||
</TooltipTrigger>
|
<Button
|
||||||
<TooltipContent>Split and keep left (Ctrl+Q)</TooltipContent>
|
variant="text"
|
||||||
</Tooltip>
|
size="icon"
|
||||||
<Tooltip>
|
onClick={handleSplitAndKeepLeft}
|
||||||
<TooltipTrigger asChild>
|
>
|
||||||
<Button
|
<ArrowLeftToLine className="h-4 w-4" />
|
||||||
variant="text"
|
</Button>
|
||||||
size="icon"
|
</TooltipTrigger>
|
||||||
onClick={handleSplitAndKeepRight}
|
<TooltipContent>Split and keep left (Ctrl+Q)</TooltipContent>
|
||||||
>
|
</Tooltip>
|
||||||
<ArrowRightToLine className="h-4 w-4" />
|
<Tooltip>
|
||||||
</Button>
|
<TooltipTrigger asChild>
|
||||||
</TooltipTrigger>
|
<Button
|
||||||
<TooltipContent>Split and keep right (Ctrl+W)</TooltipContent>
|
variant="text"
|
||||||
</Tooltip>
|
size="icon"
|
||||||
<Tooltip>
|
onClick={handleSplitAndKeepRight}
|
||||||
<TooltipTrigger asChild>
|
>
|
||||||
<Button variant="text" size="icon" onClick={handleSeparateAudio}>
|
<ArrowRightToLine className="h-4 w-4" />
|
||||||
<SplitSquareHorizontal className="h-4 w-4" />
|
</Button>
|
||||||
</Button>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipContent>Split and keep right (Ctrl+W)</TooltipContent>
|
||||||
<TooltipContent>Separate audio (Ctrl+D)</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<Button
|
||||||
<Button
|
variant="text"
|
||||||
variant="text"
|
size="icon"
|
||||||
size="icon"
|
onClick={handleSeparateAudio}
|
||||||
onClick={handleDuplicateSelected}
|
>
|
||||||
>
|
<SplitSquareHorizontal className="h-4 w-4" />
|
||||||
<Copy className="h-4 w-4" />
|
</Button>
|
||||||
</Button>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipContent>Separate audio (Ctrl+D)</TooltipContent>
|
||||||
<TooltipContent>Duplicate element (Ctrl+D)</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<Button
|
||||||
<Button variant="text" size="icon" onClick={handleFreezeSelected}>
|
variant="text"
|
||||||
<Snowflake className="h-4 w-4" />
|
size="icon"
|
||||||
</Button>
|
onClick={handleDuplicateSelected}
|
||||||
</TooltipTrigger>
|
>
|
||||||
<TooltipContent>Freeze frame (F)</TooltipContent>
|
<Copy className="h-4 w-4" />
|
||||||
</Tooltip>
|
</Button>
|
||||||
<Tooltip>
|
</TooltipTrigger>
|
||||||
<TooltipTrigger asChild>
|
<TooltipContent>Duplicate element (Ctrl+D)</TooltipContent>
|
||||||
<Button variant="text" size="icon" onClick={handleDeleteSelected}>
|
</Tooltip>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Tooltip>
|
||||||
</Button>
|
<TooltipTrigger asChild>
|
||||||
</TooltipTrigger>
|
<Button
|
||||||
<TooltipContent>Delete element (Delete)</TooltipContent>
|
variant="text"
|
||||||
</Tooltip>
|
size="icon"
|
||||||
|
onClick={handleFreezeSelected}
|
||||||
<div className="w-px h-6 bg-border mx-1" />
|
>
|
||||||
|
<Snowflake className="h-4 w-4" />
|
||||||
{/* Snapping Controls */}
|
</Button>
|
||||||
<Tooltip>
|
</TooltipTrigger>
|
||||||
<TooltipTrigger asChild>
|
<TooltipContent>Freeze frame (F)</TooltipContent>
|
||||||
<Button
|
</Tooltip>
|
||||||
variant={snappingEnabled ? "default" : "text"}
|
<Tooltip>
|
||||||
size="icon"
|
<TooltipTrigger asChild>
|
||||||
onClick={toggleSnapping}
|
<Button
|
||||||
>
|
variant="text"
|
||||||
<Magnet className="h-4 w-4" />
|
size="icon"
|
||||||
</Button>
|
onClick={handleDeleteSelected}
|
||||||
</TooltipTrigger>
|
>
|
||||||
<TooltipContent>Toggle snapping (S)</TooltipContent>
|
<Trash2 className="h-4 w-4" />
|
||||||
</Tooltip>
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
<Tooltip>
|
<TooltipContent>Delete element (Delete)</TooltipContent>
|
||||||
<TooltipTrigger asChild>
|
</Tooltip>
|
||||||
<Button
|
</TooltipProvider>
|
||||||
variant={
|
</div>
|
||||||
gridSnappingEnabled && snappingEnabled ? "default" : "text"
|
<div className="flex items-center gap-1">
|
||||||
}
|
<TooltipProvider delayDuration={500}>
|
||||||
size="icon"
|
<Tooltip>
|
||||||
onClick={toggleGridSnapping}
|
<TooltipTrigger asChild>
|
||||||
disabled={!snappingEnabled}
|
<Button variant="text" size="icon" onClick={toggleSnapping}>
|
||||||
>
|
<Lock className="h-4 w-4" />
|
||||||
<Grid3X3 className="h-4 w-4" />
|
</Button>
|
||||||
</Button>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipContent>Auto snapping</TooltipContent>
|
||||||
<TooltipContent>Toggle grid snapping</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Timeline Container */}
|
{/* Timeline Container */}
|
||||||
@@ -939,6 +879,17 @@ export function Timeline() {
|
|||||||
trackLabelsRef={trackLabelsRef}
|
trackLabelsRef={trackLabelsRef}
|
||||||
timelineRef={timelineRef}
|
timelineRef={timelineRef}
|
||||||
playheadRef={playheadRef}
|
playheadRef={playheadRef}
|
||||||
|
isSnappingToPlayhead={
|
||||||
|
showSnapIndicator && currentSnapPoint?.type === "playhead"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<SnapIndicator
|
||||||
|
snapPoint={currentSnapPoint}
|
||||||
|
zoomLevel={zoomLevel}
|
||||||
|
tracks={tracks}
|
||||||
|
timelineRef={timelineRef}
|
||||||
|
trackLabelsRef={trackLabelsRef}
|
||||||
|
isVisible={showSnapIndicator}
|
||||||
/>
|
/>
|
||||||
{/* Timeline Header with Ruler */}
|
{/* Timeline Header with Ruler */}
|
||||||
<div className="flex bg-panel sticky top-0 z-10">
|
<div className="flex bg-panel sticky top-0 z-10">
|
||||||
@@ -1138,17 +1089,6 @@ 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>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { TimelineElement, TimelineTrack } from "@/types/timeline";
|
import { TimelineTrack } from "@/types/timeline";
|
||||||
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
||||||
|
|
||||||
export interface SnapPoint {
|
export interface SnapPoint {
|
||||||
time: number;
|
time: number;
|
||||||
type: "grid" | "element-start" | "element-end" | "playhead";
|
type: "element-start" | "element-end" | "playhead";
|
||||||
elementId?: string;
|
elementId?: string;
|
||||||
trackId?: string;
|
trackId?: string;
|
||||||
}
|
}
|
||||||
@@ -17,16 +17,12 @@ export interface SnapResult {
|
|||||||
|
|
||||||
export interface UseTimelineSnappingOptions {
|
export interface UseTimelineSnappingOptions {
|
||||||
snapThreshold?: number; // Distance in pixels to trigger snapping
|
snapThreshold?: number; // Distance in pixels to trigger snapping
|
||||||
gridInterval?: number; // Grid interval in seconds
|
|
||||||
enableGridSnapping?: boolean;
|
|
||||||
enableElementSnapping?: boolean;
|
enableElementSnapping?: boolean;
|
||||||
enablePlayheadSnapping?: boolean;
|
enablePlayheadSnapping?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTimelineSnapping({
|
export function useTimelineSnapping({
|
||||||
snapThreshold = 10,
|
snapThreshold = 10,
|
||||||
gridInterval = 1,
|
|
||||||
enableGridSnapping = true,
|
|
||||||
enableElementSnapping = true,
|
enableElementSnapping = true,
|
||||||
enablePlayheadSnapping = true,
|
enablePlayheadSnapping = true,
|
||||||
}: UseTimelineSnappingOptions = {}) {
|
}: UseTimelineSnappingOptions = {}) {
|
||||||
@@ -40,23 +36,6 @@ export function useTimelineSnapping({
|
|||||||
): SnapPoint[] => {
|
): SnapPoint[] => {
|
||||||
const snapPoints: SnapPoint[] = [];
|
const snapPoints: SnapPoint[] = [];
|
||||||
|
|
||||||
// Add grid snap points
|
|
||||||
if (enableGridSnapping) {
|
|
||||||
const gridStart = Math.floor(currentTime / gridInterval) * gridInterval;
|
|
||||||
const gridEnd = Math.ceil(currentTime / gridInterval) * gridInterval;
|
|
||||||
|
|
||||||
// Add nearby grid points
|
|
||||||
for (let i = -2; i <= 2; i++) {
|
|
||||||
const gridTime = gridStart + i * gridInterval;
|
|
||||||
if (gridTime >= 0) {
|
|
||||||
snapPoints.push({
|
|
||||||
time: gridTime,
|
|
||||||
type: "grid",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add element snap points
|
// Add element snap points
|
||||||
if (enableElementSnapping) {
|
if (enableElementSnapping) {
|
||||||
tracks.forEach((track) => {
|
tracks.forEach((track) => {
|
||||||
@@ -97,12 +76,7 @@ export function useTimelineSnapping({
|
|||||||
|
|
||||||
return snapPoints;
|
return snapPoints;
|
||||||
},
|
},
|
||||||
[
|
[enableElementSnapping, enablePlayheadSnapping]
|
||||||
enableGridSnapping,
|
|
||||||
enableElementSnapping,
|
|
||||||
enablePlayheadSnapping,
|
|
||||||
gridInterval,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const snapToNearestPoint = useCallback(
|
const snapToNearestPoint = useCallback(
|
||||||
|
|||||||
@@ -44,19 +44,9 @@ interface TimelineStore {
|
|||||||
|
|
||||||
// Snapping settings
|
// Snapping settings
|
||||||
snappingEnabled: boolean;
|
snappingEnabled: boolean;
|
||||||
gridSnappingEnabled: boolean;
|
|
||||||
elementSnappingEnabled: boolean;
|
|
||||||
playheadSnappingEnabled: boolean;
|
|
||||||
snapThreshold: number;
|
|
||||||
gridInterval: number;
|
|
||||||
|
|
||||||
// Snapping actions
|
// Snapping actions
|
||||||
toggleSnapping: () => void;
|
toggleSnapping: () => void;
|
||||||
toggleGridSnapping: () => void;
|
|
||||||
toggleElementSnapping: () => void;
|
|
||||||
togglePlayheadSnapping: () => void;
|
|
||||||
setSnapThreshold: (threshold: number) => void;
|
|
||||||
setGridInterval: (interval: number) => void;
|
|
||||||
|
|
||||||
// Multi-selection
|
// Multi-selection
|
||||||
selectedElements: { trackId: string; elementId: string }[];
|
selectedElements: { trackId: string; elementId: string }[];
|
||||||
@@ -221,11 +211,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||||||
|
|
||||||
// Snapping settings defaults
|
// Snapping settings defaults
|
||||||
snappingEnabled: true,
|
snappingEnabled: true,
|
||||||
gridSnappingEnabled: true,
|
|
||||||
elementSnappingEnabled: true,
|
|
||||||
playheadSnappingEnabled: true,
|
|
||||||
snapThreshold: 10, // pixels
|
|
||||||
gridInterval: 1, // seconds
|
|
||||||
|
|
||||||
getSortedTracks: () => {
|
getSortedTracks: () => {
|
||||||
const { _tracks } = get();
|
const { _tracks } = get();
|
||||||
@@ -978,29 +963,5 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||||||
toggleSnapping: () => {
|
toggleSnapping: () => {
|
||||||
set((state) => ({ snappingEnabled: !state.snappingEnabled }));
|
set((state) => ({ snappingEnabled: !state.snappingEnabled }));
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleGridSnapping: () => {
|
|
||||||
set((state) => ({ gridSnappingEnabled: !state.gridSnappingEnabled }));
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleElementSnapping: () => {
|
|
||||||
set((state) => ({
|
|
||||||
elementSnappingEnabled: !state.elementSnappingEnabled,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
togglePlayheadSnapping: () => {
|
|
||||||
set((state) => ({
|
|
||||||
playheadSnappingEnabled: !state.playheadSnappingEnabled,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
setSnapThreshold: (threshold) => {
|
|
||||||
set({ snapThreshold: Math.max(0, threshold) }); // Ensure non-negative threshold
|
|
||||||
},
|
|
||||||
|
|
||||||
setGridInterval: (interval) => {
|
|
||||||
set({ gridInterval: Math.max(0.1, interval) }); // Ensure minimum interval
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user