clean up and simplifying more logic
This commit is contained in:
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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) {
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -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,
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
Reference in New Issue
Block a user