diff options
| author | Hyteq <DerpsDer.es@gmail.com> | 2025-06-23 15:39:56 +0300 |
|---|---|---|
| committer | Hyteq <DerpsDer.es@gmail.com> | 2025-06-23 15:39:56 +0300 |
| commit | 294ba01abeb2d8c3d3a04cb7ecc54abf97a6d4d3 (patch) | |
| tree | 6eb4288231e9af56c08d78e96079b70fff9461c2 | |
| parent | 61c172c9cce27f7f062d3fb6fb82f4b040efb35f (diff) | |
| download | opencut-294ba01abeb2d8c3d3a04cb7ecc54abf97a6d4d3.tar.gz | |
feat: initial state for composition and overlay editing on the video player
| -rw-r--r-- | apps/web/src/app/editor/page.tsx | 6 | ||||
| -rw-r--r-- | apps/web/src/components/editor/preview-panel.tsx | 532 |
2 files changed, 447 insertions, 91 deletions
diff --git a/apps/web/src/app/editor/page.tsx b/apps/web/src/app/editor/page.tsx index 2f825a22..64186a3e 100644 --- a/apps/web/src/app/editor/page.tsx +++ b/apps/web/src/app/editor/page.tsx @@ -7,7 +7,7 @@ import { ResizableHandle, } from "../../components/ui/resizable"; import { MediaPanel } from "../../components/editor/media-panel"; -import { PropertiesPanel } from "../../components/editor/properties-panel"; +// import { PropertiesPanel } from "../../components/editor/properties-panel"; import { Timeline } from "../../components/editor/timeline"; import { PreviewPanel } from "../../components/editor/preview-panel"; import { EditorHeader } from "@/components/editor-header"; @@ -74,13 +74,13 @@ export default function Editor() { <ResizableHandle withHandle /> {/* Properties Panel */} - <ResizablePanel + {/* <ResizablePanel defaultSize={propertiesPanel} minSize={15} onResize={setPropertiesPanel} > <PropertiesPanel /> - </ResizablePanel> + </ResizablePanel> */} </ResizablePanelGroup> </ResizablePanel> diff --git a/apps/web/src/components/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx index 132d6c06..5d03e941 100644 --- a/apps/web/src/components/editor/preview-panel.tsx +++ b/apps/web/src/components/editor/preview-panel.tsx @@ -3,110 +3,397 @@ import { useTimelineStore } from "@/stores/timeline-store";
import { useMediaStore } from "@/stores/media-store";
import { usePlaybackStore } from "@/stores/playback-store";
-import { ImageTimelineTreatment } from "@/components/ui/image-timeline-treatment";
import { VideoPlayer } from "@/components/ui/video-player";
import { Button } from "@/components/ui/button";
-import { Play, Pause } from "lucide-react";
+import { Play, Pause, Move, RotateCw, Crop, ZoomIn, ZoomOut } from "lucide-react";
+import { useState, useRef, useEffect, useCallback } from "react";
+
+interface ClipTransform {
+ x: number;
+ y: number;
+ scale: number;
+ rotation: number;
+ opacity: number;
+ width: number;
+ height: number;
+ blendMode: string;
+ cropTop: number;
+ cropBottom: number;
+ cropLeft: number;
+ cropRight: number;
+}
+
+interface DragState {
+ isDragging: boolean;
+ dragType: 'move' | 'resize-nw' | 'resize-ne' | 'resize-sw' | 'resize-se' | 'rotate' | 'scale' | 'crop-n' | 'crop-s' | 'crop-e' | 'crop-w';
+ startMouseX: number;
+ startMouseY: number;
+ startTransform: ClipTransform;
+ clipId: string;
+}
export function PreviewPanel() {
const { tracks } = useTimelineStore();
const { mediaItems } = useMediaStore();
const { isPlaying, toggle, currentTime } = usePlaybackStore();
- // Find the active clip at the current playback time
- const getActiveClip = () => {
- for (const track of tracks) {
- for (const clip of track.clips) {
+ const [clipTransforms, setClipTransforms] = useState<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);
+
+ // Get all active clips at current time (for overlaying)
+ const getActiveClips = () => {
+ const activeClips: Array<{
+ clip: any;
+ track: any;
+ mediaItem: any;
+ layer: number;
+ }> = [];
+
+ tracks.forEach((track, trackIndex) => {
+ track.clips.forEach((clip) => {
const clipStart = clip.startTime;
const clipEnd = clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd);
if (currentTime >= clipStart && currentTime < clipEnd) {
- return clip;
+ const mediaItem = clip.mediaId === "test"
+ ? { type: "test", name: clip.name, url: "", thumbnailUrl: "" }
+ : mediaItems.find((item) => item.id === clip.mediaId);
+
+ if (mediaItem || clip.mediaId === "test") {
+ activeClips.push({
+ clip,
+ track,
+ mediaItem,
+ layer: trackIndex, // Track index determines layer order
+ });
+ }
}
- }
- }
- return null;
+ });
+ });
+
+ // Sort by layer (track order) - higher index = on top
+ return activeClips.sort((a, b) => a.layer - b.layer);
};
- const activeClip = getActiveClip();
- const activeMediaItem = activeClip
- ? mediaItems.find((item) => item.id === activeClip.mediaId)
- : null;
+ const activeClips = getActiveClips();
+ const aspectRatio = canvasSize.width / canvasSize.height;
- const aspectRatio = activeMediaItem?.aspectRatio || 16 / 9;
+ // Get or create transform for a clip
+ const getClipTransform = (clipId: string): ClipTransform => {
+ return clipTransforms[clipId] || {
+ x: 0,
+ y: 0,
+ scale: 1,
+ rotation: 0,
+ opacity: 1,
+ width: 100, // Percentage of canvas
+ height: 100,
+ blendMode: 'normal',
+ cropTop: 0,
+ cropBottom: 0,
+ cropLeft: 0,
+ cropRight: 0,
+ };
+ };
- const renderContent = () => {
- if (!activeClip) {
- return (
- <div className="absolute inset-0 flex items-center justify-center text-muted-foreground/50">
- {tracks.length === 0 ? "Drop media to start editing" : "No clip at current time"}
- </div>
- );
+ // 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
+
+
- // Handle test clips without media items
- if (!activeMediaItem && activeClip.mediaId === "test") {
+ // Render a single clip layer
+ const renderClipLayer = (clipData: any, index: number) => {
+ const { clip, mediaItem } = clipData;
+ const transform = getClipTransform(clip.id);
+
+ const layerStyle = {
+ position: 'absolute' as const,
+ left: '50%',
+ top: '50%',
+ width: `${transform.width}%`,
+ height: `${transform.height}%`,
+ transform: `translate(-50%, -50%) translate(${transform.x}%, ${transform.y}%) scale(${transform.scale}) rotate(${transform.rotation}deg)`,
+ opacity: transform.opacity,
+ mixBlendMode: transform.blendMode as any,
+ clipPath: `inset(${transform.cropTop}% ${transform.cropRight}% ${transform.cropBottom}% ${transform.cropLeft}%)`,
+ zIndex: index + 10,
+ cursor: dragState?.isDragging && dragState.clipId === clip.id ? 'grabbing' : 'grab',
+ userSelect: 'none' as const,
+ };
+
+ const handleClipMouseDown = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ handleMouseDown(e, clip.id, 'move');
+ };
+
+ // Handle test clips
+ if (!mediaItem || clip.mediaId === "test") {
return (
- <div className="absolute inset-0 flex items-center justify-center bg-gradient-to-br from-blue-500/20 to-purple-500/20">
- <div className="text-center">
- <div className="text-6xl mb-4">🎬</div>
- <p className="text-muted-foreground">{activeClip.name}</p>
- <p className="text-xs text-muted-foreground/70 mt-2">Test clip for playback</p>
+ <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"
+ >
+ <div className="text-center pointer-events-none">
+ <div className="text-2xl">🎬</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>
);
}
- if (!activeMediaItem) {
+ // Render video
+ if (mediaItem.type === "video") {
return (
- <div className="absolute inset-0 flex items-center justify-center text-muted-foreground/50">
- Media not found
+ <div key={clip.id} style={layerStyle} onMouseDown={handleClipMouseDown} className="group">
+ <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>
);
}
- if (activeMediaItem.type === "video") {
+ // Render image
+ if (mediaItem.type === "image") {
return (
- <VideoPlayer
- src={activeMediaItem.url}
- poster={activeMediaItem.thumbnailUrl}
- className="w-full h-full"
- clipStartTime={activeClip.startTime}
- trimStart={activeClip.trimStart}
- trimEnd={activeClip.trimEnd}
- clipDuration={activeClip.duration}
- key={`${activeClip.id}-${activeClip.trimStart}-${activeClip.trimEnd}`}
- />
- );
- }
+ <div key={clip.id} style={layerStyle} onMouseDown={handleClipMouseDown} className="group">
+ <img
+ src={mediaItem.url}
+ alt={mediaItem.name}
+ className="w-full h-full object-cover rounded pointer-events-none"
+ draggable={false}
+ />
- if (activeMediaItem.type === "image") {
- return (
- <ImageTimelineTreatment
- src={activeMediaItem.url}
- alt={activeMediaItem.name}
- targetAspectRatio={aspectRatio}
- className="w-full h-full"
- backgroundType="blur"
- />
+ {/* 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>
);
}
- if (activeMediaItem.type === "audio") {
+ // Render audio (visual representation)
+ if (mediaItem.type === "audio") {
return (
- <div className="absolute inset-0 flex items-center justify-center bg-gradient-to-br from-green-500/20 to-emerald-500/20">
- <div className="text-center">
- <div className="text-6xl mb-4">🎵</div>
- <p className="text-muted-foreground">{activeMediaItem.name}</p>
- <Button
- variant="outline"
- className="mt-4"
- onClick={toggle}
- >
- {isPlaying ? <Pause className="h-4 w-4 mr-2" /> : <Play className="h-4 w-4 mr-2" />}
- {isPlaying ? "Pause" : "Play"}
- </Button>
+ <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"
+ >
+ <div className="text-center pointer-events-none">
+ <div className="text-2xl">🎵</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>
);
}
@@ -114,31 +401,100 @@ export function PreviewPanel() { return null;
};
+ // Canvas size presets
+ const canvasPresets = [
+ { name: "16:9 HD", width: 1920, height: 1080 },
+ { name: "16:9 4K", width: 3840, height: 2160 },
+ { name: "9:16 Mobile", width: 1080, height: 1920 },
+ { name: "1:1 Square", width: 1080, height: 1080 },
+ { name: "4:3 Standard", width: 1440, height: 1080 },
+ ];
+
return (
- <div className="h-full flex flex-col items-center justify-center p-4 overflow-hidden">
- <div
- className="bg-black rounded-lg shadow-lg relative overflow-hidden flex-shrink-0"
- style={{
- aspectRatio: aspectRatio.toString(),
- width: aspectRatio > 1 ? "100%" : "auto",
- height: aspectRatio <= 1 ? "100%" : "auto",
- maxWidth: "100%",
- maxHeight: "100%",
- }}
- >
- {renderContent()}
+ <div className="h-full flex flex-col">
+ {/* Canvas Controls */}
+ <div className="border-b p-2 flex items-center gap-2 text-xs">
+ <span className="text-muted-foreground">Canvas:</span>
+ <select
+ value={`${canvasSize.width}x${canvasSize.height}`}
+ onChange={(e) => {
+ 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"
+ >
+ {canvasPresets.map(preset => (
+ <option key={preset.name} value={`${preset.width}x${preset.height}`}>
+ {preset.name} ({preset.width}×{preset.height})
+ </option>
+ ))}
+ </select>
+
+ <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">
+ <div
+ ref={previewRef}
+ className="relative overflow-hidden"
+ style={{
+ aspectRatio: aspectRatio.toString(),
+ width: aspectRatio > 1 ? "100%" : "auto",
+ height: aspectRatio <= 1 ? "100%" : "auto",
+ maxWidth: "100%",
+ maxHeight: "100%",
+ background: '#000000',
+ border: '1px solid #374151'
+ }}
+
+ >
+
+
+ {/* Render all active clips as layers */}
+ {activeClips.length === 0 ? (
+ <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))
+ )}
+
+
+ </div>
</div>
- {activeMediaItem && (
- <div className="mt-4 text-center">
- <p className="text-sm text-muted-foreground">
- {activeMediaItem.name}
- </p>
- <p className="text-xs text-muted-foreground/70">
- {aspectRatio.toFixed(2)} • {aspectRatio > 1 ? "Landscape" : aspectRatio < 1 ? "Portrait" : "Square"}
- </p>
+ {/* 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>
</div>
- )}
+
+
+ </div>
</div>
);
}
|
