summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSimon Orzel <simonorzel@gmail.com>2025-07-17 02:25:24 +0200
committerSimon Orzel <simonorzel@gmail.com>2025-07-17 02:25:24 +0200
commit98b2103286f04d67db1d57f9e3330efa5d41fdb7 (patch)
treec9b6d144d2d7e4f70de99aef1737871b044ad146
parent9fa9f406fe2722cec1e0d65d9b3868acd1e99f87 (diff)
downloadopencut-98b2103286f04d67db1d57f9e3330efa5d41fdb7.tar.gz
feat: working
-rw-r--r--apps/web/src/components/editor/timeline/timeline-action-handlers.tsx42
-rw-r--r--apps/web/src/components/editor/timeline/timeline-content-click.tsx11
-rw-r--r--apps/web/src/components/editor/timeline/timeline-content.tsx6
-rw-r--r--apps/web/src/components/editor/timeline/timeline-playhead.tsx17
-rw-r--r--apps/web/src/components/editor/timeline/timeline-ruler.tsx137
-rw-r--r--apps/web/src/components/editor/timeline/timeline-tracks-area.tsx2
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">