From 294ba01abeb2d8c3d3a04cb7ecc54abf97a6d4d3 Mon Sep 17 00:00:00 2001 From: Hyteq Date: Mon, 23 Jun 2025 15:39:56 +0300 Subject: feat: initial state for composition and overlay editing on the video player --- apps/web/src/components/editor/preview-panel.tsx | 532 +++++++++++++++++++---- 1 file changed, 444 insertions(+), 88 deletions(-) (limited to 'apps/web/src/components/editor/preview-panel.tsx') diff --git a/apps/web/src/components/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx index 132d6c06..5d03e941 100644 --- a/apps/web/src/components/editor/preview-panel.tsx +++ b/apps/web/src/components/editor/preview-panel.tsx @@ -3,110 +3,397 @@ 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 { Play, Pause, Move, RotateCw, Crop, ZoomIn, ZoomOut } from "lucide-react"; +import { useState, useRef, useEffect, useCallback } from "react"; + +interface ClipTransform { + x: number; + y: number; + scale: number; + rotation: number; + opacity: number; + width: number; + height: number; + blendMode: string; + cropTop: number; + cropBottom: number; + cropLeft: number; + cropRight: number; +} + +interface DragState { + isDragging: boolean; + dragType: 'move' | 'resize-nw' | 'resize-ne' | 'resize-sw' | 'resize-se' | 'rotate' | 'scale' | 'crop-n' | 'crop-s' | 'crop-e' | 'crop-w'; + startMouseX: number; + startMouseY: number; + startTransform: ClipTransform; + clipId: string; +} export function PreviewPanel() { const { tracks } = useTimelineStore(); const { mediaItems } = useMediaStore(); const { isPlaying, toggle, currentTime } = usePlaybackStore(); - // Find the active clip at the current playback time - const getActiveClip = () => { - for (const track of tracks) { - for (const clip of track.clips) { + const [clipTransforms, setClipTransforms] = useState>({}); + const [canvasSize, setCanvasSize] = useState({ width: 1920, height: 1080 }); // Default 16:9 + const [dragState, setDragState] = useState(null); + const previewRef = useRef(null); + + // Get all active clips at current time (for overlaying) + const getActiveClips = () => { + const activeClips: Array<{ + clip: any; + track: any; + mediaItem: any; + layer: number; + }> = []; + + tracks.forEach((track, trackIndex) => { + track.clips.forEach((clip) => { const clipStart = clip.startTime; const clipEnd = clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); if (currentTime >= clipStart && currentTime < clipEnd) { - return clip; + const mediaItem = clip.mediaId === "test" + ? { type: "test", name: clip.name, url: "", thumbnailUrl: "" } + : mediaItems.find((item) => item.id === clip.mediaId); + + if (mediaItem || clip.mediaId === "test") { + activeClips.push({ + clip, + track, + mediaItem, + layer: trackIndex, // Track index determines layer order + }); + } } - } - } - return null; + }); + }); + + // Sort by layer (track order) - higher index = on top + return activeClips.sort((a, b) => a.layer - b.layer); }; - const activeClip = getActiveClip(); - const activeMediaItem = activeClip - ? mediaItems.find((item) => item.id === activeClip.mediaId) - : null; + const activeClips = getActiveClips(); + const aspectRatio = canvasSize.width / canvasSize.height; - const aspectRatio = activeMediaItem?.aspectRatio || 16 / 9; + // Get or create transform for a clip + const getClipTransform = (clipId: string): ClipTransform => { + return clipTransforms[clipId] || { + x: 0, + y: 0, + scale: 1, + rotation: 0, + opacity: 1, + width: 100, // Percentage of canvas + height: 100, + blendMode: 'normal', + cropTop: 0, + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + }; + }; - const renderContent = () => { - if (!activeClip) { - return ( -
- {tracks.length === 0 ? "Drop media to start editing" : "No clip at current time"} -
- ); + // Update clip transform + const updateClipTransform = useCallback((clipId: string, updates: Partial) => { + setClipTransforms(prev => { + const currentTransform = prev[clipId] || { + x: 0, + y: 0, + scale: 1, + rotation: 0, + opacity: 1, + width: 100, + height: 100, + blendMode: 'normal', + cropTop: 0, + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + }; + + return { + ...prev, + [clipId]: { ...currentTransform, ...updates } + }; + }); + }, []); + + // Mouse event handlers + const handleMouseDown = (e: React.MouseEvent, clipId: string, dragType: DragState['dragType']) => { + e.preventDefault(); + e.stopPropagation(); + + setDragState({ + isDragging: true, + dragType, + startMouseX: e.clientX, + startMouseY: e.clientY, + startTransform: getClipTransform(clipId), + clipId + }); + }; + + const handleMouseMove = useCallback((e: MouseEvent) => { + if (!dragState || !dragState.isDragging) return; + + const deltaX = e.clientX - dragState.startMouseX; + const deltaY = e.clientY - dragState.startMouseY; + const { startTransform, clipId, dragType } = dragState; + + switch (dragType) { + case 'move': + updateClipTransform(clipId, { + x: Math.max(-100, Math.min(100, startTransform.x + deltaX * 0.3)), + y: Math.max(-100, Math.min(100, startTransform.y + deltaY * 0.3)) + }); + break; + + case 'resize-nw': + updateClipTransform(clipId, { + width: Math.max(20, startTransform.width - deltaX * 0.5), + height: Math.max(20, startTransform.height - deltaY * 0.5) + }); + break; + + case 'resize-ne': + updateClipTransform(clipId, { + width: Math.max(20, startTransform.width + deltaX * 0.5), + height: Math.max(20, startTransform.height - deltaY * 0.5) + }); + break; + + case 'resize-sw': + updateClipTransform(clipId, { + width: Math.max(20, startTransform.width - deltaX * 0.5), + height: Math.max(20, startTransform.height + deltaY * 0.5) + }); + break; + + case 'resize-se': + updateClipTransform(clipId, { + width: Math.max(20, startTransform.width + deltaX * 0.5), + height: Math.max(20, startTransform.height + deltaY * 0.5) + }); + break; + + case 'rotate': + updateClipTransform(clipId, { + rotation: (startTransform.rotation + deltaX * 2) % 360 + }); + break; + + case 'scale': + updateClipTransform(clipId, { + scale: Math.max(0.1, Math.min(3, startTransform.scale + deltaX * 0.01)) + }); + break; + + case 'crop-n': + updateClipTransform(clipId, { + cropTop: Math.max(0, Math.min(40, startTransform.cropTop + deltaY * 0.2)) + }); + break; + + case 'crop-s': + updateClipTransform(clipId, { + cropBottom: Math.max(0, Math.min(40, startTransform.cropBottom - deltaY * 0.2)) + }); + break; + + case 'crop-e': + updateClipTransform(clipId, { + cropRight: Math.max(0, Math.min(40, startTransform.cropRight - deltaX * 0.2)) + }); + break; + + case 'crop-w': + updateClipTransform(clipId, { + cropLeft: Math.max(0, Math.min(40, startTransform.cropLeft + deltaX * 0.2)) + }); + break; + } + }, [dragState, updateClipTransform]); + + const handleMouseUp = useCallback(() => { + setDragState(null); + }, []); + + // Add global mouse event listeners + useEffect(() => { + if (dragState?.isDragging) { + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + return () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; } + }, [dragState, handleMouseMove, handleMouseUp]); + + // Initialize transforms for new clips + useEffect(() => { + const activeClips = getActiveClips(); + const newTransforms: Record = {}; + let hasNewClips = false; + + activeClips.forEach(({ clip }) => { + if (!clipTransforms[clip.id]) { + hasNewClips = true; + newTransforms[clip.id] = { + x: 0, + y: 0, + scale: 1, + rotation: 0, + opacity: 1, + width: 100, + height: 100, + blendMode: 'normal', + cropTop: 0, + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + }; + } + }); + + if (hasNewClips) { + setClipTransforms(prev => ({ ...prev, ...newTransforms })); + } + }, [tracks, currentTime]); // Re-run when tracks or time changes + + - // Handle test clips without media items - if (!activeMediaItem && activeClip.mediaId === "test") { + // Render a single clip layer + const renderClipLayer = (clipData: any, index: number) => { + const { clip, mediaItem } = clipData; + const transform = getClipTransform(clip.id); + + const layerStyle = { + position: 'absolute' as const, + left: '50%', + top: '50%', + width: `${transform.width}%`, + height: `${transform.height}%`, + transform: `translate(-50%, -50%) translate(${transform.x}%, ${transform.y}%) scale(${transform.scale}) rotate(${transform.rotation}deg)`, + opacity: transform.opacity, + mixBlendMode: transform.blendMode as any, + clipPath: `inset(${transform.cropTop}% ${transform.cropRight}% ${transform.cropBottom}% ${transform.cropLeft}%)`, + zIndex: index + 10, + cursor: dragState?.isDragging && dragState.clipId === clip.id ? 'grabbing' : 'grab', + userSelect: 'none' as const, + }; + + const handleClipMouseDown = (e: React.MouseEvent) => { + e.stopPropagation(); + handleMouseDown(e, clip.id, 'move'); + }; + + // Handle test clips + if (!mediaItem || clip.mediaId === "test") { return ( -
-
-
🎬
-

{activeClip.name}

-

Test clip for playback

+
+
+
🎬
+

{clip.name}

+ + {/* Hover resize corners */} +
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-nw'); }}>
+
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-ne'); }}>
+
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-sw'); }}>
+
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-se'); }}>
); } - if (!activeMediaItem) { + // Render video + if (mediaItem.type === "video") { return ( -
- Media not found +
+ + + {/* Hover resize corners */} +
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-nw'); }}>
+
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-ne'); }}>
+
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-sw'); }}>
+
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-se'); }}>
); } - if (activeMediaItem.type === "video") { + // Render image + if (mediaItem.type === "image") { return ( - - ); - } +
+ {mediaItem.name} - if (activeMediaItem.type === "image") { - return ( - + {/* Hover resize corners */} +
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-nw'); }}>
+
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-ne'); }}>
+
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-sw'); }}>
+
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-se'); }}>
+
); } - if (activeMediaItem.type === "audio") { + // Render audio (visual representation) + if (mediaItem.type === "audio") { return ( -
-
-
🎵
-

{activeMediaItem.name}

- +
+
+
🎵
+

{mediaItem.name}

+ + {/* Hover resize corners */} +
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-nw'); }}>
+
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-ne'); }}>
+
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-sw'); }}>
+
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-se'); }}>
); } @@ -114,31 +401,100 @@ export function PreviewPanel() { return null; }; + // Canvas size presets + const canvasPresets = [ + { name: "16:9 HD", width: 1920, height: 1080 }, + { name: "16:9 4K", width: 3840, height: 2160 }, + { name: "9:16 Mobile", width: 1080, height: 1920 }, + { name: "1:1 Square", width: 1080, height: 1080 }, + { name: "4:3 Standard", width: 1440, height: 1080 }, + ]; + return ( -
-
1 ? "100%" : "auto", - height: aspectRatio <= 1 ? "100%" : "auto", - maxWidth: "100%", - maxHeight: "100%", - }} - > - {renderContent()} +
+ {/* Canvas Controls */} +
+ Canvas: + + + +
+ + {/* Preview Area - Full Width */} +
+
1 ? "100%" : "auto", + height: aspectRatio <= 1 ? "100%" : "auto", + maxWidth: "100%", + maxHeight: "100%", + background: '#000000', + border: '1px solid #374151' + }} + + > + + + {/* Render all active clips as layers */} + {activeClips.length === 0 ? ( +
+ {tracks.length === 0 ? "Drop media to start editing" : "No clips at current time"} +
+ ) : ( + activeClips.map((clipData, index) => renderClipLayer(clipData, index)) + )} + + +
- {activeMediaItem && ( -
-

- {activeMediaItem.name} -

-

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

+ {/* Bottom Info Panel */} +
+ {/* Layer List */} +
+
Active Layers ({activeClips.length})
+
+ {activeClips.map((clipData, index) => ( +
+ + {index + 1} + + {clipData.clip.name} + {clipData.track.name} +
+ ))} +
- )} + + +
); } -- cgit v1.3.1 From dc35619017ed7fbaa5e83151ba99c143da8cf1f1 Mon Sep 17 00:00:00 2001 From: Hyteq Date: Tue, 24 Jun 2025 08:10:11 +0300 Subject: clean up and simplifying more logic --- apps/web/src/app/editor/page.tsx | 104 +++--- apps/web/src/components/editor-provider.tsx | 14 +- apps/web/src/components/editor/preview-panel.tsx | 407 +++-------------------- apps/web/src/components/ui/video-player.tsx | 113 ++----- apps/web/src/stores/panel-store.ts | 62 +--- 5 files changed, 156 insertions(+), 544 deletions(-) (limited to 'apps/web/src/components/editor/preview-panel.tsx') diff --git a/apps/web/src/app/editor/page.tsx b/apps/web/src/app/editor/page.tsx index 64186a3e..19df364c 100644 --- a/apps/web/src/app/editor/page.tsx +++ b/apps/web/src/app/editor/page.tsx @@ -42,59 +42,71 @@ export default function Editor() { return ( -
+
- - - {/* Main content area */} - - {/* Tools Panel */} - - - +
+ + + {/* Main content area */} + + {/* Tools Panel */} + + + - + - {/* Preview Area */} - - - + {/* Preview Area */} + + + - + - {/* Properties Panel */} - {/* - - */} - - + {/* Properties Panel - Hidden for now but ready */} + {/* + + */} + + - + - {/* Timeline */} - - - - + {/* Timeline */} + + + + +
); diff --git a/apps/web/src/components/editor-provider.tsx b/apps/web/src/components/editor-provider.tsx index ab55c7d7..38255956 100644 --- a/apps/web/src/components/editor-provider.tsx +++ b/apps/web/src/components/editor-provider.tsx @@ -3,7 +3,6 @@ import { useEffect } from "react"; import { Loader2 } from "lucide-react"; import { useEditorStore } from "@/stores/editor-store"; -import { usePanelStore } from "@/stores/panel-store"; interface EditorProviderProps { children: React.ReactNode; @@ -11,19 +10,10 @@ interface EditorProviderProps { export function EditorProvider({ children }: EditorProviderProps) { const { isInitializing, isPanelsReady, initializeApp } = useEditorStore(); - const { setInitialized } = usePanelStore(); useEffect(() => { - const initialize = async () => { - // Initialize the app - await initializeApp(); - - // Initialize panel store for future resize events - setInitialized(); - }; - - initialize(); - }, [initializeApp, setInitialized]); + initializeApp(); + }, [initializeApp]); // Show loading screen while initializing if (isInitializing || !isPanelsReady) { diff --git a/apps/web/src/components/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx index 5d03e941..101f1ff1 100644 --- a/apps/web/src/components/editor/preview-panel.tsx +++ b/apps/web/src/components/editor/preview-panel.tsx @@ -5,53 +5,25 @@ import { useMediaStore } from "@/stores/media-store"; import { usePlaybackStore } from "@/stores/playback-store"; import { VideoPlayer } from "@/components/ui/video-player"; import { Button } from "@/components/ui/button"; -import { Play, Pause, Move, RotateCw, Crop, ZoomIn, ZoomOut } from "lucide-react"; -import { useState, useRef, useEffect, useCallback } from "react"; - -interface ClipTransform { - x: number; - y: number; - scale: number; - rotation: number; - opacity: number; - width: number; - height: number; - blendMode: string; - cropTop: number; - cropBottom: number; - cropLeft: number; - cropRight: number; -} - -interface DragState { - isDragging: boolean; - dragType: 'move' | 'resize-nw' | 'resize-ne' | 'resize-sw' | 'resize-se' | 'rotate' | 'scale' | 'crop-n' | 'crop-s' | 'crop-e' | 'crop-w'; - startMouseX: number; - startMouseY: number; - startTransform: ClipTransform; - clipId: string; -} +import { Play, Pause } from "lucide-react"; +import { useState, useRef } from "react"; export function PreviewPanel() { const { tracks } = useTimelineStore(); const { mediaItems } = useMediaStore(); const { isPlaying, toggle, currentTime } = usePlaybackStore(); - - const [clipTransforms, setClipTransforms] = useState>({}); - const [canvasSize, setCanvasSize] = useState({ width: 1920, height: 1080 }); // Default 16:9 - const [dragState, setDragState] = useState(null); + const [canvasSize, setCanvasSize] = useState({ width: 1920, height: 1080 }); const previewRef = useRef(null); - // Get all active clips at current time (for overlaying) + // Get active clips at current time const getActiveClips = () => { const activeClips: Array<{ clip: any; track: any; mediaItem: any; - layer: number; }> = []; - tracks.forEach((track, trackIndex) => { + tracks.forEach((track) => { track.clips.forEach((clip) => { const clipStart = clip.startTime; const clipEnd = clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); @@ -62,338 +34,78 @@ export function PreviewPanel() { : mediaItems.find((item) => item.id === clip.mediaId); if (mediaItem || clip.mediaId === "test") { - activeClips.push({ - clip, - track, - mediaItem, - layer: trackIndex, // Track index determines layer order - }); + activeClips.push({ clip, track, mediaItem }); } } }); }); - // Sort by layer (track order) - higher index = on top - return activeClips.sort((a, b) => a.layer - b.layer); + return activeClips; }; const activeClips = getActiveClips(); const aspectRatio = canvasSize.width / canvasSize.height; - // Get or create transform for a clip - const getClipTransform = (clipId: string): ClipTransform => { - return clipTransforms[clipId] || { - x: 0, - y: 0, - scale: 1, - rotation: 0, - opacity: 1, - width: 100, // Percentage of canvas - height: 100, - blendMode: 'normal', - cropTop: 0, - cropBottom: 0, - cropLeft: 0, - cropRight: 0, - }; - }; - - // Update clip transform - const updateClipTransform = useCallback((clipId: string, updates: Partial) => { - setClipTransforms(prev => { - const currentTransform = prev[clipId] || { - x: 0, - y: 0, - scale: 1, - rotation: 0, - opacity: 1, - width: 100, - height: 100, - blendMode: 'normal', - cropTop: 0, - cropBottom: 0, - cropLeft: 0, - cropRight: 0, - }; - - return { - ...prev, - [clipId]: { ...currentTransform, ...updates } - }; - }); - }, []); - - // Mouse event handlers - const handleMouseDown = (e: React.MouseEvent, clipId: string, dragType: DragState['dragType']) => { - e.preventDefault(); - e.stopPropagation(); - - setDragState({ - isDragging: true, - dragType, - startMouseX: e.clientX, - startMouseY: e.clientY, - startTransform: getClipTransform(clipId), - clipId - }); - }; - - const handleMouseMove = useCallback((e: MouseEvent) => { - if (!dragState || !dragState.isDragging) return; - - const deltaX = e.clientX - dragState.startMouseX; - const deltaY = e.clientY - dragState.startMouseY; - const { startTransform, clipId, dragType } = dragState; - - switch (dragType) { - case 'move': - updateClipTransform(clipId, { - x: Math.max(-100, Math.min(100, startTransform.x + deltaX * 0.3)), - y: Math.max(-100, Math.min(100, startTransform.y + deltaY * 0.3)) - }); - break; - - case 'resize-nw': - updateClipTransform(clipId, { - width: Math.max(20, startTransform.width - deltaX * 0.5), - height: Math.max(20, startTransform.height - deltaY * 0.5) - }); - break; - - case 'resize-ne': - updateClipTransform(clipId, { - width: Math.max(20, startTransform.width + deltaX * 0.5), - height: Math.max(20, startTransform.height - deltaY * 0.5) - }); - break; - - case 'resize-sw': - updateClipTransform(clipId, { - width: Math.max(20, startTransform.width - deltaX * 0.5), - height: Math.max(20, startTransform.height + deltaY * 0.5) - }); - break; - - case 'resize-se': - updateClipTransform(clipId, { - width: Math.max(20, startTransform.width + deltaX * 0.5), - height: Math.max(20, startTransform.height + deltaY * 0.5) - }); - break; - - case 'rotate': - updateClipTransform(clipId, { - rotation: (startTransform.rotation + deltaX * 2) % 360 - }); - break; - - case 'scale': - updateClipTransform(clipId, { - scale: Math.max(0.1, Math.min(3, startTransform.scale + deltaX * 0.01)) - }); - break; - - case 'crop-n': - updateClipTransform(clipId, { - cropTop: Math.max(0, Math.min(40, startTransform.cropTop + deltaY * 0.2)) - }); - break; - - case 'crop-s': - updateClipTransform(clipId, { - cropBottom: Math.max(0, Math.min(40, startTransform.cropBottom - deltaY * 0.2)) - }); - break; - - case 'crop-e': - updateClipTransform(clipId, { - cropRight: Math.max(0, Math.min(40, startTransform.cropRight - deltaX * 0.2)) - }); - break; - - case 'crop-w': - updateClipTransform(clipId, { - cropLeft: Math.max(0, Math.min(40, startTransform.cropLeft + deltaX * 0.2)) - }); - break; - } - }, [dragState, updateClipTransform]); - - const handleMouseUp = useCallback(() => { - setDragState(null); - }, []); - - // Add global mouse event listeners - useEffect(() => { - if (dragState?.isDragging) { - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('mouseup', handleMouseUp); - return () => { - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleMouseUp); - }; - } - }, [dragState, handleMouseMove, handleMouseUp]); - - // Initialize transforms for new clips - useEffect(() => { - const activeClips = getActiveClips(); - const newTransforms: Record = {}; - let hasNewClips = false; - - activeClips.forEach(({ clip }) => { - if (!clipTransforms[clip.id]) { - hasNewClips = true; - newTransforms[clip.id] = { - x: 0, - y: 0, - scale: 1, - rotation: 0, - opacity: 1, - width: 100, - height: 100, - blendMode: 'normal', - cropTop: 0, - cropBottom: 0, - cropLeft: 0, - cropRight: 0, - }; - } - }); - - if (hasNewClips) { - setClipTransforms(prev => ({ ...prev, ...newTransforms })); - } - }, [tracks, currentTime]); // Re-run when tracks or time changes - - - - // Render a single clip layer - const renderClipLayer = (clipData: any, index: number) => { + // Render a clip + const renderClip = (clipData: any, index: number) => { const { clip, mediaItem } = clipData; - const transform = getClipTransform(clip.id); - - const layerStyle = { - position: 'absolute' as const, - left: '50%', - top: '50%', - width: `${transform.width}%`, - height: `${transform.height}%`, - transform: `translate(-50%, -50%) translate(${transform.x}%, ${transform.y}%) scale(${transform.scale}) rotate(${transform.rotation}deg)`, - opacity: transform.opacity, - mixBlendMode: transform.blendMode as any, - clipPath: `inset(${transform.cropTop}% ${transform.cropRight}% ${transform.cropBottom}% ${transform.cropLeft}%)`, - zIndex: index + 10, - cursor: dragState?.isDragging && dragState.clipId === clip.id ? 'grabbing' : 'grab', - userSelect: 'none' as const, - }; - const handleClipMouseDown = (e: React.MouseEvent) => { - e.stopPropagation(); - handleMouseDown(e, clip.id, 'move'); - }; - - // Handle test clips + // Test clips if (!mediaItem || clip.mediaId === "test") { return (
-
-
🎬
+
+
🎬

{clip.name}

- - {/* Hover resize corners */} -
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-nw'); }}>
-
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-ne'); }}>
-
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-sw'); }}>
-
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-se'); }}>
); } - // Render video + // Video clips if (mediaItem.type === "video") { return ( -
+
- - {/* Hover resize corners */} -
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-nw'); }}>
-
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-ne'); }}>
-
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-sw'); }}>
-
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-se'); }}>
); } - // Render image + // Image clips if (mediaItem.type === "image") { return ( -
+
{mediaItem.name} - - {/* Hover resize corners */} -
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-nw'); }}>
-
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-ne'); }}>
-
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-sw'); }}>
-
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-se'); }}>
); } - // Render audio (visual representation) + // Audio clips (visual representation) if (mediaItem.type === "audio") { return (
-
-
🎵
+
+
🎵

{mediaItem.name}

- - {/* Hover resize corners */} -
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-nw'); }}>
-
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-ne'); }}>
-
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-sw'); }}>
-
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-se'); }}>
); } @@ -401,7 +113,7 @@ export function PreviewPanel() { return null; }; - // Canvas size presets + // Canvas presets const canvasPresets = [ { name: "16:9 HD", width: 1920, height: 1080 }, { name: "16:9 4K", width: 3840, height: 2160 }, @@ -411,9 +123,9 @@ export function PreviewPanel() { ]; return ( -
- {/* Canvas Controls */} -
+
+ {/* Controls */} +
Canvas: -
- {/* Preview Area - Full Width */} -
+ {/* Preview Area */} +
1 ? "100%" : "auto", - height: aspectRatio <= 1 ? "100%" : "auto", - maxWidth: "100%", - maxHeight: "100%", - background: '#000000', - border: '1px solid #374151' + width: "100%", + height: "100%", }} - > - - - {/* Render all active clips as layers */} {activeClips.length === 0 ? (
{tracks.length === 0 ? "Drop media to start editing" : "No clips at current time"}
) : ( - activeClips.map((clipData, index) => renderClipLayer(clipData, index)) + activeClips.map((clipData, index) => renderClip(clipData, index)) )} - -
- {/* Bottom Info Panel */} -
- {/* Layer List */} -
-
Active Layers ({activeClips.length})
-
- {activeClips.map((clipData, index) => ( -
- - {index + 1} - - {clipData.clip.name} - {clipData.track.name} -
- ))} -
+ {/* Info Panel */} +
+
Active Clips ({activeClips.length})
+
+ {activeClips.map((clipData, index) => ( +
+ + {index + 1} + + {clipData.clip.name} +
+ ))}
- -
); diff --git a/apps/web/src/components/ui/video-player.tsx b/apps/web/src/components/ui/video-player.tsx index 8731e1a8..f489f70a 100644 --- a/apps/web/src/components/ui/video-player.tsx +++ b/apps/web/src/components/ui/video-player.tsx @@ -1,8 +1,6 @@ "use client"; import { useRef, useEffect } from "react"; -import { Button } from "./button"; -import { Play, Pause, Volume2 } from "lucide-react"; import { usePlaybackStore } from "@/stores/playback-store"; interface VideoPlayerProps { @@ -25,128 +23,87 @@ export function VideoPlayer({ clipDuration }: VideoPlayerProps) { const videoRef = useRef(null); - const { isPlaying, currentTime, volume, speed, play, pause, setVolume } = usePlaybackStore(); + const { isPlaying, currentTime, volume, speed } = usePlaybackStore(); // Calculate if we're within this clip's timeline range const clipEndTime = clipStartTime + (clipDuration - trimStart - trimEnd); const isInClipRange = currentTime >= clipStartTime && currentTime < clipEndTime; - // Calculate the video's internal time based on timeline position - const videoTime = Math.max(trimStart, Math.min( - clipDuration - trimEnd, - currentTime - clipStartTime + trimStart - )); - + // Sync playback events useEffect(() => { const video = videoRef.current; - if (!video) return; + if (!video || !isInClipRange) return; - const handleSeekEvent = (e: CustomEvent) => { - if (!isInClipRange) return; + const handleSeek = (e: CustomEvent) => { const timelineTime = e.detail.time; - const newVideoTime = Math.max(trimStart, Math.min( + const videoTime = Math.max(trimStart, Math.min( clipDuration - trimEnd, timelineTime - clipStartTime + trimStart )); - video.currentTime = newVideoTime; + video.currentTime = videoTime; }; - const handleUpdateEvent = (e: CustomEvent) => { - if (!isInClipRange) return; + const handleUpdate = (e: CustomEvent) => { const timelineTime = e.detail.time; - const targetVideoTime = Math.max(trimStart, Math.min( + const targetTime = Math.max(trimStart, Math.min( clipDuration - trimEnd, timelineTime - clipStartTime + trimStart )); - // Only sync if there's a significant difference to avoid micro-adjustments - if (Math.abs(video.currentTime - targetVideoTime) > 0.5) { - video.currentTime = targetVideoTime; + if (Math.abs(video.currentTime - targetTime) > 0.5) { + video.currentTime = targetTime; } }; - const handleSpeedEvent = (e: CustomEvent) => { - if (!isInClipRange) return; - // Set playbackRate directly without any additional checks + const handleSpeed = (e: CustomEvent) => { video.playbackRate = e.detail.speed; }; - window.addEventListener("playback-seek", handleSeekEvent as EventListener); - window.addEventListener("playback-update", handleUpdateEvent as EventListener); - window.addEventListener("playback-speed", handleSpeedEvent as EventListener); + window.addEventListener("playback-seek", handleSeek as EventListener); + window.addEventListener("playback-update", handleUpdate as EventListener); + window.addEventListener("playback-speed", handleSpeed as EventListener); return () => { - window.removeEventListener("playback-seek", handleSeekEvent as EventListener); - window.removeEventListener("playback-update", handleUpdateEvent as EventListener); - window.removeEventListener("playback-speed", handleSpeedEvent as EventListener); + window.removeEventListener("playback-seek", handleSeek as EventListener); + window.removeEventListener("playback-update", handleUpdate as EventListener); + window.removeEventListener("playback-speed", handleSpeed as EventListener); }; }, [clipStartTime, trimStart, trimEnd, clipDuration, isInClipRange]); - // Sync video playback state - only play if in clip range + // Sync playback state useEffect(() => { const video = videoRef.current; if (!video) return; if (isPlaying && isInClipRange) { - video.play().catch(console.error); + video.play().catch(() => { }); } else { video.pause(); } }, [isPlaying, isInClipRange]); - // Sync volume + // Sync volume and speed useEffect(() => { const video = videoRef.current; if (!video) return; - video.volume = volume; - }, [volume]); - // Sync speed immediately when it changes - useEffect(() => { - const video = videoRef.current; - if (!video) return; + video.volume = volume; video.playbackRate = speed; - }, [speed]); + }, [volume, speed]); return ( -
-
- {/* Info Panel */} -
-
Active Clips ({activeClips.length})
-
- {activeClips.map((clipData, index) => ( -
- - {index + 1} - - {clipData.clip.name} -
- ))} + {/* Debug Info Panel - Conditionally rendered */} + {showDebug && ( +
+
Debug: Active Clips ({activeClips.length})
+
+ {activeClips.map((clipData, index) => ( +
+ + {index + 1} + + {clipData.clip.name} + ({clipData.mediaItem?.type || 'test'}) +
+ ))} + {activeClips.length === 0 && ( + No active clips + )} +
-
+ )}
); } -- cgit v1.3.1 From b9e1dcf23e29e0adb08119b92f91e81db23ebb26 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Tue, 24 Jun 2025 10:32:19 +0200 Subject: refactor: change button variant from 'ghost' to 'text' across multiple components for consistency --- apps/web/src/app/(auth)/login/page.tsx | 2 +- apps/web/src/app/(auth)/signup/page.tsx | 2 +- apps/web/src/components/auth-form.tsx | 2 +- apps/web/src/components/editor/preview-panel.tsx | 2 +- apps/web/src/components/editor/timeline.tsx | 18 +++++++++--------- apps/web/src/components/header.tsx | 2 +- apps/web/src/components/ui/button.tsx | 2 +- apps/web/src/components/ui/calendar.tsx | 2 +- apps/web/src/components/ui/input.tsx | 2 +- apps/web/src/components/ui/pagination.tsx | 2 +- apps/web/src/components/ui/sidebar.tsx | 2 +- 11 files changed, 19 insertions(+), 19 deletions(-) (limited to 'apps/web/src/components/editor/preview-panel.tsx') diff --git a/apps/web/src/app/(auth)/login/page.tsx b/apps/web/src/app/(auth)/login/page.tsx index 5f37ced9..9935e153 100644 --- a/apps/web/src/app/(auth)/login/page.tsx +++ b/apps/web/src/app/(auth)/login/page.tsx @@ -137,7 +137,7 @@ export default function LoginPage() { return (
@@ -558,7 +558,7 @@ export function Timeline() { - @@ -567,7 +567,7 @@ export function Timeline() { - @@ -576,7 +576,7 @@ export function Timeline() { - @@ -585,7 +585,7 @@ export function Timeline() { - @@ -594,7 +594,7 @@ export function Timeline() { - @@ -603,7 +603,7 @@ export function Timeline() { - @@ -1625,7 +1625,7 @@ function TimelineTrackContent({ {/* Clip options menu */}