Files
brandly/src/components/panels/AudioPanel.tsx
T

228 lines
9.5 KiB
TypeScript

import React, { useState, useEffect, useCallback } from 'react';
import { X, Music, Play, Pause, Clock, Loader2 } from 'lucide-react';
import { useEditor } from '../../context/EditorContext';
import { FileDropZone } from '../ui/FileDropZone';
import { uploadMedia } from '../../utils/mediaUploader';
import { useAudioPreview } from '../../hooks/useAudioPreview';
import { getAudioDuration, formatDuration } from '../../utils/audioMetadata';
import { AudioWaveformCanvas } from '../timeline/AudioWaveformCanvas';
interface AudioPanelProps {
onClose: () => void;
}
interface AudioItem {
src: string;
name: string;
duration: number | null;
}
/**
* Panel for adding audio files. Draggable to timeline.
* Auto-routes to audio layers. Supports preview and waveform.
*/
export const AudioPanel: React.FC<AudioPanelProps> = ({ onClose }) => {
const { designMD } = useEditor();
const [localAudios, setLocalAudios] = useState<AudioItem[]>([]);
const [brandDuration, setBrandDuration] = useState<number | null>(null);
const preview = useAudioPreview();
const [playingIdx, setPlayingIdx] = useState<number | null>(null);
const [isUploading, setIsUploading] = useState(false);
// Load brand audio duration
useEffect(() => {
if (designMD.brandAudioUrl) {
getAudioDuration(designMD.brandAudioUrl).then(d => setBrandDuration(d));
}
}, [designMD.brandAudioUrl]);
const handleUpload = useCallback(async (files: File[]) => {
const audioFiles = files.filter(f => f.type.startsWith('audio/'));
if (audioFiles.length === 0) return;
setIsUploading(true);
try {
const items: AudioItem[] = [];
for (const file of audioFiles) {
const result = await uploadMedia(file);
let duration: number | null = null;
try {
duration = await getAudioDuration(result.url);
} catch {}
items.push({ src: result.url, name: result.originalName, duration });
}
setLocalAudios(prev => [...items, ...prev]);
} catch (err) {
console.error('Audio upload failed:', err);
} finally {
setIsUploading(false);
}
}, []);
const handleTogglePreview = useCallback((src: string, idx: number) => {
if (playingIdx === idx) {
preview.pause();
setPlayingIdx(null);
} else {
preview.setSrc(src);
preview.play();
setPlayingIdx(idx);
}
}, [playingIdx, preview]);
// Stop preview when panel closes
useEffect(() => {
return () => {
preview.pause();
};
}, []);
const allAudios = [...localAudios];
const hasBrandAudio = !!designMD.brandAudioUrl;
return (
<div className="w-64 bg-neutral-900 border-r border-neutral-800/60 flex flex-col h-full z-10 shrink-0 shadow-lg animate-in slide-in-from-left-2 duration-200">
<div className="p-3 border-b border-neutral-800 flex items-center justify-between">
<h3 className="text-sm font-semibold text-white flex items-center gap-2">
<Music size={14} className="text-violet-400" />
Audio
</h3>
<button onClick={onClose} title="Cerrar Panel" className="text-neutral-400 hover:text-white p-1 rounded-md hover:bg-neutral-800 transition-colors">
<X size={16} />
</button>
</div>
<div className="p-3 flex-1 overflow-y-auto space-y-4">
{/* Upload */}
<FileDropZone
accept="audio/*"
multiple
onFiles={handleUpload}
label={isUploading ? 'Subiendo...' : "Subir audio"}
sublabel={isUploading ? undefined : "MP3, WAV, OGG"}
/>
{isUploading && (
<div className="flex items-center justify-center gap-2 py-2 text-violet-400">
<Loader2 size={14} className="animate-spin" />
<span className="text-[10px] font-medium">Subiendo al servidor...</span>
</div>
)}
{/* Brand Audio */}
{hasBrandAudio && (
<div>
<span className="text-[10px] font-semibold text-neutral-500 uppercase tracking-widest mb-2 block">Audio de Marca</span>
<div
className="flex flex-col gap-2 p-2.5 bg-neutral-800/50 border border-neutral-800 rounded-lg cursor-grab active:cursor-grabbing hover:border-violet-500/40 transition-colors group"
draggable
onDragStart={(e) => {
e.dataTransfer.setData('text/plain', designMD.brandAudioUrl!);
e.dataTransfer.setData('application/json', JSON.stringify({ type: 'audio', src: designMD.brandAudioUrl }));
e.dataTransfer.effectAllowed = 'copy';
}}
>
<div className="flex items-center gap-3">
<button
onClick={(e) => { e.stopPropagation(); handleTogglePreview(designMD.brandAudioUrl!, -1); }}
className="w-8 h-8 rounded-md bg-violet-600/20 border border-violet-500/30 flex items-center justify-center shrink-0 hover:bg-violet-600/40 transition-colors"
title={playingIdx === -1 ? "Pausar Preview" : "Escuchar Preview"}
>
{playingIdx === -1 ? <Pause size={12} className="text-violet-300" /> : <Play size={12} className="text-violet-400 ml-0.5" />}
</button>
<div className="flex-1 min-w-0">
<span className="text-[11px] font-medium text-white block truncate">Jingle de Marca</span>
<div className="flex items-center gap-1.5">
<span className="text-[9px] text-neutral-500">Arrastrar al timeline</span>
{brandDuration !== null && (
<span className="text-[9px] text-neutral-600 font-mono flex items-center gap-0.5">
<Clock size={8} /> {formatDuration(brandDuration)}
</span>
)}
</div>
</div>
</div>
{/* Mini Waveform */}
<div className="bg-neutral-900/50 rounded overflow-hidden">
<AudioWaveformCanvas
src={designMD.brandAudioUrl!}
width={220}
height={24}
color="rgba(139, 92, 246, 0.4)"
resolution={100}
/>
</div>
</div>
</div>
)}
{/* Uploaded Audios */}
{localAudios.length > 0 && (
<div>
<span className="text-[10px] font-semibold text-neutral-500 uppercase tracking-widest mb-2 block">Mis Audios</span>
<div className="space-y-2">
{localAudios.map((audio, i) => (
<div
key={`audio-${i}`}
className="flex flex-col gap-2 p-2.5 bg-neutral-950/50 border border-neutral-800/60 rounded-lg cursor-grab active:cursor-grabbing hover:border-neutral-700 transition-colors group"
draggable
onDragStart={(e) => {
e.dataTransfer.setData('text/plain', audio.src);
e.dataTransfer.setData('application/json', JSON.stringify({
type: 'audio',
src: audio.src,
fileName: audio.name,
}));
e.dataTransfer.effectAllowed = 'copy';
}}
>
<div className="flex items-center gap-3">
<button
onClick={(e) => { e.stopPropagation(); handleTogglePreview(audio.src, i); }}
className="w-8 h-8 rounded-md bg-neutral-800 flex items-center justify-center shrink-0 hover:bg-neutral-700 transition-colors"
title={playingIdx === i ? "Pausar Preview" : "Escuchar Preview"}
>
{playingIdx === i ? <Pause size={12} className="text-violet-300" /> : <Play size={12} className="text-neutral-400 ml-0.5" />}
</button>
<div className="flex-1 min-w-0">
<span className="text-[11px] font-medium text-neutral-300 block truncate" title={audio.name}>{audio.name}</span>
<div className="flex items-center gap-1.5">
<span className="text-[9px] text-neutral-600">Arrastrar al timeline</span>
{audio.duration !== null && (
<span className="text-[9px] text-neutral-600 font-mono flex items-center gap-0.5">
<Clock size={8} /> {formatDuration(audio.duration)}
</span>
)}
</div>
</div>
</div>
{/* Mini Waveform */}
<div className="bg-neutral-900/30 rounded overflow-hidden">
<AudioWaveformCanvas
src={audio.src}
width={220}
height={20}
color="rgba(129, 140, 248, 0.35)"
resolution={80}
/>
</div>
</div>
))}
</div>
</div>
)}
{/* Empty state */}
{!hasBrandAudio && localAudios.length === 0 && (
<div className="text-center py-6 text-neutral-500">
<Music size={28} className="mx-auto mb-2 opacity-40" />
<p className="text-xs font-medium">Sin audio disponible</p>
<p className="text-[10px] mt-1">Sube archivos de audio o configura el jingle de marca</p>
</div>
)}
</div>
</div>
);
};