feat: Add batch video export support with Export Queue and modal
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { Film, Volume2, Music, X, Upload, Wand2 } from 'lucide-react';
|
||||
import { Film, Volume2, Music, X, Upload, Wand2, Maximize2, Minimize2, Move, Pipette } from 'lucide-react';
|
||||
import { DesignMD } from '../../types';
|
||||
import { FileDropZone } from '../ui/FileDropZone';
|
||||
|
||||
@@ -62,6 +62,10 @@ export const BrandTabMedia: React.FC<BrandTabMediaProps & { onEditAsset?: (type:
|
||||
}}
|
||||
onEdit={() => onEditAsset?.('introVideoUrl', designMD.introVideoUrl || '')}
|
||||
showEdit={!!(designMD.introVideoUrl && onEditAsset)}
|
||||
fit={designMD.introVideoFit}
|
||||
onFitChange={(fit) => handleDesignChange('introVideoFit', fit)}
|
||||
bgColor={designMD.introVideoBgColor}
|
||||
onBgColorChange={(color) => handleDesignChange('introVideoBgColor', color ?? '')}
|
||||
/>
|
||||
|
||||
{/* ═══ Outro Video ═══ */}
|
||||
@@ -80,6 +84,10 @@ export const BrandTabMedia: React.FC<BrandTabMediaProps & { onEditAsset?: (type:
|
||||
}}
|
||||
onEdit={() => onEditAsset?.('outroVideoUrl', designMD.outroVideoUrl || '')}
|
||||
showEdit={!!(designMD.outroVideoUrl && onEditAsset)}
|
||||
fit={designMD.outroVideoFit}
|
||||
onFitChange={(fit) => handleDesignChange('outroVideoFit', fit)}
|
||||
bgColor={designMD.outroVideoBgColor}
|
||||
onBgColorChange={(color) => handleDesignChange('outroVideoBgColor', color ?? '')}
|
||||
/>
|
||||
|
||||
{/* ═══ Brand Audio ═══ */}
|
||||
@@ -184,8 +192,13 @@ const VideoUploadSimple: React.FC<{
|
||||
onClear: () => void;
|
||||
onEdit?: () => void;
|
||||
showEdit?: boolean;
|
||||
}> = ({ label, description, videoUrl, accentColor, onUrlChange, onClear, onEdit, showEdit }) => {
|
||||
fit?: 'cover' | 'contain' | 'fill';
|
||||
onFitChange?: (fit: 'cover' | 'contain' | 'fill') => void;
|
||||
bgColor?: string | null;
|
||||
onBgColorChange?: (color: string | null) => void;
|
||||
}> = ({ label, description, videoUrl, accentColor, onUrlChange, onClear, onEdit, showEdit, fit = 'cover', onFitChange, bgColor, onBgColorChange }) => {
|
||||
const hasVideo = !!videoUrl && videoUrl.trim().length > 0;
|
||||
const colorInputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
return (
|
||||
<div className="bg-neutral-900/50 border border-neutral-800 rounded-xl p-4 space-y-3">
|
||||
@@ -270,16 +283,70 @@ const VideoUploadSimple: React.FC<{
|
||||
|
||||
{/* Status badge */}
|
||||
{hasVideo && (
|
||||
<div
|
||||
className="flex items-center gap-1.5 text-[10px] font-medium px-2.5 py-1 rounded-lg w-fit"
|
||||
style={{
|
||||
backgroundColor: `${accentColor}15`,
|
||||
color: accentColor,
|
||||
border: `1px solid ${accentColor}30`,
|
||||
}}
|
||||
>
|
||||
<div className="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: accentColor }} />
|
||||
Video cargado
|
||||
<div className="flex flex-col gap-3 pt-1 border-t border-neutral-800/50">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-[10px] text-neutral-500 mr-2">Ajuste de video:</span>
|
||||
{([
|
||||
{ key: 'cover' as const, label: 'Cover', icon: <Maximize2 size={10} />, tip: 'Llenar pantalla' },
|
||||
{ key: 'contain' as const, label: 'Contain', icon: <Minimize2 size={10} />, tip: 'Mostrar completo' },
|
||||
{ key: 'fill' as const, label: 'Fill', icon: <Move size={10} />, tip: 'Estirar' },
|
||||
]).map(opt => (
|
||||
<button
|
||||
key={opt.key}
|
||||
onClick={() => onFitChange?.(opt.key)}
|
||||
title={opt.tip}
|
||||
className={`flex items-center gap-1.5 px-2 py-1 rounded-md text-[9px] font-medium transition-all border ${
|
||||
fit === opt.key
|
||||
? `border-[${accentColor}]/50 bg-[${accentColor}]/15 text-[${accentColor}]`
|
||||
: 'border-neutral-800 bg-neutral-900 text-neutral-400 hover:text-neutral-200'
|
||||
}`}
|
||||
style={fit === opt.key ? { borderColor: `${accentColor}50`, backgroundColor: `${accentColor}15`, color: accentColor } : {}}
|
||||
>
|
||||
{opt.icon} {opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{fit === 'contain' && onBgColorChange && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-[10px] text-neutral-500 mr-2">Color de fondo:</span>
|
||||
<button
|
||||
onClick={() => colorInputRef.current?.click()}
|
||||
title={bgColor ? `Color: ${bgColor}` : 'Seleccionar color de fondo'}
|
||||
className="w-6 h-6 rounded border border-neutral-700 hover:border-neutral-500 transition-colors overflow-hidden flex items-center justify-center shrink-0"
|
||||
style={{
|
||||
backgroundColor: bgColor || undefined,
|
||||
...(!bgColor ? {
|
||||
backgroundImage: 'linear-gradient(45deg, #444 25%, transparent 25%), linear-gradient(-45deg, #444 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #444 75%), linear-gradient(-45deg, transparent 75%, #444 75%)',
|
||||
backgroundSize: '8px 8px',
|
||||
backgroundPosition: '0 0, 0 4px, 4px -4px, -4px 0px',
|
||||
} : {}),
|
||||
}}
|
||||
>
|
||||
{!bgColor && <Pipette size={10} className="text-neutral-400" />}
|
||||
</button>
|
||||
<input
|
||||
ref={colorInputRef}
|
||||
type="color"
|
||||
value={bgColor || '#000000'}
|
||||
onChange={(e) => onBgColorChange(e.target.value)}
|
||||
className="sr-only"
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<button
|
||||
onClick={() => onBgColorChange(null)}
|
||||
className={`flex items-center gap-1 px-2 py-1 rounded-md text-[9px] font-medium transition-all border ${
|
||||
!bgColor
|
||||
? `border-[${accentColor}]/50 bg-[${accentColor}]/15 text-[${accentColor}]`
|
||||
: 'border-neutral-800 bg-neutral-900 text-neutral-400 hover:text-neutral-200'
|
||||
}`}
|
||||
style={!bgColor ? { borderColor: `${accentColor}50`, backgroundColor: `${accentColor}15`, color: accentColor } : {}}
|
||||
>
|
||||
<X size={10} /> Transparente
|
||||
</button>
|
||||
{bgColor && <span className="text-[9px] text-neutral-500 font-mono ml-1">{bgColor}</span>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user