From ed6ab6cd5b6fec5f8e7d7cbcaa5224393729164e Mon Sep 17 00:00:00 2001 From: Hyteq Date: Mon, 23 Jun 2025 09:18:15 +0300 Subject: feat: cleaned up video player, added functionality, zooming in/out, timeline position viewer, keyboard controls --- apps/web/src/components/editor/timeline.tsx | 106 ++++++++++++++++++++-------- 1 file changed, 78 insertions(+), 28 deletions(-) (limited to 'apps/web/src/components/editor/timeline.tsx') diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index 6b507b7d..f089cb73 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -20,6 +20,7 @@ import { import { DragOverlay } from "../ui/drag-overlay"; import { useTimelineStore, type TimelineTrack } from "@/stores/timeline-store"; import { useMediaStore } from "@/stores/media-store"; +import { usePlaybackStore } from "@/stores/playback-store"; import { processMediaFiles } from "@/lib/media-processing"; import { ImageTimelineTreatment } from "@/components/ui/image-timeline-treatment"; import { toast } from "sonner"; @@ -28,9 +29,12 @@ import { useState, useRef } from "react"; export function Timeline() { const { tracks, addTrack, addClipToTrack } = useTimelineStore(); const { mediaItems, addMediaItem } = useMediaStore(); + const { currentTime, duration, seek } = usePlaybackStore(); const [isDragOver, setIsDragOver] = useState(false); const [isProcessing, setIsProcessing] = useState(false); + const [zoomLevel, setZoomLevel] = useState(1); const dragCounterRef = useRef(0); + const timelineRef = useRef(null); const handleDragEnter = (e: React.DragEvent) => { e.preventDefault(); @@ -171,6 +175,25 @@ export function Timeline() { } }; + const handleTimelineClick = (e: React.MouseEvent) => { + const timeline = timelineRef.current; + if (!timeline || duration === 0) return; + + const rect = timeline.getBoundingClientRect(); + const x = e.clientX - rect.left; + const timelineWidth = rect.width; + const visibleDuration = duration / zoomLevel; + const clickedTime = (x / timelineWidth) * visibleDuration; + + seek(Math.max(0, Math.min(duration, clickedTime))); + }; + + const handleWheel = (e: React.WheelEvent) => { + e.preventDefault(); + const delta = e.deltaY > 0 ? -0.05 : 0.05; + setZoomLevel(prev => Math.max(0.1, Math.min(10, prev + delta))); + }; + const dragProps = { onDragEnter: handleDragEnter, onDragOver: handleDragOver, @@ -264,46 +287,73 @@ export function Timeline() { {/* Tracks Area */} -
- {/* Time Markers */} -
- {Array.from({ length: 16 }).map((_, i) => ( +
+ {/* Timeline Header */} +
+ {/* Playhead */} + {duration > 0 && (
- {i}s +
- ))} + )} + + {/* Zoom indicator */} +
+ {zoomLevel.toFixed(1)}x +
{/* Timeline Tracks */} - {tracks.length === 0 ? ( -
-
- +
+ {tracks.length === 0 ? ( +
+
+ +
+

+ No tracks in timeline +

+

+ Add a video or audio track to get started +

-

- No tracks in timeline -

-

- Add a video or audio track to get started -

-
- ) : ( -
- {tracks.map((track) => ( - - ))} -
- )} + ) : ( +
+ {tracks.map((track) => ( + + ))} +
+ )} + + {/* Playhead for tracks area */} + {tracks.length > 0 && duration > 0 && ( +
+ )} +
); } -function TimelineTrackComponent({ track }: { track: TimelineTrack }) { +function TimelineTrackComponent({ track, zoomLevel }: { track: TimelineTrack, zoomLevel: number }) { const { mediaItems } = useMediaStore(); const { moveClipToTrack, reorderClipInTrack } = useTimelineStore(); const [isDropping, setIsDropping] = useState(false); @@ -528,7 +578,7 @@ function TimelineTrackComponent({ track }: { track: TimelineTrack }) { key={clip.id} className={`timeline-clip h-full rounded-sm border cursor-grab active:cursor-grabbing transition-colors ${getTrackColor(track.type)} flex items-center py-3 min-w-[80px] overflow-hidden`} style={{ - width: `${Math.max(80, clip.duration * 50)}px`, + width: `${Math.max(80, clip.duration * 50 * zoomLevel)}px`, }} draggable={true} onDragStart={(e) => handleClipDragStart(e, clip)} -- cgit v1.3.1