import React, { useState, useMemo, useCallback, useEffect, useRef } from 'react'; import { ExportModal } from '../export/ExportModal'; import { StudioToolbar, PanelType } from '../StudioToolbar'; import { StudioWorkspace } from '../StudioWorkspace'; import { CanvasZoomControls } from '../ui/CanvasZoomControls'; import { PlaybackInfo } from '../ui/PlaybackInfo'; import { StudioProperties } from '../StudioProperties'; import { StudioTimeline } from '../StudioTimeline'; import { MediaLibraryPanel } from '../MediaLibraryPanel'; import { TextPanel } from '../panels/TextPanel'; import { StickersPanel } from '../panels/StickersPanel'; import { AudioPanel } from '../panels/AudioPanel'; import { ShapesPanel } from '../panels/ShapesPanel'; import { SoundEffectsPanel } from '../panels/SoundEffectsPanel'; import { ShortcutsOverlay } from '../ui/ShortcutsOverlay'; import { RenderHistoryPanel } from '../ui/RenderHistoryPanel'; import { ElementSearch } from '../ui/ElementSearch'; import { TimelineMarkerList, TimelineMarker } from '../timeline/TimelineMarkerList'; import { ResponsivePreviewToggle } from '../ui/ResponsivePreviewToggle'; import { AutoSaveIndicator } from '../ui/AutoSaveIndicator'; import { CanvasGridOverlay } from '../ui/CanvasGridOverlay'; import { useEditor } from '../../context/EditorContext'; import { useKeyboardShortcuts } from '../../hooks/useKeyboardShortcuts'; import { useCanvasShortcuts } from '../../hooks/useCanvasShortcuts'; import { RenderProps, TimelineElement } from '../../types'; /** * StudioEditor: The main editing view. * Reads all state from EditorContext — no prop drilling needed. */ export const StudioEditor: React.FC<{ onAssetSaved?: (url: string) => void }> = ({ onAssetSaved }) => { const { timelineElements, setTimelineElements, layers, setLayers, selectedElementId, setSelectedElementId, activeLayerId, setActiveLayerId, activeTool, setActiveTool, designMD, textOverlay, setTextOverlay, playerRef, outputFormat, aspectRatio, setAspectRatio, timelineZoom, setTimelineZoom, timeUnit, setTimeUnit, durationInFrames, canvasZoom, setCanvasZoom, undo, redo, brandContent, brandVisibility, setBrandVisibility, activeAction, setActiveAction, selectedElementIds, toggleElementSelection, clearSelection, editingBrandAsset, } = useEditor(); // Panel state (replaces old activeTool for toolbar) const [activePanel, setActivePanel] = useState(null); const [showExportModal, setShowExportModal] = useState(false); const [showShortcuts, setShowShortcuts] = useState(false); const [showRenderHistory, setShowRenderHistory] = useState(false); const [showElementSearch, setShowElementSearch] = useState(false); const [markers, setMarkers] = useState([]); const [previewMode, setPreviewMode] = useState<'desktop' | 'tablet' | 'phone' | null>(null); const [showGrid, setShowGrid] = useState(false); const [showSafeZone, setShowSafeZone] = useState(false); // ═══ Auto-save to localStorage ═══ const AUTOSAVE_KEY = 'studio-autosave'; const autoSaveTimer = useRef>(); const [lastSaved, setLastSaved] = useState(null); // Auto-save after 2s of inactivity useEffect(() => { if (editingBrandAsset) return; // Disable autosave in brand asset mode if (autoSaveTimer.current) clearTimeout(autoSaveTimer.current); autoSaveTimer.current = setTimeout(() => { try { const data = { timelineElements: timelineElements.filter(e => !e.isBrandElement), aspectRatio, markers, savedAt: Date.now(), }; localStorage.setItem(AUTOSAVE_KEY, JSON.stringify(data)); setLastSaved(Date.now()); } catch { /* quota exceeded — silently fail */ } }, 2000); return () => { if (autoSaveTimer.current) clearTimeout(autoSaveTimer.current); }; }, [timelineElements, aspectRatio, markers]); // Auto-load on mount (only if no elements exist) useEffect(() => { if (editingBrandAsset) return; // Disable autoload in brand asset mode try { const saved = localStorage.getItem(AUTOSAVE_KEY); if (!saved) return; const data = JSON.parse(saved); if (data.timelineElements?.length && timelineElements.filter(e => !e.isBrandElement).length === 0) { setTimelineElements(prev => { const brand = prev.filter(e => e.isBrandElement); return [...brand, ...data.timelineElements]; }); if (data.markers) setMarkers(data.markers); } } catch { /* corrupted data — ignore */ } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Canvas zoom keyboard shortcuts (Cmd+=/Cmd+-/Cmd+0) useCanvasShortcuts(setCanvasZoom); // ? key toggles shortcuts overlay, Cmd+F toggles element search useEffect(() => { const handleKey = (e: KeyboardEvent) => { if ( e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || (e.target as HTMLElement).isContentEditable ) return; if (e.key === '?' || (e.shiftKey && e.code === 'Slash')) { e.preventDefault(); setShowShortcuts(prev => !prev); } if ((e.metaKey || e.ctrlKey) && e.key === 'f') { e.preventDefault(); setShowElementSearch(prev => !prev); } // G = toggle grid, Shift+S = toggle safe zone if (e.key === 'g' && !e.metaKey && !e.ctrlKey) { setShowGrid(prev => !prev); } if (e.key === 'S' && e.shiftKey && !e.metaKey && !e.ctrlKey) { setShowSafeZone(prev => !prev); } // Escape clears selection if (e.key === 'Escape') { clearSelection(); } }; window.addEventListener('keydown', handleKey); return () => window.removeEventListener('keydown', handleKey); }, []); // Keyboard shortcuts useKeyboardShortcuts({ enabled: true, playerRef, durationInFrames, selectedElementId, setSelectedElementId, timelineElements, setTimelineElements, undo, redo, }); // --- Memoized callbacks for composition --- const handleElementClick = useCallback((id: string) => { setSelectedElementId(id); const element = timelineElements.find(el => el.id === id); // In image mode, auto-switch active layer to the element's layer if (element && outputFormat === 'image') { setActiveLayerId(element.layerId); } if (element && playerRef.current) { const currentFrame = playerRef.current.getCurrentFrame(); if (currentFrame < element.startFrame || currentFrame >= element.endFrame) { playerRef.current.seekTo(element.startFrame); } } }, [timelineElements, playerRef, setSelectedElementId, outputFormat, setActiveLayerId]); const handlePositionChange = useCallback((id: string, x: number, y: number) => { setTimelineElements(prev => prev.map(el => el.id === id ? { ...el, x, y } : el)); }, [setTimelineElements]); const handleTransformChange = useCallback((id: string, updates: Partial) => { setTimelineElements(prev => prev.map(el => el.id === id ? { ...el, ...updates } : el)); }, [setTimelineElements]); const handleDuplicate = useCallback((id: string) => { setTimelineElements(prev => { const el = prev.find(e => e.id === id); if (!el) return prev; const copy: TimelineElement = { ...el, id: 'el-' + Date.now(), x: el.x + 3, y: el.y + 3, isBrandElement: false, isLocked: false, }; const idx = prev.findIndex(e => e.id === id); const next = [...prev]; next.splice(idx + 1, 0, copy); return next; }); }, [setTimelineElements]); const handleDelete = useCallback((id: string) => { setTimelineElements(prev => { const el = prev.find(e => e.id === id); if (el?.isBrandElement) return prev; return prev.filter(e => e.id !== id); }); setSelectedElementId(null); }, [setTimelineElements, setSelectedElementId]); const handleLock = useCallback((id: string) => { setTimelineElements(prev => prev.map(el => el.id === id ? { ...el, isLocked: !el.isLocked } : el )); }, [setTimelineElements]); // --- Composition Props (memoized) --- const compositionProps: RenderProps = useMemo(() => ({ designMD, textOverlay, layers, timelineElements: timelineElements .filter(el => { const layer = layers.find(l => l.id === el.layerId); return layer ? layer.isVisible !== false : true; }) .sort((a, b) => { const aIsActive = a.layerId === activeLayerId ? 1 : 0; const bIsActive = b.layerId === activeLayerId ? 1 : 0; if (aIsActive !== bIsActive) return aIsActive - bIsActive; const indexA = layers.findIndex(l => l.id === a.layerId); const indexB = layers.findIndex(l => l.id === b.layerId); return indexB - indexA; }), selectedElementId, activeLayerId, onElementClick: handleElementClick, onElementPositionChange: handlePositionChange, onElementTransformChange: handleTransformChange, onElementDuplicate: handleDuplicate, onElementDelete: handleDelete, onElementLock: handleLock, activeAction, brandVisibility: editingBrandAsset ? { logo: false, frame: false, background: false } : brandVisibility, outputFormat, }), [designMD, textOverlay, layers, timelineElements, selectedElementId, activeLayerId, activeAction, brandVisibility, outputFormat, handleElementClick, handlePositionChange, handleTransformChange, handleDuplicate, handleDelete, handleLock, editingBrandAsset]); return ( <>
setShowShortcuts(true)} outputFormat={outputFormat} /> {/* Sliding Panels */} {activePanel === 'media' && ( setActivePanel(null)} designMD={designMD} brandContent={brandContent} /> )} {activePanel === 'text' && ( setActivePanel(null)} /> )} {activePanel === 'stickers' && ( setActivePanel(null)} /> )} {activePanel === 'shapes' && ( setActivePanel(null)} /> )} {activePanel === 'audio' && ( setActivePanel(null)} /> )} {activePanel === 'sfx' && ( setActivePanel(null)} /> )}
setCanvasZoom(prev => Math.min(5, prev + 0.25))} onZoomOut={() => setCanvasZoom(prev => Math.max(0.1, prev - 0.25))} onZoomReset={() => setCanvasZoom(1)} onFitToScreen={() => setCanvasZoom(1)} onUndo={undo} onRedo={redo} onSetZoom={setCanvasZoom} /> {/* Responsive Preview Toggle + Grid/SafeZone toggles */}
{/* Canvas Grid + Safe Zone Overlay */} {/* Auto-save indicator */}
setShowExportModal(true)} onShowRenderHistory={() => setShowRenderHistory(true)} showGrid={showGrid} setShowGrid={setShowGrid} showSafeZone={showSafeZone} setShowSafeZone={setShowSafeZone} selectedElementIds={selectedElementIds} clearSelection={clearSelection} />
{outputFormat !== 'image' && (
playerRef.current?.seekTo(frame)} />
)}
{/* Export Modal */} setShowExportModal(false)} designMD={designMD} textOverlay={textOverlay} timelineElements={timelineElements} layers={layers} durationInFrames={durationInFrames} brandVisibility={editingBrandAsset ? { logo: false, frame: false, background: false } : brandVisibility} outputFormat={outputFormat} aspectRatio={aspectRatio} onAssetSaved={onAssetSaved} /> {/* Shortcuts Overlay */} setShowShortcuts(false)} /> {/* Render History */} setShowRenderHistory(false)} /> {/* Element Search (Cmd+F toggle) */} {showElementSearch && (
🔍 Buscar Elementos
{ setSelectedElementId(id); setShowElementSearch(false); }} />
)} ); };