From 98b2103286f04d67db1d57f9e3330efa5d41fdb7 Mon Sep 17 00:00:00 2001 From: Simon Orzel Date: Thu, 17 Jul 2025 02:25:24 +0200 Subject: [PATCH] feat: working --- .../timeline/timeline-action-handlers.tsx | 42 ++++-- .../timeline/timeline-content-click.tsx | 11 -- .../editor/timeline/timeline-content.tsx | 6 +- .../editor/timeline/timeline-playhead.tsx | 17 +-- .../editor/timeline/timeline-ruler.tsx | 137 +++++++++--------- .../editor/timeline/timeline-tracks-area.tsx | 2 +- 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 ( -
-
0 ? `grid-rows-[20px_repeat(${tracks.length},minmax(0,max-content))]` : 'grid-rows-[20px_1fr]'}`}> +
+
0 ? `grid-rows-[20px_repeat(${tracks.length},minmax(0,max-content))]` : 'grid-rows-[20px_1fr]'}`}> {/* Top-Left Corner (Empty space above track labels) */}
@@ -85,7 +85,7 @@ export function TimelineContent({ />
- {/* Overlay Components */} + {/* Overlay Components - positioned absolutely relative to the timeline container */} 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 (
{ + // 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( +
+ + {formatTime(time, interval)} + +
+ ); + } + + return markers; + }, [duration, zoomLevel]); // Only recalculate when duration or zoomLevel changes + return (
- {(() => { - // 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 ( -
- - {(() => { - 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); - })()} - -
- ); - }).filter(Boolean); - })()} + {rulerMarkers}
); 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({ {/* Left Column (Sticky Track Labels) */}