feat: working

This commit is contained in:
Simon Orzel
2025-07-17 02:25:24 +02:00
parent 9fa9f406fe
commit 98b2103286
6 changed files with 110 additions and 105 deletions
@@ -1,12 +1,11 @@
import { useMemo } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { useTimelineStore } from "@/stores/timeline-store"; import { useTimelineStore } from "@/stores/timeline-store";
import { usePlaybackStore } from "@/stores/playback-store"; import { usePlaybackStore } from "@/stores/playback-store";
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
export function useTimelineActionHandlers() { export function useTimelineActionHandlers() {
const { const {
tracks, tracks,
addTrack,
addElementToTrack, addElementToTrack,
removeElementFromTrack, removeElementFromTrack,
selectedElements, selectedElements,
@@ -18,6 +17,29 @@ export function useTimelineActionHandlers() {
} = useTimelineStore(); } = useTimelineStore();
const { currentTime } = usePlaybackStore(); 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 // Action handlers for toolbar
const handleSplitSelected = () => { const handleSplitSelected = () => {
if (selectedElements.length === 0) { if (selectedElements.length === 0) {
@@ -26,8 +48,7 @@ export function useTimelineActionHandlers() {
} }
let splitCount = 0; let splitCount = 0;
selectedElements.forEach(({ trackId, elementId }) => { selectedElements.forEach(({ trackId, elementId }) => {
const track = tracks.find((t) => t.id === trackId); const { track, element } = findTrackAndElement(trackId, elementId);
const element = track?.elements.find((c) => c.id === elementId);
if (element && track) { if (element && track) {
const effectiveStart = element.startTime; const effectiveStart = element.startTime;
const effectiveEnd = const effectiveEnd =
@@ -53,11 +74,8 @@ export function useTimelineActionHandlers() {
const canDuplicate = selectedElements.length === 1; const canDuplicate = selectedElements.length === 1;
if (!canDuplicate) return; if (!canDuplicate) return;
const newSelections: { trackId: string; elementId: string }[] = [];
selectedElements.forEach(({ trackId, elementId }) => { selectedElements.forEach(({ trackId, elementId }) => {
const track = tracks.find((t) => t.id === trackId); const { element } = findTrackAndElement(trackId, elementId);
const element = track?.elements.find((el) => el.id === elementId);
if (element) { if (element) {
const newStartTime = const newStartTime =
@@ -91,8 +109,7 @@ export function useTimelineActionHandlers() {
return; return;
} }
const { trackId, elementId } = selectedElements[0]; const { trackId, elementId } = selectedElements[0];
const track = tracks.find((t) => t.id === trackId); const { element } = findTrackAndElement(trackId, elementId);
const element = track?.elements.find((c) => c.id === elementId);
if (!element) return; if (!element) return;
const effectiveStart = element.startTime; const effectiveStart = element.startTime;
const effectiveEnd = const effectiveEnd =
@@ -111,8 +128,7 @@ export function useTimelineActionHandlers() {
return; return;
} }
const { trackId, elementId } = selectedElements[0]; const { trackId, elementId } = selectedElements[0];
const track = tracks.find((t) => t.id === trackId); const { element } = findTrackAndElement(trackId, elementId);
const element = track?.elements.find((c) => c.id === elementId);
if (!element) return; if (!element) return;
const effectiveStart = element.startTime; const effectiveStart = element.startTime;
const effectiveEnd = const effectiveEnd =
@@ -131,7 +147,7 @@ export function useTimelineActionHandlers() {
return; return;
} }
const { trackId, elementId } = selectedElements[0]; const { trackId, elementId } = selectedElements[0];
const track = tracks.find((t) => t.id === trackId); const { track } = findTrackAndElement(trackId, elementId);
if (!track || track.type !== "media") { if (!track || track.type !== "media") {
toast.error("Select a media element to separate audio"); toast.error("Select a media element to separate audio");
return; return;
@@ -30,16 +30,6 @@ export function useTimelineContentClick({
// Timeline content click to seek handler // Timeline content click to seek handler
const handleTimelineContentClick = useCallback( const handleTimelineContentClick = useCallback(
(e: React.MouseEvent) => { (e: React.MouseEvent) => {
console.log(
JSON.stringify({
timelineClick: {
isSelecting,
justFinishedSelecting,
willReturn: isSelecting || justFinishedSelecting,
},
})
);
// Don't seek if this was a selection box operation // Don't seek if this was a selection box operation
if (isSelecting || justFinishedSelecting) { if (isSelecting || justFinishedSelecting) {
return; return;
@@ -62,7 +52,6 @@ export function useTimelineContentClick({
} }
// Clear selected elements when clicking empty timeline area // Clear selected elements when clicking empty timeline area
console.log(JSON.stringify({ clearingSelectedElements: true }));
clearSelectedElements(); clearSelectedElements();
// Determine if we're clicking in ruler or tracks area // Determine if we're clicking in ruler or tracks area
@@ -56,8 +56,8 @@ export function TimelineContent({
showSnapIndicator, showSnapIndicator,
}: TimelineContentProps) { }: TimelineContentProps) {
return ( return (
<div className="min-w-max min-h-max" style={{ minWidth: `${dynamicTimelineWidth}px` }}> <div className="relative 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={`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) */} {/* Top-Left Corner (Empty space above track labels) */}
<div className="sticky top-0 left-0 border-inset bg-card/[0.99]"></div> <div className="sticky top-0 left-0 border-inset bg-card/[0.99]"></div>
@@ -85,7 +85,7 @@ export function TimelineContent({
/> />
</div> </div>
{/* Overlay Components */} {/* Overlay Components - positioned absolutely relative to the timeline container */}
<SelectionBox <SelectionBox
startPos={selectionBox?.startPos || null} startPos={selectionBox?.startPos || null}
currentPos={selectionBox?.currentPos || null} currentPos={selectionBox?.currentPos || null}
@@ -54,23 +54,18 @@ export function TimelinePlayhead({
const timelineContainerHeight = timelineRef.current?.offsetHeight || 400; const timelineContainerHeight = timelineRef.current?.offsetHeight || 400;
const totalHeight = timelineContainerHeight - 8; // 8px padding from edges const totalHeight = timelineContainerHeight - 8; // 8px padding from edges
// Get dynamic track labels width, fallback to 0 if no tracks or no ref // Get dynamic track labels width, fallback to 192px (ml-48) if no tracks or no ref
const trackLabelsWidth = const trackLabelsWidth = 192; // Fixed width from grid layout
tracks.length > 0 && trackLabelsRef?.current const leftPosition = playheadPosition * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
? trackLabelsRef.current.offsetWidth
: 0;
const leftPosition =
trackLabelsWidth +
playheadPosition * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
return ( return (
<div <div
ref={playheadRef} ref={playheadRef}
className="absolute pointer-events-auto z-[100] ml-48" className="absolute pointer-events-auto z-[95]"
style={{ style={{
left: `${leftPosition}px`, left: `${trackLabelsWidth + leftPosition}px`,
top: 0, top: 0,
height: `${totalHeight}px`, bottom: 0,
width: "2px", // Slightly wider for better click target width: "2px", // Slightly wider for better click target
}} }}
onMouseDown={handlePlayheadMouseDown} onMouseDown={handlePlayheadMouseDown}
@@ -1,3 +1,4 @@
import { useMemo } from "react";
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
import { useTimelinePlayheadRuler } from "./timeline-playhead"; import { useTimelinePlayheadRuler } from "./timeline-playhead";
@@ -38,23 +39,11 @@ export function TimelineRuler({
playheadRef, playheadRef,
}); });
return ( // Memoize the expensive ruler markers calculation
<div const rulerMarkers = useMemo(() => {
className="sticky top-0 bg-card/[0.99] border-b border-muted/30 z-[95]"
onMouseDown={handleSelectionMouseDown}
onClick={handleTimelineContentClick}
data-ruler-area
>
<div
ref={rulerRef}
className="relative h-5 select-none cursor-default pb-1 z-[95]"
onMouseDown={handleRulerMouseDown}
>
{(() => {
// Calculate appropriate time interval based on zoom level // Calculate appropriate time interval based on zoom level
const getTimeInterval = (zoom: number) => { const getTimeInterval = (zoom: number) => {
const pixelsPerSecond = const pixelsPerSecond = TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoom;
TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoom;
if (pixelsPerSecond >= 200) return 0.1; // Every 0.1s when very zoomed in 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 >= 100) return 0.5; // Every 0.5s when zoomed in
if (pixelsPerSecond >= 50) return 1; // Every 1s at normal zoom if (pixelsPerSecond >= 50) return 1; // Every 1s at normal zoom
@@ -64,35 +53,7 @@ export function TimelineRuler({
return 30; // Every 30s when extremely zoomed out return 30; // Every 30s when extremely zoomed out
}; };
const interval = getTimeInterval(zoomLevel); const formatTime = (seconds: number, interval: number) => {
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 hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60); const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60; const secs = seconds % 60;
@@ -107,13 +68,57 @@ export function TimelineRuler({
return `${secs.toFixed(1)}s`; return `${secs.toFixed(1)}s`;
} }
}; };
return formatTime(time);
})()} 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> </span>
</div> </div>
); );
}).filter(Boolean); }
})()}
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-[97]"
onMouseDown={handleSelectionMouseDown}
onClick={handleTimelineContentClick}
data-ruler-area
>
<div
ref={rulerRef}
className="relative h-5 select-none cursor-default pb-1 z-[97]"
onMouseDown={handleRulerMouseDown}
>
{rulerMarkers}
</div> </div>
</div> </div>
); );
@@ -35,7 +35,7 @@ export function TimelineTracksArea({
<Fragment key={track.id}> <Fragment key={track.id}>
{/* Left Column (Sticky Track Labels) */} {/* Left Column (Sticky Track Labels) */}
<div <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` }} style={{ height: `${getTrackHeight(track.type)}px` }}
> >
<div className="flex items-center gap-2 px-2"> <div className="flex items-center gap-2 px-2">