feat: daily timeline advanced UI and calendar integration
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { Film, Volume2, Music, X, Upload, Wand2, Maximize2, Minimize2, Move, Pipette } from 'lucide-react';
|
||||
import { DesignMD } from '../../types';
|
||||
import { DesignMD, CompanyProfile } from '../../types';
|
||||
import { FileDropZone } from '../ui/FileDropZone';
|
||||
|
||||
interface BrandTabMediaProps {
|
||||
company: CompanyProfile;
|
||||
designMD: DesignMD;
|
||||
handleDesignChange: (key: keyof DesignMD, value: string | number | string[] | boolean) => void;
|
||||
}
|
||||
@@ -15,7 +16,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 & { onEditAsset?: (type: keyof DesignMD, url: string) => void }> = ({ designMD, handleDesignChange, onEditAsset }) => {
|
||||
export const BrandTabMedia: React.FC<BrandTabMediaProps & { onEditAsset?: (type: keyof DesignMD, url: string) => void }> = ({ company, designMD, handleDesignChange, onEditAsset }) => {
|
||||
|
||||
/** Auto-detect video duration and store it in DesignMD (for BrandPreview playback) */
|
||||
const probeVideoDuration = useCallback((url: string, key: 'introDurationFrames' | 'outroDurationFrames') => {
|
||||
@@ -48,6 +49,7 @@ export const BrandTabMedia: React.FC<BrandTabMediaProps & { onEditAsset?: (type:
|
||||
|
||||
{/* ═══ Intro Video ═══ */}
|
||||
<VideoUploadSimple
|
||||
company={company}
|
||||
label="Video de Cabezote (Intro)"
|
||||
description="Se usará automáticamente en plantillas que incluyan segmento de intro de marca"
|
||||
videoUrl={designMD.introVideoUrl || ''}
|
||||
@@ -70,6 +72,7 @@ export const BrandTabMedia: React.FC<BrandTabMediaProps & { onEditAsset?: (type:
|
||||
|
||||
{/* ═══ Outro Video ═══ */}
|
||||
<VideoUploadSimple
|
||||
company={company}
|
||||
label="Video de Cierre (Outro)"
|
||||
description="Se usará automáticamente en plantillas que incluyan segmento de outro de marca"
|
||||
videoUrl={designMD.outroVideoUrl || ''}
|
||||
@@ -144,10 +147,25 @@ export const BrandTabMedia: React.FC<BrandTabMediaProps & { onEditAsset?: (type:
|
||||
accept="audio/*"
|
||||
label="Subir audio"
|
||||
onFiles={async (files) => {
|
||||
let workspacePath = '';
|
||||
if (window.electronAPI) {
|
||||
workspacePath = await window.electronAPI.fs.getWorkspacePath();
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', files[0]);
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/upload', { method: 'POST', body: formData });
|
||||
let res;
|
||||
if (workspacePath && company.id) {
|
||||
formData.append('brandId', company.id);
|
||||
formData.append('workspacePath', workspacePath);
|
||||
res = await fetch('/api/upload/brand', { method: 'POST', body: formData });
|
||||
} else {
|
||||
res = await fetch('/api/upload', { method: 'POST', body: formData });
|
||||
}
|
||||
|
||||
if (!res.ok) throw new Error('Upload failed');
|
||||
const data = await res.json();
|
||||
if (data.url) handleDesignChange('brandAudioUrl', data.url);
|
||||
} catch (err) {
|
||||
@@ -184,6 +202,7 @@ export const BrandTabMedia: React.FC<BrandTabMediaProps & { onEditAsset?: (type:
|
||||
/* ── Simple Video Upload Card ── */
|
||||
|
||||
const VideoUploadSimple: React.FC<{
|
||||
company: CompanyProfile;
|
||||
label: string;
|
||||
description: string;
|
||||
videoUrl: string;
|
||||
@@ -196,7 +215,7 @@ const VideoUploadSimple: React.FC<{
|
||||
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 }) => {
|
||||
}> = ({ company, 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);
|
||||
|
||||
@@ -258,10 +277,25 @@ const VideoUploadSimple: React.FC<{
|
||||
accept="video/*"
|
||||
label="Subir archivo"
|
||||
onFiles={async (files) => {
|
||||
let workspacePath = '';
|
||||
if (window.electronAPI) {
|
||||
workspacePath = await window.electronAPI.fs.getWorkspacePath();
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', files[0]);
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/upload', { method: 'POST', body: formData });
|
||||
let res;
|
||||
if (workspacePath && company.id) {
|
||||
formData.append('brandId', company.id);
|
||||
formData.append('workspacePath', workspacePath);
|
||||
res = await fetch('/api/upload/brand', { method: 'POST', body: formData });
|
||||
} else {
|
||||
res = await fetch('/api/upload', { method: 'POST', body: formData });
|
||||
}
|
||||
|
||||
if (!res.ok) throw new Error('Upload failed');
|
||||
const data = await res.json();
|
||||
if (data.url) onUrlChange(data.url);
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user