import React, { useState, useMemo } from 'react'; import { X, Download, Film, Image as ImageIcon, Wifi, WifiOff, Zap, ChevronDown } from 'lucide-react'; import { useExportQueue, RenderFormat, ExportConfig } from '../../hooks/useExportQueue'; import { ExportJobItem } from './ExportJobItem'; import type { DesignMD, TimelineElement, TimelineLayer } from '../../types'; interface ExportModalProps { isOpen: boolean; onClose: () => void; designMD: DesignMD; textOverlay: string; timelineElements: TimelineElement[]; layers: TimelineLayer[]; durationInFrames: number; brandVisibility?: { logo: boolean; frame: boolean; background: boolean }; outputFormat?: 'video' | 'image'; /** Template aspect ratio — used to filter resolution presets */ aspectRatio?: '9:16' | '16:9' | '1:1' | '4:5' | '4:3'; } const FORMAT_OPTIONS: { value: RenderFormat; label: string; icon: typeof Film; desc: string }[] = [ { value: 'mp4', label: 'MP4', icon: Film, desc: 'Compatible con todo' }, { value: 'webm', label: 'WebM', icon: Film, desc: 'Web optimizado' }, { value: 'gif', label: 'GIF', icon: Film, desc: 'Animación ligera' }, { value: 'png', label: 'PNG', icon: ImageIcon, desc: 'Imagen sin fondo' }, { value: 'jpeg', label: 'JPEG', icon: ImageIcon, desc: 'Imagen comprimida' }, ]; const RESOLUTION_PRESETS = [ { label: '1080×1080', w: 1080, h: 1080, desc: 'Instagram Post', ratio: '1:1' }, { label: '720×720', w: 720, h: 720, desc: 'Preview rápido', ratio: '1:1' }, { label: '1080×1920', w: 1080, h: 1920, desc: 'Story / Reel', ratio: '9:16' }, { label: '720×1280', w: 720, h: 1280, desc: 'Preview rápido', ratio: '9:16' }, { label: '1920×1080', w: 1920, h: 1080, desc: 'YouTube / TV', ratio: '16:9' }, { label: '1280×720', w: 1280, h: 720, desc: 'HD 720p', ratio: '16:9' }, { label: '1080×1350', w: 1080, h: 1350, desc: 'Feed 4:5', ratio: '4:5' }, { label: '720×900', w: 720, h: 900, desc: 'Preview 4:5', ratio: '4:5' }, { label: '1440×1080', w: 1440, h: 1080, desc: 'Pantalla 4:3', ratio: '4:3' }, { label: '960×720', w: 960, h: 720, desc: 'Preview 4:3', ratio: '4:3' }, ]; /** * Export modal — format selection, resolution presets, and live job queue. */ export const ExportModal: React.FC = ({ isOpen, onClose, designMD, textOverlay, timelineElements, layers, durationInFrames, brandVisibility, outputFormat, aspectRatio, }) => { const { jobs, activeJobs, hasActiveJobs, isConnected, startExport, cancelJob, downloadJob } = useExportQueue(); const [format, setFormat] = useState('mp4'); const [fps, setFps] = useState(30); const [isExporting, setIsExporting] = useState(false); const [showAdvanced, setShowAdvanced] = useState(false); const [quality, setQuality] = useState<'draft' | 'standard' | 'high' | 'ultra'>('high'); const isStill = format === 'png' || format === 'jpeg'; // Filter resolution presets to match the template's aspect ratio const filteredPresets = useMemo(() => { if (!aspectRatio) return RESOLUTION_PRESETS; const matching = RESOLUTION_PRESETS.filter(p => p.ratio === aspectRatio); return matching.length > 0 ? matching : RESOLUTION_PRESETS; }, [aspectRatio]); const [resIdx, setResIdx] = useState(0); const selectedRes = filteredPresets[resIdx] || filteredPresets[0]; // Estimated file size const estimatedSize = useMemo(() => { if (isStill) return '~0.5 MB'; const seconds = durationInFrames / fps; const pixels = selectedRes.w * selectedRes.h; const bitrateMap: Record = { mp4: 5, webm: 3, gif: 15 }; const bitrate = bitrateMap[format] ?? 5; // MB per minute per megapixel const mp = pixels / 1_000_000; const sizeMB = (seconds / 60) * bitrate * mp; return sizeMB < 1 ? `~${Math.round(sizeMB * 1024)} KB` : `~${sizeMB.toFixed(1)} MB`; }, [format, selectedRes, durationInFrames, fps, isStill]); // Auto-select image format in image mode const filteredFormats = useMemo(() => { if (outputFormat === 'image') { return FORMAT_OPTIONS.filter(f => f.value === 'png' || f.value === 'jpeg'); } return FORMAT_OPTIONS; }, [outputFormat]); const handleExport = async () => { setIsExporting(true); try { const config: ExportConfig = { format, width: selectedRes.w, height: selectedRes.h, fps, durationInFrames: isStill ? 1 : durationInFrames, designMD, textOverlay, timelineElements, layers, brandVisibility, outputFormat, }; await startExport(config); } finally { setIsExporting(false); } }; if (!isOpen) return null; return (
{/* Header */}

Exportar

Renderizar y descargar

{isConnected ? : } {isConnected ? 'Live' : 'Offline'}
{/* Body */}
{/* Format Selection */}
{filteredFormats.map(opt => { const Icon = opt.icon; return ( ); })}
{/* Resolution */}
{filteredPresets.map((preset, idx) => ( ))}

{selectedRes.desc}

{/* Advanced Settings */} {!isStill && (
{showAdvanced && (
{[24, 30, 60].map(f => ( ))}
Duración {(durationInFrames / fps).toFixed(1)}s ({durationInFrames} frames)
{/* Quality Tier */}
{[ { value: 'draft' as const, label: 'Draft', color: 'text-neutral-400' }, { value: 'standard' as const, label: 'Std', color: 'text-sky-400' }, { value: 'high' as const, label: 'High', color: 'text-violet-400' }, { value: 'ultra' as const, label: 'Ultra', color: 'text-amber-400' }, ].map(q => ( ))}
)}
)} {/* Export Button */} {/* Job Queue */} {jobs.length > 0 && (
{hasActiveJobs && ( {activeJobs.length} activ{activeJobs.length > 1 ? 'os' : 'o'} )}
{jobs.map(job => ( ))}
)}
); };