diff options
| author | Simon Orzel <simonorzel@gmail.com> | 2025-07-17 02:25:24 +0200 |
|---|---|---|
| committer | Simon Orzel <simonorzel@gmail.com> | 2025-07-17 02:25:24 +0200 |
| commit | 98b2103286f04d67db1d57f9e3330efa5d41fdb7 (patch) | |
| tree | c9b6d144d2d7e4f70de99aef1737871b044ad146 | |
| parent | 9fa9f406fe2722cec1e0d65d9b3868acd1e99f87 (diff) | |
| download | opencut-98b2103286f04d67db1d57f9e3330efa5d41fdb7.tar.gz | |
feat: working
6 files changed, 110 insertions, 105 deletions
diff --git a/apps/web/src/components/editor/timeline/timeline-action-handlers.tsx b/apps/web/src/components/editor/timeline/timeline-action-handlers.tsx index ca12d71a..c9560507 100644 --- a/apps/web/src/components/editor/timeline/timeline-action-handlers.tsx +++ b/apps/web/src/components/editor/timeline/timeline-action-handlers.tsx @@ -1,12 +1,11 @@ +import { useMemo } from "react"; import { toast } from "sonner"; import { useTimelineStore } from "@/stores/timeline-store"; import { usePlaybackStore } from "@/stores/playback-store"; -import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; export function useTimelineActionHandlers() { const { tracks, - addTrack, addElementToTrack, removeElementFromTrack, selectedElements, @@ -18,6 +17,29 @@ export function useTimelineActionHandlers() { } = useTimelineStore(); const { currentTime } = usePlaybackStore(); + // Create optimized lookup maps for O(1) access instead of O(n) find operations + const trackMap = useMemo(() => { + const map = new Map(); + tracks.forEach(track => { + map.set(track.id, track); + // Also create element lookup for this track + const elementMap = new Map(); + track.elements.forEach(element => { + elementMap.set(element.id, element); + }); + map.set(`${track.id}_elements`, elementMap); + }); + return map; + }, [tracks]); + + // Helper function for O(1) track/element lookup + const findTrackAndElement = (trackId: string, elementId: string) => { + const track = trackMap.get(trackId); + const elementMap = trackMap.get(`${trackId}_elements`); + const element = elementMap?.get(elementId); + return { track, element }; + }; + // Action handlers for toolbar const handleSplitSelected = () => { if (selectedElements.length === 0) { @@ -26,8 +48,7 @@ export function useTimelineActionHandlers() { } let splitCount = 0; selectedElements.forEach(({ trackId, elementId }) => { - const track = tracks.find((t) => t.id === trackId); - const element = track?.elements.find((c) => c.id === elementId); + const { track, element } = findTrackAndElement(trackId, elementId); if (element && track) { const effectiveStart = element.startTime; const effectiveEnd = @@ -53,11 +74,8 @@ export function useTimelineActionHandlers() { const canDuplicate = selectedElements.length === 1; if (!canDuplicate) return; - const newSelections: { trackId: string; elementId: string }[] = []; - selectedElements.forEach(({ trackId, elementId }) => { - const track = tracks.find((t) => t.id === trackId); - const element = track?.elements.find((el) => el.id === elementId); + const { element } = findTrackAndElement(trackId, elementId); if (element) { const newStartTime = @@ -91,8 +109,7 @@ export function useTimelineActionHandlers() { return; } const { trackId, elementId } = selectedElements[0]; - const track = tracks.find((t) => t.id === trackId); - const element = track?.elements.find((c) => c.id === elementId); + const { element } = findTrackAndElement(trackId, elementId); if (!element) return; const effectiveStart = element.startTime; const effectiveEnd = @@ -111,8 +128,7 @@ export function useTimelineActionHandlers() { return; } const { trackId, elementId } = selectedElements[0]; - const track = tracks.find((t) => t.id === trackId); - const element = track?.elements.find((c) => c.id === elementId); + const { element } = findTrackAndElement(trackId, elementId); if (!element) return; const effectiveStart = element.startTime; const effectiveEnd = @@ -131,7 +147,7 @@ export function useTimelineActionHandlers() { return; } const { trackId, elementId } = selectedElements[0]; - const track = tracks.find((t) => t.id === trackId); + const { track } = findTrackAndElement(trackId, elementId); if (!track || track.type !== "media") { toast.error("Select a media element to separate audio"); return; diff --git a/apps/web/src/components/editor/timeline/timeline-content-click.tsx b/apps/web/src/components/editor/timeline/timeline-content-click.tsx index 9b86effc..8fb1ce2b 100644 --- a/apps/web/src/components/editor/timeline/timeline-content-click.tsx +++ b/apps/web/src/components/editor/timeline/timeline-content-click.tsx @@ -30,16 +30,6 @@ export function useTimelineContentClick({ // Timeline content click to seek handler const handleTimelineContentClick = useCallback( (e: React.MouseEvent) => { - console.log( - JSON.stringify({ - timelineClick: { - isSelecting, - justFinishedSelecting, - willReturn: isSelecting || justFinishedSelecting, - }, - }) - ); - // Don't seek if this was a selection box operation if (isSelecting || justFinishedSelecting) { return; @@ -62,7 +52,6 @@ export function useTimelineContentClick({ } // Clear selected elements when clicking empty timeline area - console.log(JSON.stringify({ clearingSelectedElements: true })); clearSelectedElements(); // Determine if we're clicking in ruler or tracks area diff --git a/apps/web/src/components/editor/timeline/timeline-content.tsx b/apps/web/src/components/editor/timeline/timeline-content.tsx index a1fd6906..ed0375da 100644 --- a/apps/web/src/components/editor/timeline/timeline-content.tsx +++ b/apps/web/src/components/editor/timeline/timeline-content.tsx @@ -56,8 +56,8 @@ export function TimelineContent({ showSnapIndicator, }: TimelineContentProps) { return ( - <div className="min-w-max min-h-max" style={{ minWidth: `${dynamicTimelineWidth}px` }}> - <div className={`grid grid-cols-[192px_1fr] ${tracks.length > 0 ? `grid-rows-[20px_repeat(${tracks.length},minmax(0,max-content))]` : 'grid-rows-[20px_1fr]'}`}> + <div className="relative min-w-max min-h-max" style={{ minWidth: `${dynamicTimelineWidth}px` }}> + <div className={`h-full grid grid-cols-[192px_1fr] ${tracks.length > 0 ? `grid-rows-[20px_repeat(${tracks.length},minmax(0,max-content))]` : 'grid-rows-[20px_1fr]'}`}> {/* Top-Left Corner (Empty space above track labels) */} <div className="sticky top-0 left-0 border-inset bg-card/[0.99]"></div> @@ -85,7 +85,7 @@ export function TimelineContent({ /> </div> - {/* Overlay Components */} + {/* Overlay Components - positioned absolutely relative to the timeline container */} <SelectionBox startPos={selectionBox?.startPos || null} currentPos={selectionBox?.currentPos || null} diff --git a/apps/web/src/components/editor/timeline/timeline-playhead.tsx b/apps/web/src/components/editor/timeline/timeline-playhead.tsx index 7e64a890..a4d035f6 100644 --- a/apps/web/src/components/editor/timeline/timeline-playhead.tsx +++ b/apps/web/src/components/editor/timeline/timeline-playhead.tsx @@ -54,23 +54,18 @@ export function TimelinePlayhead({ 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 =
- trackLabelsWidth +
- playheadPosition * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
+ // Get dynamic track labels width, fallback to 192px (ml-48) if no tracks or no ref
+ const trackLabelsWidth = 192; // Fixed width from grid layout
+ const leftPosition = playheadPosition * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
return (
<div
ref={playheadRef}
- className="absolute pointer-events-auto z-[100] ml-48"
+ className="absolute pointer-events-auto z-[95]"
style={{
- left: `${leftPosition}px`,
+ left: `${trackLabelsWidth + leftPosition}px`,
top: 0,
- height: `${totalHeight}px`,
+ bottom: 0,
width: "2px", // Slightly wider for better click target
}}
onMouseDown={handlePlayheadMouseDown}
diff --git a/apps/web/src/components/editor/timeline/timeline-ruler.tsx b/apps/web/src/components/editor/timeline/timeline-ruler.tsx index 85972089..07a4ad5d 100644 --- a/apps/web/src/components/editor/timeline/timeline-ruler.tsx +++ b/apps/web/src/components/editor/timeline/timeline-ruler.tsx @@ -1,3 +1,4 @@ +import { useMemo } from "react"; import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; import { useTimelinePlayheadRuler } from "./timeline-playhead"; @@ -38,82 +39,86 @@ export function TimelineRuler({ playheadRef, }); + // Memoize the expensive ruler markers calculation + const rulerMarkers = useMemo(() => { + // Calculate appropriate time interval based on zoom level + const getTimeInterval = (zoom: number) => { + const pixelsPerSecond = TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoom; + if (pixelsPerSecond >= 200) return 0.1; // Every 0.1s when very zoomed in + if (pixelsPerSecond >= 100) return 0.5; // Every 0.5s when zoomed in + if (pixelsPerSecond >= 50) return 1; // Every 1s at normal zoom + if (pixelsPerSecond >= 25) return 2; // Every 2s when zoomed out + if (pixelsPerSecond >= 12) return 5; // Every 5s when more zoomed out + if (pixelsPerSecond >= 6) return 10; // Every 10s when very zoomed out + return 30; // Every 30s when extremely zoomed out + }; + + const formatTime = (seconds: number, interval: number) => { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = seconds % 60; + + if (hours > 0) { + return `${hours}:${minutes.toString().padStart(2, "0")}:${Math.floor(secs).toString().padStart(2, "0")}`; + } else if (minutes > 0) { + return `${minutes}:${Math.floor(secs).toString().padStart(2, "0")}`; + } else if (interval >= 1) { + return `${Math.floor(secs)}s`; + } else { + return `${secs.toFixed(1)}s`; + } + }; + + const interval = getTimeInterval(zoomLevel); + const markerCount = Math.ceil(duration / interval) + 1; + const markers = []; + + for (let i = 0; i < markerCount; i++) { + const time = i * interval; + if (time > duration) break; + + const isMainMarker = time % (interval >= 1 ? Math.max(1, interval) : 1) === 0; + const leftPosition = time * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel; + + markers.push( + <div + key={i} + className={`absolute top-0 bottom-0 z-[97] ${ + isMainMarker + ? "border-l border-muted-foreground/40" + : "border-l border-muted-foreground/20" + }`} + style={{ left: `${leftPosition}px` }} + > + <span + className={`absolute top-1 left-1 text-[0.6rem] z-[97] ${ + isMainMarker + ? "text-muted-foreground font-medium" + : "text-muted-foreground/70" + }`} + > + {formatTime(time, interval)} + </span> + </div> + ); + } + + return markers; + }, [duration, zoomLevel]); // Only recalculate when duration or zoomLevel changes + return ( <div - className="sticky top-0 bg-card/[0.99] border-b border-muted/30 z-[95]" + className="sticky top-0 bg-card/[0.99] border-b border-muted/30 z-[97]" onMouseDown={handleSelectionMouseDown} onClick={handleTimelineContentClick} data-ruler-area > <div ref={rulerRef} - className="relative h-5 select-none cursor-default pb-1 z-[95]" + className="relative h-5 select-none cursor-default pb-1 z-[97]" onMouseDown={handleRulerMouseDown} > - {(() => { - // Calculate appropriate time interval based on zoom level - const getTimeInterval = (zoom: number) => { - const pixelsPerSecond = - TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoom; - if (pixelsPerSecond >= 200) return 0.1; // Every 0.1s when very zoomed in - if (pixelsPerSecond >= 100) return 0.5; // Every 0.5s when zoomed in - if (pixelsPerSecond >= 50) return 1; // Every 1s at normal zoom - if (pixelsPerSecond >= 25) return 2; // Every 2s when zoomed out - if (pixelsPerSecond >= 12) return 5; // Every 5s when more zoomed out - if (pixelsPerSecond >= 6) return 10; // Every 10s when very zoomed out - return 30; // Every 30s when extremely zoomed out - }; - - const interval = getTimeInterval(zoomLevel); - const markerCount = Math.ceil(duration / interval) + 1; - - return Array.from({ length: markerCount }, (_, i) => { - const time = i * interval; - if (time > duration) return null; - - const isMainMarker = - time % (interval >= 1 ? Math.max(1, interval) : 1) === 0; - - return ( - <div - key={i} - className={`absolute top-0 bottom-0 z-[95] ${isMainMarker - ? "border-l border-muted-foreground/40" - : "border-l border-muted-foreground/20" - }`} - style={{ - left: `${time * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel}px`, - }} - > - <span - className={`absolute top-1 left-1 text-[0.6rem] z-[95] ${isMainMarker - ? "text-muted-foreground font-medium" - : "text-muted-foreground/70" - }`} - > - {(() => { - const formatTime = (seconds: number) => { - const hours = Math.floor(seconds / 3600); - const minutes = Math.floor((seconds % 3600) / 60); - const secs = seconds % 60; - - if (hours > 0) { - return `${hours}:${minutes.toString().padStart(2, "0")}:${Math.floor(secs).toString().padStart(2, "0")}`; - } else if (minutes > 0) { - return `${minutes}:${Math.floor(secs).toString().padStart(2, "0")}`; - } else if (interval >= 1) { - return `${Math.floor(secs)}s`; - } else { - return `${secs.toFixed(1)}s`; - } - }; - return formatTime(time); - })()} - </span> - </div> - ); - }).filter(Boolean); - })()} + {rulerMarkers} </div> </div> ); diff --git a/apps/web/src/components/editor/timeline/timeline-tracks-area.tsx b/apps/web/src/components/editor/timeline/timeline-tracks-area.tsx index 293c8cc9..0e11f366 100644 --- a/apps/web/src/components/editor/timeline/timeline-tracks-area.tsx +++ b/apps/web/src/components/editor/timeline/timeline-tracks-area.tsx @@ -35,7 +35,7 @@ export function TimelineTracksArea({ <Fragment key={track.id}> {/* Left Column (Sticky Track Labels) */} <div - className="sticky left-0 flex items-center border-b border-panel border-inset group bg-card/[0.99] z-[90]" + className="sticky left-0 flex items-center border-b border-panel border-inset group bg-card/[0.99] z-[96]" style={{ height: `${getTrackHeight(track.type)}px` }} > <div className="flex items-center gap-2 px-2"> |
