clean up and simplifying more logic

This commit is contained in:
Hyteq
2025-06-24 08:10:11 +03:00
parent 55c95cd574
commit dc35619017
5 changed files with 156 additions and 544 deletions

View File

@ -42,59 +42,71 @@ export default function Editor() {
return ( return (
<EditorProvider> <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 /> <EditorHeader />
<ResizablePanelGroup direction="vertical"> <div className="flex-1 min-h-0 min-w-0">
<ResizablePanel <ResizablePanelGroup direction="vertical" className="h-full w-full">
defaultSize={mainContent} <ResizablePanel
minSize={30} defaultSize={mainContent}
onResize={setMainContent} minSize={30}
> maxSize={85}
{/* Main content area */} onResize={setMainContent}
<ResizablePanelGroup direction="horizontal"> className="min-h-0"
{/* Tools Panel */} >
<ResizablePanel {/* Main content area */}
defaultSize={toolsPanel} <ResizablePanelGroup direction="horizontal" className="h-full w-full">
minSize={15} {/* Tools Panel */}
onResize={setToolsPanel} <ResizablePanel
> defaultSize={toolsPanel}
<MediaPanel /> minSize={15}
</ResizablePanel> maxSize={40}
onResize={setToolsPanel}
className="min-w-0"
>
<MediaPanel />
</ResizablePanel>
<ResizableHandle withHandle /> <ResizableHandle withHandle />
{/* Preview Area */} {/* Preview Area */}
<ResizablePanel <ResizablePanel
defaultSize={previewPanel} defaultSize={previewPanel}
onResize={setPreviewPanel} minSize={30}
> onResize={setPreviewPanel}
<PreviewPanel /> className="min-w-0 min-h-0 flex-1"
</ResizablePanel> >
<PreviewPanel />
</ResizablePanel>
<ResizableHandle withHandle /> <ResizableHandle withHandle />
{/* Properties Panel */} {/* Properties Panel - Hidden for now but ready */}
{/* <ResizablePanel {/* <ResizablePanel
defaultSize={propertiesPanel} defaultSize={propertiesPanel}
minSize={15} minSize={15}
onResize={setPropertiesPanel} maxSize={40}
> onResize={setPropertiesPanel}
<PropertiesPanel /> className="min-w-0"
</ResizablePanel> */} >
</ResizablePanelGroup> <PropertiesPanel />
</ResizablePanel> </ResizablePanel> */}
</ResizablePanelGroup>
</ResizablePanel>
<ResizableHandle withHandle /> <ResizableHandle withHandle />
{/* Timeline */} {/* Timeline */}
<ResizablePanel <ResizablePanel
defaultSize={timeline} defaultSize={timeline}
minSize={15} minSize={15}
onResize={setTimeline} maxSize={70}
> onResize={setTimeline}
<Timeline /> className="min-h-0"
</ResizablePanel> >
</ResizablePanelGroup> <Timeline />
</ResizablePanel>
</ResizablePanelGroup>
</div>
</div> </div>
</EditorProvider> </EditorProvider>
); );

View File

@ -3,7 +3,6 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
import { useEditorStore } from "@/stores/editor-store"; import { useEditorStore } from "@/stores/editor-store";
import { usePanelStore } from "@/stores/panel-store";
interface EditorProviderProps { interface EditorProviderProps {
children: React.ReactNode; children: React.ReactNode;
@ -11,19 +10,10 @@ interface EditorProviderProps {
export function EditorProvider({ children }: EditorProviderProps) { export function EditorProvider({ children }: EditorProviderProps) {
const { isInitializing, isPanelsReady, initializeApp } = useEditorStore(); const { isInitializing, isPanelsReady, initializeApp } = useEditorStore();
const { setInitialized } = usePanelStore();
useEffect(() => { useEffect(() => {
const initialize = async () => { initializeApp();
// Initialize the app }, [initializeApp]);
await initializeApp();
// Initialize panel store for future resize events
setInitialized();
};
initialize();
}, [initializeApp, setInitialized]);
// Show loading screen while initializing // Show loading screen while initializing
if (isInitializing || !isPanelsReady) { if (isInitializing || !isPanelsReady) {

View File

@ -5,53 +5,25 @@ import { useMediaStore } from "@/stores/media-store";
import { usePlaybackStore } from "@/stores/playback-store"; import { usePlaybackStore } from "@/stores/playback-store";
import { VideoPlayer } from "@/components/ui/video-player"; import { VideoPlayer } from "@/components/ui/video-player";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Play, Pause, Move, RotateCw, Crop, ZoomIn, ZoomOut } from "lucide-react"; import { Play, Pause } from "lucide-react";
import { useState, useRef, useEffect, useCallback } from "react"; import { useState, useRef } 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() { export function PreviewPanel() {
const { tracks } = useTimelineStore(); const { tracks } = useTimelineStore();
const { mediaItems } = useMediaStore(); const { mediaItems } = useMediaStore();
const { isPlaying, toggle, currentTime } = usePlaybackStore(); const { isPlaying, toggle, currentTime } = usePlaybackStore();
const [canvasSize, setCanvasSize] = useState({ width: 1920, height: 1080 });
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 previewRef = useRef<HTMLDivElement>(null); const previewRef = useRef<HTMLDivElement>(null);
// Get all active clips at current time (for overlaying) // Get active clips at current time
const getActiveClips = () => { const getActiveClips = () => {
const activeClips: Array<{ const activeClips: Array<{
clip: any; clip: any;
track: any; track: any;
mediaItem: any; mediaItem: any;
layer: number;
}> = []; }> = [];
tracks.forEach((track, trackIndex) => { tracks.forEach((track) => {
track.clips.forEach((clip) => { track.clips.forEach((clip) => {
const clipStart = clip.startTime; const clipStart = clip.startTime;
const clipEnd = clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); const clipEnd = clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd);
@ -62,338 +34,78 @@ export function PreviewPanel() {
: mediaItems.find((item) => item.id === clip.mediaId); : mediaItems.find((item) => item.id === clip.mediaId);
if (mediaItem || clip.mediaId === "test") { if (mediaItem || clip.mediaId === "test") {
activeClips.push({ activeClips.push({ clip, track, mediaItem });
clip,
track,
mediaItem,
layer: trackIndex, // Track index determines layer order
});
} }
} }
}); });
}); });
// Sort by layer (track order) - higher index = on top return activeClips;
return activeClips.sort((a, b) => a.layer - b.layer);
}; };
const activeClips = getActiveClips(); const activeClips = getActiveClips();
const aspectRatio = canvasSize.width / canvasSize.height; const aspectRatio = canvasSize.width / canvasSize.height;
// Get or create transform for a clip // Render a clip
const getClipTransform = (clipId: string): ClipTransform => { const renderClip = (clipData: any, index: number) => {
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) => {
const { clip, mediaItem } = clipData; const { clip, mediaItem } = clipData;
const transform = getClipTransform(clip.id);
const layerStyle = { // Test clips
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") { if (!mediaItem || clip.mediaId === "test") {
return ( return (
<div <div
key={clip.id} key={clip.id}
style={layerStyle} className="absolute inset-0 bg-gradient-to-br from-blue-500/20 to-purple-500/20 flex items-center justify-center"
onMouseDown={handleClipMouseDown}
className="bg-gradient-to-br from-blue-500/20 to-purple-500/20 flex items-center justify-center group"
> >
<div className="text-center pointer-events-none"> <div className="text-center">
<div className="text-2xl">🎬</div> <div className="text-2xl mb-2">🎬</div>
<p className="text-xs text-white">{clip.name}</p> <p className="text-xs text-white">{clip.name}</p>
</div> </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> </div>
); );
} }
// Render video // Video clips
if (mediaItem.type === "video") { if (mediaItem.type === "video") {
return ( return (
<div key={clip.id} style={layerStyle} onMouseDown={handleClipMouseDown} className="group"> <div key={clip.id} className="absolute inset-0">
<VideoPlayer <VideoPlayer
src={mediaItem.url} src={mediaItem.url}
poster={mediaItem.thumbnailUrl} poster={mediaItem.thumbnailUrl}
className="w-full h-full pointer-events-none"
clipStartTime={clip.startTime} clipStartTime={clip.startTime}
trimStart={clip.trimStart} trimStart={clip.trimStart}
trimEnd={clip.trimEnd} trimEnd={clip.trimEnd}
clipDuration={clip.duration} 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> </div>
); );
} }
// Render image // Image clips
if (mediaItem.type === "image") { if (mediaItem.type === "image") {
return ( return (
<div key={clip.id} style={layerStyle} onMouseDown={handleClipMouseDown} className="group"> <div key={clip.id} className="absolute inset-0">
<img <img
src={mediaItem.url} src={mediaItem.url}
alt={mediaItem.name} alt={mediaItem.name}
className="w-full h-full object-cover rounded pointer-events-none" className="w-full h-full object-cover"
draggable={false} 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> </div>
); );
} }
// Render audio (visual representation) // Audio clips (visual representation)
if (mediaItem.type === "audio") { if (mediaItem.type === "audio") {
return ( return (
<div <div
key={clip.id} key={clip.id}
style={layerStyle} className="absolute inset-0 bg-gradient-to-br from-green-500/20 to-emerald-500/20 flex items-center justify-center"
onMouseDown={handleClipMouseDown}
className="bg-gradient-to-br from-green-500/20 to-emerald-500/20 flex items-center justify-center group"
> >
<div className="text-center pointer-events-none"> <div className="text-center">
<div className="text-2xl">🎵</div> <div className="text-2xl mb-2">🎵</div>
<p className="text-xs text-white">{mediaItem.name}</p> <p className="text-xs text-white">{mediaItem.name}</p>
</div> </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> </div>
); );
} }
@ -401,7 +113,7 @@ export function PreviewPanel() {
return null; return null;
}; };
// Canvas size presets // Canvas presets
const canvasPresets = [ const canvasPresets = [
{ name: "16:9 HD", width: 1920, height: 1080 }, { name: "16:9 HD", width: 1920, height: 1080 },
{ name: "16:9 4K", width: 3840, height: 2160 }, { name: "16:9 4K", width: 3840, height: 2160 },
@ -411,9 +123,9 @@ export function PreviewPanel() {
]; ];
return ( return (
<div className="h-full flex flex-col"> <div className="h-full w-full flex flex-col min-h-0 min-w-0">
{/* Canvas Controls */} {/* Controls */}
<div className="border-b p-2 flex items-center gap-2 text-xs"> <div className="border-b p-2 flex items-center gap-2 text-xs flex-shrink-0">
<span className="text-muted-foreground">Canvas:</span> <span className="text-muted-foreground">Canvas:</span>
<select <select
value={`${canvasSize.width}x${canvasSize.height}`} 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); const preset = canvasPresets.find(p => `${p.width}x${p.height}` === e.target.value);
if (preset) setCanvasSize({ width: preset.width, height: preset.height }); 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 => ( {canvasPresets.map(preset => (
<option key={preset.name} value={`${preset.width}x${preset.height}`}> <option key={preset.name} value={`${preset.width}x${preset.height}`}>
@ -430,70 +142,49 @@ export function PreviewPanel() {
))} ))}
</select> </select>
<Button <Button variant="outline" size="sm" onClick={toggle} className="ml-auto">
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 className="h-3 w-3 mr-1" /> : <Play className="h-3 w-3 mr-1" />}
{isPlaying ? "Pause" : "Play"} {isPlaying ? "Pause" : "Play"}
</Button> </Button>
</div> </div>
{/* Preview Area - Full Width */} {/* Preview Area */}
<div className="flex-1 flex items-center justify-center p-4 bg-gray-900"> <div className="flex-1 flex items-center justify-center p-2 sm:p-4 bg-gray-900 min-h-0 min-w-0">
<div <div
ref={previewRef} 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={{ style={{
aspectRatio: aspectRatio.toString(), aspectRatio: aspectRatio.toString(),
width: aspectRatio > 1 ? "100%" : "auto", width: "100%",
height: aspectRatio <= 1 ? "100%" : "auto", height: "100%",
maxWidth: "100%",
maxHeight: "100%",
background: '#000000',
border: '1px solid #374151'
}} }}
> >
{/* Render all active clips as layers */}
{activeClips.length === 0 ? ( {activeClips.length === 0 ? (
<div className="absolute inset-0 flex items-center justify-center text-white/50"> <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"} {tracks.length === 0 ? "Drop media to start editing" : "No clips at current time"}
</div> </div>
) : ( ) : (
activeClips.map((clipData, index) => renderClipLayer(clipData, index)) activeClips.map((clipData, index) => renderClip(clipData, index))
)} )}
</div> </div>
</div> </div>
{/* Bottom Info Panel */} {/* Info Panel */}
<div className="border-t bg-background"> <div className="border-t bg-background p-2 flex-shrink-0">
{/* Layer List */} <div className="text-xs font-medium mb-1">Active Clips ({activeClips.length})</div>
<div className="p-2 border-b"> <div className="flex gap-2 overflow-x-auto">
<div className="text-xs font-medium mb-2">Active Layers ({activeClips.length})</div> {activeClips.map((clipData, index) => (
<div className="space-y-1 max-h-20 overflow-y-auto"> <div
{activeClips.map((clipData, index) => ( key={clipData.clip.id}
<div className="flex items-center gap-1 px-2 py-1 bg-muted rounded text-xs whitespace-nowrap"
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-primary/20 rounded text-center text-xs leading-4">
> {index + 1}
<span className="w-4 h-4 bg-muted rounded text-center text-xs leading-4"> </span>
{index + 1} <span>{clipData.clip.name}</span>
</span> </div>
<span className="flex-1 truncate">{clipData.clip.name}</span> ))}
<span className="text-muted-foreground">{clipData.track.name}</span>
</div>
))}
</div>
</div> </div>
</div> </div>
</div> </div>
); );

View File

@ -1,8 +1,6 @@
"use client"; "use client";
import { useRef, useEffect } from "react"; import { useRef, useEffect } from "react";
import { Button } from "./button";
import { Play, Pause, Volume2 } from "lucide-react";
import { usePlaybackStore } from "@/stores/playback-store"; import { usePlaybackStore } from "@/stores/playback-store";
interface VideoPlayerProps { interface VideoPlayerProps {
@ -25,128 +23,87 @@ export function VideoPlayer({
clipDuration clipDuration
}: VideoPlayerProps) { }: VideoPlayerProps) {
const videoRef = useRef<HTMLVideoElement>(null); 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 // Calculate if we're within this clip's timeline range
const clipEndTime = clipStartTime + (clipDuration - trimStart - trimEnd); const clipEndTime = clipStartTime + (clipDuration - trimStart - trimEnd);
const isInClipRange = currentTime >= clipStartTime && currentTime < clipEndTime; const isInClipRange = currentTime >= clipStartTime && currentTime < clipEndTime;
// Calculate the video's internal time based on timeline position // Sync playback events
const videoTime = Math.max(trimStart, Math.min(
clipDuration - trimEnd,
currentTime - clipStartTime + trimStart
));
useEffect(() => { useEffect(() => {
const video = videoRef.current; const video = videoRef.current;
if (!video) return; if (!video || !isInClipRange) return;
const handleSeekEvent = (e: CustomEvent) => { const handleSeek = (e: CustomEvent) => {
if (!isInClipRange) return;
const timelineTime = e.detail.time; const timelineTime = e.detail.time;
const newVideoTime = Math.max(trimStart, Math.min( const videoTime = Math.max(trimStart, Math.min(
clipDuration - trimEnd, clipDuration - trimEnd,
timelineTime - clipStartTime + trimStart timelineTime - clipStartTime + trimStart
)); ));
video.currentTime = newVideoTime; video.currentTime = videoTime;
}; };
const handleUpdateEvent = (e: CustomEvent) => { const handleUpdate = (e: CustomEvent) => {
if (!isInClipRange) return;
const timelineTime = e.detail.time; const timelineTime = e.detail.time;
const targetVideoTime = Math.max(trimStart, Math.min( const targetTime = Math.max(trimStart, Math.min(
clipDuration - trimEnd, clipDuration - trimEnd,
timelineTime - clipStartTime + trimStart timelineTime - clipStartTime + trimStart
)); ));
// Only sync if there's a significant difference to avoid micro-adjustments if (Math.abs(video.currentTime - targetTime) > 0.5) {
if (Math.abs(video.currentTime - targetVideoTime) > 0.5) { video.currentTime = targetTime;
video.currentTime = targetVideoTime;
} }
}; };
const handleSpeedEvent = (e: CustomEvent) => { const handleSpeed = (e: CustomEvent) => {
if (!isInClipRange) return;
// Set playbackRate directly without any additional checks
video.playbackRate = e.detail.speed; video.playbackRate = e.detail.speed;
}; };
window.addEventListener("playback-seek", handleSeekEvent as EventListener); window.addEventListener("playback-seek", handleSeek as EventListener);
window.addEventListener("playback-update", handleUpdateEvent as EventListener); window.addEventListener("playback-update", handleUpdate as EventListener);
window.addEventListener("playback-speed", handleSpeedEvent as EventListener); window.addEventListener("playback-speed", handleSpeed as EventListener);
return () => { return () => {
window.removeEventListener("playback-seek", handleSeekEvent as EventListener); window.removeEventListener("playback-seek", handleSeek as EventListener);
window.removeEventListener("playback-update", handleUpdateEvent as EventListener); window.removeEventListener("playback-update", handleUpdate as EventListener);
window.removeEventListener("playback-speed", handleSpeedEvent as EventListener); window.removeEventListener("playback-speed", handleSpeed as EventListener);
}; };
}, [clipStartTime, trimStart, trimEnd, clipDuration, isInClipRange]); }, [clipStartTime, trimStart, trimEnd, clipDuration, isInClipRange]);
// Sync video playback state - only play if in clip range // Sync playback state
useEffect(() => { useEffect(() => {
const video = videoRef.current; const video = videoRef.current;
if (!video) return; if (!video) return;
if (isPlaying && isInClipRange) { if (isPlaying && isInClipRange) {
video.play().catch(console.error); video.play().catch(() => { });
} else { } else {
video.pause(); video.pause();
} }
}, [isPlaying, isInClipRange]); }, [isPlaying, isInClipRange]);
// Sync volume // Sync volume and speed
useEffect(() => { useEffect(() => {
const video = videoRef.current; const video = videoRef.current;
if (!video) return; if (!video) return;
video.volume = volume;
}, [volume]);
// Sync speed immediately when it changes video.volume = volume;
useEffect(() => {
const video = videoRef.current;
if (!video) return;
video.playbackRate = speed; video.playbackRate = speed;
}, [speed]); }, [volume, speed]);
return ( return (
<div className={`relative group ${className}`}> <video
<video ref={videoRef}
ref={videoRef} src={src}
src={src} poster={poster}
poster={poster} className={`w-full h-full object-cover ${className}`}
className="w-full h-full object-cover" playsInline
playsInline preload="auto"
preload="auto" controls={false}
/> disablePictureInPicture
disableRemotePlayback
<div className="absolute inset-0 bg-black/20 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" /> style={{ pointerEvents: 'none' }}
onContextMenu={(e) => e.preventDefault()}
<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>
); );
} }

View File

@ -2,78 +2,40 @@ import { create } from "zustand";
import { persist } from "zustand/middleware"; import { persist } from "zustand/middleware";
interface PanelState { interface PanelState {
// Horizontal panel sizes // Panel sizes as percentages
toolsPanel: number; toolsPanel: number;
previewPanel: number; previewPanel: number;
propertiesPanel: number; propertiesPanel: number;
// Vertical panel sizes
mainContent: number; mainContent: number;
timeline: number; timeline: number;
// Flag to prevent initial overwrites
isInitialized: boolean;
// Actions // Actions
setToolsPanel: (size: number) => void; setToolsPanel: (size: number) => void;
setPreviewPanel: (size: number) => void; setPreviewPanel: (size: number) => void;
setPropertiesPanel: (size: number) => void; setPropertiesPanel: (size: number) => void;
setMainContent: (size: number) => void; setMainContent: (size: number) => void;
setTimeline: (size: number) => void; setTimeline: (size: number) => void;
setInitialized: () => void;
} }
export const usePanelStore = create<PanelState>()( export const usePanelStore = create<PanelState>()(
persist( persist(
(set, get) => ({ (set) => ({
// Default sizes // Default sizes - optimized for responsiveness
toolsPanel: 20, toolsPanel: 25,
previewPanel: 60, previewPanel: 75,
propertiesPanel: 20, propertiesPanel: 20,
mainContent: 50, mainContent: 70,
timeline: 50, timeline: 30,
isInitialized: false,
// Actions // Actions
setToolsPanel: (size) => { setToolsPanel: (size) => set({ toolsPanel: size }),
const state = get(); setPreviewPanel: (size) => set({ previewPanel: size }),
if (!state.isInitialized) return; setPropertiesPanel: (size) => set({ propertiesPanel: size }),
set({ toolsPanel: size }); setMainContent: (size) => set({ mainContent: size }),
}, setTimeline: (size) => set({ timeline: 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 });
},
}), }),
{ {
name: "panel-sizes", name: "panel-sizes",
partialize: (state) => ({
toolsPanel: state.toolsPanel,
previewPanel: state.previewPanel,
propertiesPanel: state.propertiesPanel,
mainContent: state.mainContent,
timeline: state.timeline,
}),
} }
) )
); );