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/preview-panel.tsx | 92 +++++++------------- apps/web/src/components/editor/timeline.tsx | 106 +++++++++++++++++------ 2 files changed, 109 insertions(+), 89 deletions(-) (limited to 'apps/web/src/components/editor') diff --git a/apps/web/src/components/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx index 16c808f4..593758e1 100644 --- a/apps/web/src/components/editor/preview-panel.tsx +++ b/apps/web/src/components/editor/preview-panel.tsx @@ -2,57 +2,52 @@ import { useTimelineStore } from "@/stores/timeline-store"; import { useMediaStore } from "@/stores/media-store"; +import { usePlaybackStore } from "@/stores/playback-store"; import { ImageTimelineTreatment } from "@/components/ui/image-timeline-treatment"; +import { VideoPlayer } from "@/components/ui/video-player"; import { Button } from "@/components/ui/button"; import { Play, Pause } from "lucide-react"; -import { useState } from "react"; export function PreviewPanel() { const { tracks } = useTimelineStore(); const { mediaItems } = useMediaStore(); - const [isPlaying, setIsPlaying] = useState(false); + const { isPlaying, toggle } = usePlaybackStore(); - // Get the first clip from the first track for preview (simplified for now) const firstClip = tracks[0]?.clips[0]; const firstMediaItem = firstClip ? mediaItems.find((item) => item.id === firstClip.mediaId) : null; - // Calculate dynamic aspect ratio - default to 16:9 if no media const aspectRatio = firstMediaItem?.aspectRatio || 16 / 9; - const renderPreviewContent = () => { + const renderContent = () => { if (!firstMediaItem) { return ( -
- Drop media here or click to import +
+ Drop media to start editing
); } - if (firstMediaItem.type === "image") { + if (firstMediaItem.type === "video") { return ( - ); } - if (firstMediaItem.type === "video") { - return firstMediaItem.thumbnailUrl ? ( - {firstMediaItem.name} - ) : ( -
- Video Preview -
); } @@ -62,6 +57,14 @@ export function PreviewPanel() {
🎵

{firstMediaItem.name}

+
); @@ -73,7 +76,7 @@ export function PreviewPanel() { return (
1 ? "100%" : "auto", @@ -82,49 +85,16 @@ export function PreviewPanel() { maxHeight: "100%", }} > - {renderPreviewContent()} - - {/* Playback Controls Overlay */} - {firstMediaItem && ( -
-
- - - {firstClip?.name || "No clip selected"} - -
-
- )} + {renderContent()}
- {/* Preview Info */} {firstMediaItem && (

- Preview: {firstMediaItem.name} - {firstMediaItem.type === "image" && - " (with CapCut-style treatment)"} -
- - Aspect Ratio: {aspectRatio.toFixed(2)} ( - {aspectRatio > 1 - ? "Landscape" - : aspectRatio < 1 - ? "Portrait" - : "Square"} - ) - + {firstMediaItem.name} +

+

+ {aspectRatio.toFixed(2)} • {aspectRatio > 1 ? "Landscape" : aspectRatio < 1 ? "Portrait" : "Square"}

)} 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