feat: working
This commit is contained in:
parent
9fa9f406fe
commit
98b2103286
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Reference in New Issue