summaryrefslogtreecommitdiff
path: root/apps/web/src
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src')
-rw-r--r--apps/web/src/app/editor/page.tsx104
-rw-r--r--apps/web/src/components/editor-provider.tsx14
-rw-r--r--apps/web/src/components/editor/preview-panel.tsx407
-rw-r--r--apps/web/src/components/ui/video-player.tsx113
-rw-r--r--apps/web/src/stores/panel-store.ts62
5 files changed, 156 insertions, 544 deletions
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 (
<EditorProvider>
- <div className="h-screen w-screen flex flex-col bg-background">
+ <div className="h-screen w-screen flex flex-col bg-background overflow-hidden">
<EditorHeader />
- <ResizablePanelGroup direction="vertical">
- <ResizablePanel
- defaultSize={mainContent}
- minSize={30}
- onResize={setMainContent}
- >
- {/* Main content area */}
- <ResizablePanelGroup direction="horizontal">
- {/* Tools Panel */}
- <ResizablePanel
- defaultSize={toolsPanel}
- minSize={15}
- onResize={setToolsPanel}
- >
- <MediaPanel />
- </ResizablePanel>
+ <div className="flex-1 min-h-0 min-w-0">
+ <ResizablePanelGroup direction="vertical" className="h-full w-full">
+ <ResizablePanel
+ defaultSize={mainContent}
+ minSize={30}
+ maxSize={85}
+ onResize={setMainContent}
+ className="min-h-0"
+ >
+ {/* Main content area */}
+ <ResizablePanelGroup direction="horizontal" className="h-full w-full">
+ {/* Tools Panel */}
+ <ResizablePanel
+ defaultSize={toolsPanel}
+ minSize={15}
+ maxSize={40}
+ onResize={setToolsPanel}
+ className="min-w-0"
+ >
+ <MediaPanel />
+ </ResizablePanel>
- <ResizableHandle withHandle />
+ <ResizableHandle withHandle />
- {/* Preview Area */}
- <ResizablePanel
- defaultSize={previewPanel}
- onResize={setPreviewPanel}
- >
- <PreviewPanel />
- </ResizablePanel>
+ {/* Preview Area */}
+ <ResizablePanel
+ defaultSize={previewPanel}
+ minSize={30}
+ onResize={setPreviewPanel}
+ className="min-w-0 min-h-0 flex-1"
+ >
+ <PreviewPanel />
+ </ResizablePanel>
- <ResizableHandle withHandle />
+ <ResizableHandle withHandle />
- {/* Properties Panel */}
- {/* <ResizablePanel
- defaultSize={propertiesPanel}
- minSize={15}
- onResize={setPropertiesPanel}
- >
- <PropertiesPanel />
- </ResizablePanel> */}
- </ResizablePanelGroup>
- </ResizablePanel>
+ {/* Properties Panel - Hidden for now but ready */}
+ {/* <ResizablePanel
+ defaultSize={propertiesPanel}
+ minSize={15}
+ maxSize={40}
+ onResize={setPropertiesPanel}
+ className="min-w-0"
+ >
+ <PropertiesPanel />
+ </ResizablePanel> */}
+ </ResizablePanelGroup>
+ </ResizablePanel>
- <ResizableHandle withHandle />
+ <ResizableHandle withHandle />
- {/* Timeline */}
- <ResizablePanel
- defaultSize={timeline}
- minSize={15}
- onResize={setTimeline}
- >
- <Timeline />
- </ResizablePanel>
- </ResizablePanelGroup>
+ {/* Timeline */}
+ <ResizablePanel
+ defaultSize={timeline}
+ minSize={15}
+ maxSize={70}
+ onResize={setTimeline}
+ className="min-h-0"
+ >
+ <Timeline />
+ </ResizablePanel>
+ </ResizablePanelGroup>
+ </div>
</div>
</EditorProvider>
);
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<Record<string, ClipTransform>>({});
- const [canvasSize, setCanvasSize] = useState({ width: 1920, height: 1080 }); // Default 16:9
- const [dragState, setDragState] = useState<DragState | null>(null);
+ const [canvasSize, setCanvasSize] = useState({ width: 1920, height: 1080 });
const previewRef = useRef<HTMLDivElement>(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<ClipTransform>) => {
- 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<string, ClipTransform> = {};
- 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 (
<div
key={clip.id}
- style={layerStyle}
- onMouseDown={handleClipMouseDown}
- className="bg-gradient-to-br from-blue-500/20 to-purple-500/20 flex items-center justify-center group"
+ className="absolute inset-0 bg-gradient-to-br from-blue-500/20 to-purple-500/20 flex items-center justify-center"
>
- <div className="text-center pointer-events-none">
- <div className="text-2xl">🎬</div>
+ <div className="text-center">
+ <div className="text-2xl mb-2">🎬</div>
<p className="text-xs text-white">{clip.name}</p>
</div>
-
- {/* Hover resize corners */}
- <div className="absolute -top-1 -left-1 w-3 h-3 bg-blue-500 border border-white rounded-sm cursor-nw-resize opacity-0 group-hover:opacity-100 transition-opacity"
- onMouseDown={(e) => { e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-nw'); }}></div>
- <div className="absolute -top-1 -right-1 w-3 h-3 bg-blue-500 border border-white rounded-sm cursor-ne-resize opacity-0 group-hover:opacity-100 transition-opacity"
- onMouseDown={(e) => { e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-ne'); }}></div>
- <div className="absolute -bottom-1 -left-1 w-3 h-3 bg-blue-500 border border-white rounded-sm cursor-sw-resize opacity-0 group-hover:opacity-100 transition-opacity"
- onMouseDown={(e) => { e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-sw'); }}></div>
- <div className="absolute -bottom-1 -right-1 w-3 h-3 bg-blue-500 border border-white rounded-sm cursor-se-resize opacity-0 group-hover:opacity-100 transition-opacity"
- onMouseDown={(e) => { e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-se'); }}></div>
</div>
);
}
- // Render video
+ // Video clips
if (mediaItem.type === "video") {
return (
- <div key={clip.id} style={layerStyle} onMouseDown={handleClipMouseDown} className="group">
+ <div key={clip.id} className="absolute inset-0">
<VideoPlayer
src={mediaItem.url}
poster={mediaItem.thumbnailUrl}
- className="w-full h-full pointer-events-none"
clipStartTime={clip.startTime}
trimStart={clip.trimStart}
trimEnd={clip.trimEnd}
clipDuration={clip.duration}
/>
-
- {/* Hover resize corners */}
- <div className="absolute -top-1 -left-1 w-3 h-3 bg-blue-500 border border-white rounded-sm cursor-nw-resize opacity-0 group-hover:opacity-100 transition-opacity"
- onMouseDown={(e) => { e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-nw'); }}></div>
- <div className="absolute -top-1 -right-1 w-3 h-3 bg-blue-500 border border-white rounded-sm cursor-ne-resize opacity-0 group-hover:opacity-100 transition-opacity"
- onMouseDown={(e) => { e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-ne'); }}></div>
- <div className="absolute -bottom-1 -left-1 w-3 h-3 bg-blue-500 border border-white rounded-sm cursor-sw-resize opacity-0 group-hover:opacity-100 transition-opacity"
- onMouseDown={(e) => { e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-sw'); }}></div>
- <div className="absolute -bottom-1 -right-1 w-3 h-3 bg-blue-500 border border-white rounded-sm cursor-se-resize opacity-0 group-hover:opacity-100 transition-opacity"
- onMouseDown={(e) => { e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-se'); }}></div>
</div>
);
}
- // Render image
+ // Image clips
if (mediaItem.type === "image") {
return (
- <div key={clip.id} style={layerStyle} onMouseDown={handleClipMouseDown} className="group">
+ <div key={clip.id} className="absolute inset-0">
<img
src={mediaItem.url}
alt={mediaItem.name}
- className="w-full h-full object-cover rounded pointer-events-none"
+ className="w-full h-full object-cover"
draggable={false}
/>
-
- {/* Hover resize corners */}
- <div className="absolute -top-1 -left-1 w-3 h-3 bg-blue-500 border border-white rounded-sm cursor-nw-resize opacity-0 group-hover:opacity-100 transition-opacity"
- onMouseDown={(e) => { e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-nw'); }}></div>
- <div className="absolute -top-1 -right-1 w-3 h-3 bg-blue-500 border border-white rounded-sm cursor-ne-resize opacity-0 group-hover:opacity-100 transition-opacity"
- onMouseDown={(e) => { e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-ne'); }}></div>
- <div className="absolute -bottom-1 -left-1 w-3 h-3 bg-blue-500 border border-white rounded-sm cursor-sw-resize opacity-0 group-hover:opacity-100 transition-opacity"
- onMouseDown={(e) => { e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-sw'); }}></div>
- <div className="absolute -bottom-1 -right-1 w-3 h-3 bg-blue-500 border border-white rounded-sm cursor-se-resize opacity-0 group-hover:opacity-100 transition-opacity"
- onMouseDown={(e) => { e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-se'); }}></div>
</div>
);
}
- // Render audio (visual representation)
+ // Audio clips (visual representation)
if (mediaItem.type === "audio") {
return (
<div
key={clip.id}
- style={layerStyle}
- onMouseDown={handleClipMouseDown}
- className="bg-gradient-to-br from-green-500/20 to-emerald-500/20 flex items-center justify-center group"
+ className="absolute inset-0 bg-gradient-to-br from-green-500/20 to-emerald-500/20 flex items-center justify-center"
>
- <div className="text-center pointer-events-none">
- <div className="text-2xl">🎵</div>
+ <div className="text-center">
+ <div className="text-2xl mb-2">🎵</div>
<p className="text-xs text-white">{mediaItem.name}</p>
</div>
-
- {/* Hover resize corners */}
- <div className="absolute -top-1 -left-1 w-3 h-3 bg-blue-500 border border-white rounded-sm cursor-nw-resize opacity-0 group-hover:opacity-100 transition-opacity"
- onMouseDown={(e) => { e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-nw'); }}></div>
- <div className="absolute -top-1 -right-1 w-3 h-3 bg-blue-500 border border-white rounded-sm cursor-ne-resize opacity-0 group-hover:opacity-100 transition-opacity"
- onMouseDown={(e) => { e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-ne'); }}></div>
- <div className="absolute -bottom-1 -left-1 w-3 h-3 bg-blue-500 border border-white rounded-sm cursor-sw-resize opacity-0 group-hover:opacity-100 transition-opacity"
- onMouseDown={(e) => { e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-sw'); }}></div>
- <div className="absolute -bottom-1 -right-1 w-3 h-3 bg-blue-500 border border-white rounded-sm cursor-se-resize opacity-0 group-hover:opacity-100 transition-opacity"
- onMouseDown={(e) => { e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-se'); }}></div>
</div>
);
}
@@ -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 (
- <div className="h-full flex flex-col">
- {/* Canvas Controls */}
- <div className="border-b p-2 flex items-center gap-2 text-xs">
+ <div className="h-full w-full flex flex-col min-h-0 min-w-0">
+ {/* Controls */}
+ <div className="border-b p-2 flex items-center gap-2 text-xs flex-shrink-0">
<span className="text-muted-foreground">Canvas:</span>
<select
value={`${canvasSize.width}x${canvasSize.height}`}
@@ -421,7 +133,7 @@ export function PreviewPanel() {
const preset = canvasPresets.find(p => `${p.width}x${p.height}` === e.target.value);
if (preset) setCanvasSize({ width: preset.width, height: preset.height });
}}
- className="bg-background border rounded px-2 py-1"
+ className="bg-background border rounded px-2 py-1 text-xs"
>
{canvasPresets.map(preset => (
<option key={preset.name} value={`${preset.width}x${preset.height}`}>
@@ -430,70 +142,49 @@ export function PreviewPanel() {
))}
</select>
- <Button
- variant="outline"
- size="sm"
- onClick={toggle}
- className="ml-auto"
- >
+ <Button variant="outline" size="sm" onClick={toggle} className="ml-auto">
{isPlaying ? <Pause className="h-3 w-3 mr-1" /> : <Play className="h-3 w-3 mr-1" />}
{isPlaying ? "Pause" : "Play"}
</Button>
</div>
- {/* Preview Area - Full Width */}
- <div className="flex-1 flex items-center justify-center p-4 bg-gray-900">
+ {/* Preview Area */}
+ <div className="flex-1 flex items-center justify-center p-2 sm:p-4 bg-gray-900 min-h-0 min-w-0">
<div
ref={previewRef}
- className="relative overflow-hidden"
+ className="relative overflow-hidden rounded-sm max-w-full max-h-full bg-black border border-gray-600"
style={{
aspectRatio: aspectRatio.toString(),
- width: aspectRatio > 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 ? (
<div className="absolute inset-0 flex items-center justify-center text-white/50">
{tracks.length === 0 ? "Drop media to start editing" : "No clips at current time"}
</div>
) : (
- activeClips.map((clipData, index) => renderClipLayer(clipData, index))
+ activeClips.map((clipData, index) => renderClip(clipData, index))
)}
-
-
</div>
</div>
- {/* Bottom Info Panel */}
- <div className="border-t bg-background">
- {/* Layer List */}
- <div className="p-2 border-b">
- <div className="text-xs font-medium mb-2">Active Layers ({activeClips.length})</div>
- <div className="space-y-1 max-h-20 overflow-y-auto">
- {activeClips.map((clipData, index) => (
- <div
- key={clipData.clip.id}
- className="flex items-center gap-2 p-1 rounded text-xs hover:bg-muted/50"
- >
- <span className="w-4 h-4 bg-muted rounded text-center text-xs leading-4">
- {index + 1}
- </span>
- <span className="flex-1 truncate">{clipData.clip.name}</span>
- <span className="text-muted-foreground">{clipData.track.name}</span>
- </div>
- ))}
- </div>
+ {/* Info Panel */}
+ <div className="border-t bg-background p-2 flex-shrink-0">
+ <div className="text-xs font-medium mb-1">Active Clips ({activeClips.length})</div>
+ <div className="flex gap-2 overflow-x-auto">
+ {activeClips.map((clipData, index) => (
+ <div
+ key={clipData.clip.id}
+ className="flex items-center gap-1 px-2 py-1 bg-muted rounded text-xs whitespace-nowrap"
+ >
+ <span className="w-4 h-4 bg-primary/20 rounded text-center text-xs leading-4">
+ {index + 1}
+ </span>
+ <span>{clipData.clip.name}</span>
+ </div>
+ ))}
</div>
-
-
</div>
</div>
);
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<HTMLVideoElement>(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 (
- <div className={`relative group ${className}`}>
- <video
- ref={videoRef}
- src={src}
- poster={poster}
- className="w-full h-full object-cover"
- playsInline
- preload="auto"
- />
-
- <div className="absolute inset-0 bg-black/20 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />
-
- <div className="absolute bottom-2 left-2 right-2 flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
- <Button
- variant="ghost"
- size="icon"
- className="h-8 w-8 bg-black/50 text-white hover:bg-black/70"
- onClick={isPlaying ? pause : play}
- >
- {isPlaying ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4" />}
- </Button>
-
- <div className="flex-1 h-1 bg-white/30 rounded-full overflow-hidden">
- <div
- className="h-full bg-white transition-all duration-100"
- style={{ width: `${(currentTime / usePlaybackStore.getState().duration) * 100}%` }}
- />
- </div>
-
- <Button
- variant="ghost"
- size="icon"
- className="h-8 w-8 bg-black/50 text-white hover:bg-black/70"
- onClick={() => setVolume(volume > 0 ? 0 : 1)}
- >
- <Volume2 className="h-4 w-4" />
- </Button>
- </div>
- </div>
+ <video
+ ref={videoRef}
+ src={src}
+ poster={poster}
+ className={`w-full h-full object-cover ${className}`}
+ playsInline
+ preload="auto"
+ controls={false}
+ disablePictureInPicture
+ disableRemotePlayback
+ style={{ pointerEvents: 'none' }}
+ onContextMenu={(e) => e.preventDefault()}
+ />
);
} \ No newline at end of file
diff --git a/apps/web/src/stores/panel-store.ts b/apps/web/src/stores/panel-store.ts
index ea2dacb9..7706417a 100644
--- a/apps/web/src/stores/panel-store.ts
+++ b/apps/web/src/stores/panel-store.ts
@@ -2,78 +2,40 @@ import { create } from "zustand";
import { persist } from "zustand/middleware";
interface PanelState {
- // Horizontal panel sizes
+ // Panel sizes as percentages
toolsPanel: number;
previewPanel: number;
propertiesPanel: number;
-
- // Vertical panel sizes
mainContent: number;
timeline: number;
- // Flag to prevent initial overwrites
- isInitialized: boolean;
-
// Actions
setToolsPanel: (size: number) => void;
setPreviewPanel: (size: number) => void;
setPropertiesPanel: (size: number) => void;
setMainContent: (size: number) => void;
setTimeline: (size: number) => void;
- setInitialized: () => void;
}
export const usePanelStore = create<PanelState>()(
persist(
- (set, get) => ({
- // Default sizes
- toolsPanel: 20,
- previewPanel: 60,
+ (set) => ({
+ // Default sizes - optimized for responsiveness
+ toolsPanel: 25,
+ previewPanel: 75,
propertiesPanel: 20,
- mainContent: 50,
- timeline: 50,
- isInitialized: false,
+ mainContent: 70,
+ timeline: 30,
// Actions
- setToolsPanel: (size) => {
- const state = get();
- if (!state.isInitialized) return;
- set({ toolsPanel: size });
- },
- setPreviewPanel: (size) => {
- const state = get();
- if (!state.isInitialized) return;
- set({ previewPanel: size });
- },
- setPropertiesPanel: (size) => {
- const state = get();
- if (!state.isInitialized) return;
- set({ propertiesPanel: size });
- },
- setMainContent: (size) => {
- const state = get();
- if (!state.isInitialized) return;
- set({ mainContent: size });
- },
- setTimeline: (size) => {
- const state = get();
- if (!state.isInitialized) return;
- set({ timeline: size });
- },
- setInitialized: () => {
- console.log("Panel store initialized for resize events");
- set({ isInitialized: true });
- },
+ setToolsPanel: (size) => set({ toolsPanel: size }),
+ setPreviewPanel: (size) => set({ previewPanel: size }),
+ setPropertiesPanel: (size) => set({ propertiesPanel: size }),
+ setMainContent: (size) => set({ mainContent: size }),
+ setTimeline: (size) => set({ timeline: size }),
}),
{
name: "panel-sizes",
- partialize: (state) => ({
- toolsPanel: state.toolsPanel,
- previewPanel: state.previewPanel,
- propertiesPanel: state.propertiesPanel,
- mainContent: state.mainContent,
- timeline: state.timeline,
- }),
}
)
);