Refactor: remove AGPL imgly dependency and migrate background removal to python backend
This commit is contained in:
@@ -14,6 +14,7 @@ interface BrandArchitectureProps {
|
||||
designMD: DesignMD;
|
||||
handleDesignChange: (key: keyof DesignMD, value: string | number | string[] | boolean) => void;
|
||||
onContinue: () => void;
|
||||
onEditAsset?: (type: keyof DesignMD, url: string) => void;
|
||||
}
|
||||
|
||||
const TABS = [
|
||||
@@ -25,7 +26,7 @@ const TABS = [
|
||||
|
||||
type TabId = typeof TABS[number]['id'];
|
||||
|
||||
export const BrandArchitecture: React.FC<BrandArchitectureProps> = ({ company, handleCompanyChange, designMD, handleDesignChange, onContinue }) => {
|
||||
export const BrandArchitecture: React.FC<BrandArchitectureProps> = ({ company, handleCompanyChange, designMD, handleDesignChange, onContinue, onEditAsset }) => {
|
||||
const [zoom, setZoom] = useState(1);
|
||||
const [aspectRatio, setAspectRatio] = useState<'16:9'|'1:1'|'9:16'>('9:16');
|
||||
const [activeTab, setActiveTab] = useState<TabId>('general');
|
||||
@@ -186,6 +187,7 @@ export const BrandArchitecture: React.FC<BrandArchitectureProps> = ({ company, h
|
||||
<BrandTabVisual
|
||||
designMD={designMD}
|
||||
handleDesignChange={handleDesignChange}
|
||||
onEditAsset={onEditAsset}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'typography' && (
|
||||
@@ -195,6 +197,7 @@ export const BrandArchitecture: React.FC<BrandArchitectureProps> = ({ company, h
|
||||
<BrandTabMedia
|
||||
designMD={designMD}
|
||||
handleDesignChange={handleDesignChange}
|
||||
onEditAsset={onEditAsset}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -85,9 +85,9 @@ export const StudioProperties: React.FC<StudioPropertiesProps> = ({
|
||||
const isImageMode = outputFormat === 'image';
|
||||
|
||||
return (
|
||||
<aside className="w-72 bg-neutral-900 border-l border-neutral-800/60 flex flex-col z-10 shrink-0" onClick={(e) => e.stopPropagation()}>
|
||||
<aside className="w-72 bg-neutral-900 border-l border-neutral-800/60 flex flex-col z-10 shrink-0 h-full" onClick={(e) => e.stopPropagation()}>
|
||||
{/* Properties section */}
|
||||
<div className={isImageMode ? 'shrink-0 border-b border-neutral-800 overflow-y-auto max-h-[50%]' : 'flex-1 overflow-y-auto'}>
|
||||
<div className={isImageMode ? 'flex-1 min-h-0 flex flex-col border-b border-neutral-800' : 'flex-1 min-h-0 flex flex-col'}>
|
||||
{activeTool === 'transitions' ? (
|
||||
<TransitionsPanel designMD={designMD} />
|
||||
) : (selectedElementIds && selectedElementIds.size >= 2) ? (
|
||||
@@ -137,7 +137,7 @@ export const StudioProperties: React.FC<StudioPropertiesProps> = ({
|
||||
|
||||
{/* Layers panel — image mode only (replaces the hidden timeline) */}
|
||||
{isImageMode && (
|
||||
<div className="flex-1 min-h-0 border-t border-neutral-800">
|
||||
<div className="flex-1 min-h-0 flex flex-col bg-neutral-900">
|
||||
<ImageLayersPanel
|
||||
timelineElements={timelineElements}
|
||||
setTimelineElements={setTimelineElements}
|
||||
|
||||
@@ -18,8 +18,8 @@ interface StudioWorkspaceProps {
|
||||
durationInFrames: number;
|
||||
timelineElements?: TimelineElement[];
|
||||
setTimelineElements?: React.Dispatch<React.SetStateAction<TimelineElement[]>>;
|
||||
aspectRatio: '16:9' | '9:16' | '1:1' | '4:5' | '4:3';
|
||||
setAspectRatio: (ratio: '16:9' | '9:16' | '1:1' | '4:5' | '4:3') => void;
|
||||
aspectRatio: string;
|
||||
setAspectRatio: (ratio: string) => void;
|
||||
outputFormat?: 'video' | 'image';
|
||||
activeLayerId?: string;
|
||||
/** Lifted zoom state for TopHeader integration */
|
||||
@@ -262,7 +262,17 @@ export const StudioWorkspace: React.FC<StudioWorkspaceProps> = ({
|
||||
if (aspectRatio === '1:1') return { width: 1080, height: 1080 };
|
||||
if (aspectRatio === '4:5') return { width: 1080, height: 1350 };
|
||||
if (aspectRatio === '4:3') return { width: 1440, height: 1080 };
|
||||
return { width: 1080, height: 1920 }; // 9:16
|
||||
if (aspectRatio === '9:16') return { width: 1080, height: 1920 };
|
||||
|
||||
// Custom aspect ratio fallback W:H
|
||||
const parts = aspectRatio.split(':');
|
||||
if (parts.length === 2) {
|
||||
const w = parseInt(parts[0], 10);
|
||||
const h = parseInt(parts[1], 10);
|
||||
if (!isNaN(w) && !isNaN(h)) return { width: w, height: h };
|
||||
}
|
||||
|
||||
return { width: 1080, height: 1920 }; // Default
|
||||
};
|
||||
const dimensions = getDimensions();
|
||||
|
||||
|
||||
@@ -15,8 +15,10 @@ interface TopHeaderProps {
|
||||
onZoomOut?: () => void;
|
||||
onZoomReset?: () => void;
|
||||
/** Aspect ratio controls */
|
||||
aspectRatio?: '16:9' | '1:1' | '9:16' | '4:5' | '4:3';
|
||||
onAspectRatioChange?: (ratio: '16:9' | '1:1' | '9:16' | '4:5' | '4:3') => void;
|
||||
aspectRatio?: string;
|
||||
onAspectRatioChange?: (ratio: string) => void;
|
||||
disableAspectControls?: boolean;
|
||||
titleOverride?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const TopHeader: React.FC<TopHeaderProps> = ({
|
||||
@@ -31,14 +33,19 @@ export const TopHeader: React.FC<TopHeaderProps> = ({
|
||||
onZoomReset,
|
||||
aspectRatio = '9:16',
|
||||
onAspectRatioChange,
|
||||
disableAspectControls,
|
||||
titleOverride,
|
||||
}) => {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const isStudio = currentStep === 'studio';
|
||||
|
||||
return (
|
||||
<header className="flex-none border-b border-neutral-800/60 bg-neutral-900/95 backdrop-blur-sm px-3 h-11 flex items-center justify-between z-30 relative">
|
||||
<header
|
||||
className="flex-none border-b border-neutral-800/60 bg-neutral-900/95 backdrop-blur-sm px-3 h-11 flex items-center justify-between z-30 relative"
|
||||
style={{ WebkitAppRegion: 'drag' } as React.CSSProperties}
|
||||
>
|
||||
{/* Left: Hamburger + Logo */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 ml-[72px]" style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}>
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setMenuOpen(!menuOpen)}
|
||||
@@ -88,13 +95,15 @@ export const TopHeader: React.FC<TopHeaderProps> = ({
|
||||
<div className="bg-violet-600/20 p-1 rounded text-violet-400">
|
||||
<LayoutTemplate size={14} />
|
||||
</div>
|
||||
<span className="text-xs font-semibold text-white tracking-tight">SaaS Branding</span>
|
||||
<span className="text-xs font-semibold text-white tracking-tight">
|
||||
{titleOverride || 'SaaS Branding'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Center: Zoom controls (only in studio) */}
|
||||
{isStudio && onZoomIn && onZoomOut && (
|
||||
<div className="absolute left-1/2 -translate-x-1/2 flex items-center gap-1">
|
||||
<div className="absolute left-1/2 -translate-x-1/2 flex items-center gap-1" style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onZoomOut(); }}
|
||||
title="Zoom Out"
|
||||
@@ -118,7 +127,7 @@ export const TopHeader: React.FC<TopHeaderProps> = ({
|
||||
</button>
|
||||
|
||||
{/* Aspect ratio pills */}
|
||||
{onAspectRatioChange && (
|
||||
{onAspectRatioChange && !disableAspectControls && (
|
||||
<>
|
||||
<div className="w-px h-4 bg-neutral-700 mx-1" />
|
||||
{(['16:9', '9:16', '1:1', '4:5', '4:3'] as const).map(ratio => (
|
||||
@@ -141,7 +150,7 @@ export const TopHeader: React.FC<TopHeaderProps> = ({
|
||||
)}
|
||||
|
||||
{/* Right: Editor buttons + Format badge */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 pr-2" style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}>
|
||||
{/* Express / Pro buttons — only on dashboard */}
|
||||
{currentStep === 'dashboard' && onStartExpressBlank && (
|
||||
<button
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { Film, Volume2, Music, X, Upload } from 'lucide-react';
|
||||
import { Film, Volume2, Music, X, Upload, Wand2 } from 'lucide-react';
|
||||
import { DesignMD } from '../../types';
|
||||
import { FileDropZone } from '../ui/FileDropZone';
|
||||
|
||||
@@ -15,7 +15,7 @@ interface BrandTabMediaProps {
|
||||
* All positioning, fit, duration, and blend controls live in the TemplateBuilder
|
||||
* (per-template segment configuration), avoiding collisions.
|
||||
*/
|
||||
export const BrandTabMedia: React.FC<BrandTabMediaProps> = ({ designMD, handleDesignChange }) => {
|
||||
export const BrandTabMedia: React.FC<BrandTabMediaProps & { onEditAsset?: (type: keyof DesignMD, url: string) => void }> = ({ designMD, handleDesignChange, onEditAsset }) => {
|
||||
|
||||
/** Auto-detect video duration and store it in DesignMD (for BrandPreview playback) */
|
||||
const probeVideoDuration = useCallback((url: string, key: 'introDurationFrames' | 'outroDurationFrames') => {
|
||||
@@ -60,6 +60,8 @@ export const BrandTabMedia: React.FC<BrandTabMediaProps> = ({ designMD, handleDe
|
||||
handleDesignChange('introVideoUrl', '');
|
||||
handleDesignChange('introDurationFrames', 60);
|
||||
}}
|
||||
onEdit={() => onEditAsset?.('introVideoUrl', designMD.introVideoUrl || '')}
|
||||
showEdit={!!(designMD.introVideoUrl && onEditAsset)}
|
||||
/>
|
||||
|
||||
{/* ═══ Outro Video ═══ */}
|
||||
@@ -76,6 +78,8 @@ export const BrandTabMedia: React.FC<BrandTabMediaProps> = ({ designMD, handleDe
|
||||
handleDesignChange('outroVideoUrl', '');
|
||||
handleDesignChange('outroDurationFrames', 60);
|
||||
}}
|
||||
onEdit={() => onEditAsset?.('outroVideoUrl', designMD.outroVideoUrl || '')}
|
||||
showEdit={!!(designMD.outroVideoUrl && onEditAsset)}
|
||||
/>
|
||||
|
||||
{/* ═══ Brand Audio ═══ */}
|
||||
@@ -178,7 +182,9 @@ const VideoUploadSimple: React.FC<{
|
||||
accentColor: string;
|
||||
onUrlChange: (url: string) => void;
|
||||
onClear: () => void;
|
||||
}> = ({ label, description, videoUrl, accentColor, onUrlChange, onClear }) => {
|
||||
onEdit?: () => void;
|
||||
showEdit?: boolean;
|
||||
}> = ({ label, description, videoUrl, accentColor, onUrlChange, onClear, onEdit, showEdit }) => {
|
||||
const hasVideo = !!videoUrl && videoUrl.trim().length > 0;
|
||||
|
||||
return (
|
||||
@@ -250,6 +256,15 @@ const VideoUploadSimple: React.FC<{
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{showEdit && (
|
||||
<button
|
||||
onClick={onEdit}
|
||||
className="w-full flex items-center justify-center gap-2 py-2.5 rounded-lg bg-violet-600/20 text-violet-400 hover:bg-violet-600/30 transition-colors text-xs font-semibold border border-violet-500/20"
|
||||
>
|
||||
<Wand2 size={14} />
|
||||
Abrir en Editor Avanzado
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,27 +1,33 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { Settings2, ImageIcon } from 'lucide-react';
|
||||
import { Settings2, ImageIcon, Wand2 } from 'lucide-react';
|
||||
import { DesignMD } from '../../types';
|
||||
import { FileDropZone } from '../ui/FileDropZone';
|
||||
|
||||
interface BrandTabVisualProps {
|
||||
designMD: DesignMD;
|
||||
handleDesignChange: (key: keyof DesignMD, value: string | number | string[] | boolean) => void;
|
||||
onEditAsset?: (type: keyof DesignMD, url: string) => void;
|
||||
}
|
||||
|
||||
export const BrandTabVisual: React.FC<BrandTabVisualProps> = ({
|
||||
designMD,
|
||||
handleDesignChange,
|
||||
onEditAsset,
|
||||
}) => {
|
||||
const handleLogoFiles = useCallback((files: File[]) => {
|
||||
const handleLogoFiles = useCallback(async (files: File[]) => {
|
||||
const file = files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
if (event.target?.result) {
|
||||
handleDesignChange('logoUrl', event.target.result as string);
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const res = await fetch('/api/upload', { method: 'POST', body: formData });
|
||||
if (!res.ok) throw new Error('Upload failed');
|
||||
const data = await res.json();
|
||||
handleDesignChange('logoUrl', data.url);
|
||||
} catch (err) {
|
||||
console.error('Logo upload failed:', err);
|
||||
}
|
||||
}, [handleDesignChange]);
|
||||
|
||||
return (
|
||||
@@ -53,6 +59,15 @@ export const BrandTabVisual: React.FC<BrandTabVisualProps> = ({
|
||||
label="Subir desde archivo"
|
||||
onFiles={handleLogoFiles}
|
||||
/>
|
||||
{designMD.logoUrl && onEditAsset && (
|
||||
<button
|
||||
onClick={() => onEditAsset('logoUrl', designMD.logoUrl)}
|
||||
className="w-full flex items-center justify-center gap-2 py-2.5 rounded-lg bg-violet-600/20 text-violet-400 hover:bg-violet-600/30 transition-colors text-xs font-semibold border border-violet-500/20"
|
||||
>
|
||||
<Wand2 size={14} />
|
||||
Abrir en Editor Avanzado
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,8 +14,8 @@ interface ExportModalProps {
|
||||
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';
|
||||
aspectRatio?: string;
|
||||
onAssetSaved?: (url: string) => void;
|
||||
}
|
||||
|
||||
const FORMAT_OPTIONS: { value: RenderFormat; label: string; icon: typeof Film; desc: string }[] = [
|
||||
@@ -53,6 +53,7 @@ export const ExportModal: React.FC<ExportModalProps> = ({
|
||||
brandVisibility,
|
||||
outputFormat,
|
||||
aspectRatio,
|
||||
onAssetSaved,
|
||||
}) => {
|
||||
const { jobs, activeJobs, hasActiveJobs, isConnected, startExport, cancelJob, downloadJob } = useExportQueue();
|
||||
|
||||
@@ -68,7 +69,24 @@ export const ExportModal: React.FC<ExportModalProps> = ({
|
||||
const filteredPresets = useMemo(() => {
|
||||
if (!aspectRatio) return RESOLUTION_PRESETS;
|
||||
const matching = RESOLUTION_PRESETS.filter(p => p.ratio === aspectRatio);
|
||||
return matching.length > 0 ? matching : RESOLUTION_PRESETS;
|
||||
if (matching.length > 0) return matching;
|
||||
|
||||
// Parse custom W:H
|
||||
const parts = aspectRatio.split(':');
|
||||
if (parts.length === 2) {
|
||||
const w = parseInt(parts[0], 10);
|
||||
const h = parseInt(parts[1], 10);
|
||||
if (!isNaN(w) && !isNaN(h)) {
|
||||
return [{
|
||||
label: `${w}x${h}`,
|
||||
w: w,
|
||||
h: h,
|
||||
desc: 'Resolución Original',
|
||||
ratio: aspectRatio
|
||||
}];
|
||||
}
|
||||
}
|
||||
return RESOLUTION_PRESETS;
|
||||
}, [aspectRatio]);
|
||||
|
||||
const [resIdx, setResIdx] = useState(0);
|
||||
@@ -94,6 +112,22 @@ export const ExportModal: React.FC<ExportModalProps> = ({
|
||||
return FORMAT_OPTIONS;
|
||||
}, [outputFormat]);
|
||||
|
||||
// Ensure selected format is valid for the current mode
|
||||
React.useEffect(() => {
|
||||
if (!filteredFormats.find(f => f.value === format)) {
|
||||
setFormat(filteredFormats[0].value);
|
||||
}
|
||||
}, [filteredFormats, format]);
|
||||
|
||||
// Track finished jobs to call onAssetSaved automatically
|
||||
React.useEffect(() => {
|
||||
if (!onAssetSaved) return;
|
||||
const completedJob = jobs.find(j => j.status === 'completed' && j.resultUrl);
|
||||
if (completedJob && completedJob.resultUrl) {
|
||||
onAssetSaved(completedJob.resultUrl);
|
||||
}
|
||||
}, [jobs, onAssetSaved]);
|
||||
|
||||
const handleExport = async () => {
|
||||
setIsExporting(true);
|
||||
try {
|
||||
@@ -128,8 +162,8 @@ export const ExportModal: React.FC<ExportModalProps> = ({
|
||||
<Download size={18} className="text-violet-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-sm font-bold text-white">Exportar</h2>
|
||||
<p className="text-[10px] text-neutral-500">Renderizar y descargar</p>
|
||||
<h2 className="text-sm font-bold text-white">{onAssetSaved ? 'Guardar Activo de Marca' : 'Exportar'}</h2>
|
||||
<p className="text-[10px] text-neutral-500">{onAssetSaved ? 'Renderizar y aplicar cambios' : 'Renderizar y descargar'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -284,7 +318,7 @@ export const ExportModal: React.FC<ExportModalProps> = ({
|
||||
}`}
|
||||
>
|
||||
<Zap size={16} />
|
||||
{isExporting ? 'Iniciando...' : `Exportar ${isStill ? 'Imagen' : 'Video'}`}
|
||||
{isExporting ? 'Iniciando...' : (onAssetSaved ? 'Renderizar y Guardar' : `Exportar ${isStill ? 'Imagen' : 'Video'}`)}
|
||||
<span className="text-[9px] opacity-60 font-mono">({estimatedSize})</span>
|
||||
</button>
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { FilterPresets } from '../ui/FilterPresets';
|
||||
import { TextStylePresets } from '../ui/TextStylePresets';
|
||||
import { CollapsibleSection } from '../ui/CollapsibleSection';
|
||||
import { useColorHistory } from '../../hooks/useColorHistory';
|
||||
|
||||
import { removeImageBackground } from '../../utils/backgroundRemoval';
|
||||
interface ElementPropertiesPanelProps {
|
||||
selectedElementId: string;
|
||||
setSelectedElementId: (id: string | null) => void;
|
||||
@@ -247,6 +247,7 @@ export const ElementPropertiesPanel: React.FC<ElementPropertiesPanelProps> = ({
|
||||
const [showBorderEffects, setShowBorderEffects] = useState(false);
|
||||
const [showShadow, setShowShadow] = useState(false);
|
||||
const [showChromaKey, setShowChromaKey] = useState(false);
|
||||
const [isRemovingBg, setIsRemovingBg] = useState(false);
|
||||
const [copiedStyle, setCopiedStyle] = useState<Partial<TimelineElement> | null>(null);
|
||||
const { recentColors, addColor } = useColorHistory();
|
||||
|
||||
@@ -582,6 +583,32 @@ export const ElementPropertiesPanel: React.FC<ElementPropertiesPanelProps> = ({
|
||||
>
|
||||
<Pipette size={10} /> Chroma
|
||||
</button>
|
||||
{/* IA Quitar Fondo */}
|
||||
{(el.type === 'image' || el.type === 'sticker') && (
|
||||
<button
|
||||
disabled={isRemovingBg}
|
||||
onClick={async () => {
|
||||
try {
|
||||
setIsRemovingBg(true);
|
||||
const newUrl = await removeImageBackground(el.content);
|
||||
update({ content: newUrl, chromaKeyEnabled: false, blendMode: 'normal' });
|
||||
} catch (err) {
|
||||
alert('Error al remover el fondo. Inténtalo de nuevo.');
|
||||
} finally {
|
||||
setIsRemovingBg(false);
|
||||
}
|
||||
}}
|
||||
title="Magia IA: Remover fondo de la imagen automáticamente"
|
||||
className={`flex-1 min-w-[60px] py-1.5 rounded-lg text-[9px] font-medium transition-all border flex items-center justify-center gap-1 ${
|
||||
isRemovingBg
|
||||
? 'bg-neutral-800 text-neutral-500 border-neutral-700 cursor-not-allowed'
|
||||
: 'bg-violet-600/20 border-violet-500/60 text-violet-300 hover:bg-violet-600/30'
|
||||
}`}
|
||||
>
|
||||
{isRemovingBg ? <Loader2 size={10} className="animate-spin" /> : <Wand2 size={10} />}
|
||||
{isRemovingBg ? 'Procesando...' : 'IA Fondo'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Chroma Key Controls ── */}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { CollapsibleSection } from '../ui/CollapsibleSection';
|
||||
import type { BradlyPlayerRef } from '../../engine/player';
|
||||
import { EXPORT_PRESETS } from '../../config/constants';
|
||||
import { TimelineElement, TimelineLayer } from '../../types';
|
||||
import { useEditor } from '../../context/EditorContext';
|
||||
import { ProjectStats } from '../ui/ProjectStats';
|
||||
import { QuickElementTemplates } from '../ui/QuickElementTemplates';
|
||||
import { BulkActionsBar } from '../ui/BulkActionsBar';
|
||||
@@ -32,6 +33,7 @@ export const GlobalSettingsPanel: React.FC<GlobalSettingsPanelProps> = ({
|
||||
timelineElements, setTimelineElements, showGrid, setShowGrid, showSafeZone, setShowSafeZone,
|
||||
onShowRenderHistory, layers, durationInFrames, fps,
|
||||
}) => {
|
||||
const { editingBrandAsset } = useEditor();
|
||||
// ═══ Export frame as PNG ═══
|
||||
const handleExportFrame = useCallback(() => {
|
||||
const player = playerRef?.current;
|
||||
@@ -277,11 +279,11 @@ export const GlobalSettingsPanel: React.FC<GlobalSettingsPanelProps> = ({
|
||||
</button>
|
||||
{/* Render button */}
|
||||
<button
|
||||
title="Exportar Video"
|
||||
title={editingBrandAsset ? "Guardar Activo de Marca" : "Exportar Video"}
|
||||
onClick={onExportClick}
|
||||
className="w-full bg-violet-600 hover:bg-violet-500 text-white font-medium py-3 rounded-lg transition-colors flex items-center justify-center gap-2 text-sm shadow-xl shadow-violet-900/20"
|
||||
>
|
||||
<Play size={16} fill="currentColor" /> Renderizar
|
||||
<Play size={16} fill="currentColor" /> {editingBrandAsset ? "Guardar Activo de Marca" : "Renderizar"}
|
||||
</button>
|
||||
{/* Project Save/Load */}
|
||||
<div className="flex gap-1.5 mt-1">
|
||||
|
||||
@@ -29,7 +29,7 @@ import { RenderProps, TimelineElement } from '../../types';
|
||||
* StudioEditor: The main editing view.
|
||||
* Reads all state from EditorContext — no prop drilling needed.
|
||||
*/
|
||||
export const StudioEditor: React.FC = () => {
|
||||
export const StudioEditor: React.FC<{ onAssetSaved?: (url: string) => void }> = ({ onAssetSaved }) => {
|
||||
const {
|
||||
timelineElements, setTimelineElements,
|
||||
layers, setLayers,
|
||||
@@ -50,6 +50,7 @@ export const StudioEditor: React.FC = () => {
|
||||
brandVisibility, setBrandVisibility,
|
||||
activeAction, setActiveAction,
|
||||
selectedElementIds, toggleElementSelection, clearSelection,
|
||||
editingBrandAsset,
|
||||
} = useEditor();
|
||||
|
||||
// Panel state (replaces old activeTool for toolbar)
|
||||
@@ -70,6 +71,7 @@ export const StudioEditor: React.FC = () => {
|
||||
|
||||
// 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 {
|
||||
@@ -88,6 +90,7 @@ export const StudioEditor: React.FC = () => {
|
||||
|
||||
// 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;
|
||||
@@ -236,9 +239,9 @@ export const StudioEditor: React.FC = () => {
|
||||
onElementDelete: handleDelete,
|
||||
onElementLock: handleLock,
|
||||
activeAction,
|
||||
brandVisibility,
|
||||
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]);
|
||||
}), [designMD, textOverlay, layers, timelineElements, selectedElementId, activeLayerId, activeAction, brandVisibility, outputFormat, handleElementClick, handlePositionChange, handleTransformChange, handleDuplicate, handleDelete, handleLock, editingBrandAsset]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -408,8 +411,10 @@ export const StudioEditor: React.FC = () => {
|
||||
timelineElements={timelineElements}
|
||||
layers={layers}
|
||||
durationInFrames={durationInFrames}
|
||||
brandVisibility={brandVisibility}
|
||||
brandVisibility={editingBrandAsset ? { logo: false, frame: false, background: false } : brandVisibility}
|
||||
outputFormat={outputFormat}
|
||||
aspectRatio={aspectRatio}
|
||||
onAssetSaved={onAssetSaved}
|
||||
/>
|
||||
|
||||
{/* Shortcuts Overlay */}
|
||||
|
||||
@@ -11,7 +11,15 @@ interface StudioTopBarProps {
|
||||
* Lives inside EditorProvider so it can access canvas zoom state.
|
||||
*/
|
||||
export const StudioTopBar: React.FC<StudioTopBarProps> = ({ setCurrentStep }) => {
|
||||
const { canvasZoom, setCanvasZoom, aspectRatio, setAspectRatio, outputFormat } = useEditor();
|
||||
const { canvasZoom, setCanvasZoom, aspectRatio, setAspectRatio, outputFormat, editingBrandAsset } = useEditor();
|
||||
|
||||
const titleOverride = editingBrandAsset ? (
|
||||
<span>Editando Activo: <span className="text-violet-400">{
|
||||
editingBrandAsset.type === 'logoUrl' ? 'Logo' :
|
||||
editingBrandAsset.type === 'introVideoUrl' ? 'Video Intro' :
|
||||
'Video Outro'
|
||||
}</span></span>
|
||||
) : undefined;
|
||||
|
||||
return (
|
||||
<TopHeader
|
||||
@@ -24,6 +32,8 @@ export const StudioTopBar: React.FC<StudioTopBarProps> = ({ setCurrentStep }) =>
|
||||
onZoomReset={() => setCanvasZoom(1)}
|
||||
aspectRatio={aspectRatio}
|
||||
onAspectRatioChange={setAspectRatio}
|
||||
disableAspectControls={!!editingBrandAsset}
|
||||
titleOverride={titleOverride}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user