OmniVoice-Studio / frontend /src /components /BatchAddDialog.jsx
Lê Phi Nam
Deploy to HF Space
94004a2
Raw
History Blame Contribute Delete
6.28 kB
import React, { useState, useRef, useCallback } from 'react';
import { Upload, Film, Globe, X, Plus, Loader } from 'lucide-react';
import { Button } from '../ui';
import MultiLangPicker from './MultiLangPicker';
import { PRESETS } from '../utils/constants';
import './BatchAddDialog.css';
/**
* BatchAddDialog — multi-file drop zone + shared settings for batch dubbing.
*
* Users drop N video files, pick languages + voice, then click "Add to Queue".
* Each file is POSTed as a separate job to the batch endpoint.
*/
export default function BatchAddDialog({
open,
onClose,
profiles = [],
onEnqueue, // async (files, settings) => void
}) {
const [files, setFiles] = useState([]);
const [langs, setLangs] = useState([{ lang: 'Spanish', code: 'es' }]);
const [voiceId, setVoiceId] = useState('');
const [preserveBg, setPreserveBg] = useState(true);
const [submitting, setSubmitting] = useState(false);
const fileInputRef = useRef(null);
const handleDrop = useCallback((e) => {
e.preventDefault();
const dropped = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('video/'));
if (dropped.length) setFiles(prev => [...prev, ...dropped]);
}, []);
const removeFile = (idx) => {
setFiles(prev => prev.filter((_, i) => i !== idx));
};
const handleSubmit = async () => {
if (!files.length || !langs.length) return;
setSubmitting(true);
try {
await onEnqueue?.(files, { langs, voiceId, preserveBg });
setFiles([]);
onClose?.();
} finally {
setSubmitting(false);
}
};
if (!open) return null;
return (
<div className="batch-add-overlay" onClick={onClose}>
<div className="batch-add" onClick={e => e.stopPropagation()}>
<div className="batch-add__head">
<span className="batch-add__title">
<Plus size={13} /> Add Videos to Queue
</span>
<button type="button" className="batch-add__close" onClick={onClose}>
<X size={13} />
</button>
</div>
<div className="batch-add__body">
{/* Drop zone */}
<div
className="batch-add__drop"
onDragOver={e => { e.preventDefault(); e.currentTarget.classList.add('is-over'); }}
onDragLeave={e => e.currentTarget.classList.remove('is-over')}
onDrop={e => { e.currentTarget.classList.remove('is-over'); handleDrop(e); }}
onClick={() => fileInputRef.current?.click()}
>
<Upload size={24} />
<span>Drop video files here or click to browse</span>
<span className="batch-add__drop-hint">MP4 · MOV · MKV · WEBM</span>
</div>
<input
ref={fileInputRef}
type="file"
accept="video/*"
multiple
className="batch-add__file-input"
onChange={e => {
const added = Array.from(e.target.files);
if (added.length) setFiles(prev => [...prev, ...added]);
e.target.value = '';
}}
/>
{/* File list */}
{files.length > 0 && (
<div className="batch-add__files">
<span className="batch-add__kicker">FILES ({files.length})</span>
{files.map((f, i) => (
<div key={`${f.name}-${i}`} className="batch-add__file-row">
<Film size={10} />
<span className="batch-add__file-name">{f.name}</span>
<span className="batch-add__file-size">{(f.size / 1024 / 1024).toFixed(1)} MB</span>
<button type="button" className="batch-add__file-x" onClick={() => removeFile(i)}>
<X size={9} />
</button>
</div>
))}
</div>
)}
{/* Settings */}
<div className="batch-add__settings">
<div className="batch-add__field">
<span className="batch-add__kicker"><Globe size={9} /> TARGET LANGUAGES</span>
<MultiLangPicker selected={langs} onChange={setLangs} />
</div>
<div className="batch-add__field">
<span className="batch-add__kicker">VOICE</span>
<select
className="input-base batch-add__select"
value={voiceId}
onChange={e => setVoiceId(e.target.value)}
>
<option value="">Default</option>
{profiles.filter(p => !p.instruct).length > 0 && (
<optgroup label="Clone Profiles">
{profiles.filter(p => !p.instruct).map(p => (
<option key={p.id} value={p.id}>{p.name}</option>
))}
</optgroup>
)}
{PRESETS.length > 0 && (
<optgroup label="Presets">
{PRESETS.map(p => (
<option key={p.id} value={`preset:${p.id}`}>{p.name}</option>
))}
</optgroup>
)}
</select>
</div>
<label className="batch-add__toggle">
<input type="checkbox" checked={preserveBg} onChange={e => setPreserveBg(e.target.checked)} />
<span>Preserve background audio (music/FX)</span>
</label>
</div>
</div>
<div className="batch-add__foot">
<span className="batch-add__estimate">
{files.length > 0 && langs.length > 0
? `${files.length} video${files.length > 1 ? 's' : ''} × ${langs.length} lang${langs.length > 1 ? 's' : ''} = ${files.length * langs.length} job${files.length * langs.length > 1 ? 's' : ''}`
: 'Select files and languages'}
</span>
<Button variant="ghost" size="sm" onClick={onClose}>Cancel</Button>
<Button
variant="primary"
size="sm"
onClick={handleSubmit}
disabled={!files.length || !langs.length || submitting}
loading={submitting}
leading={!submitting && <Plus size={10} />}
>
Add to Queue
</Button>
</div>
</div>
</div>
);
}