'use client'; import * as React from 'react'; import { SliderRow } from '@/components/ui/slider-row'; import { MobileTabs } from '@/components/ui/mobile-tabs'; import { FileUpload } from './FileUpload'; import { ConversionPreview } from './ConversionPreview'; import { toast } from 'sonner'; import { getFormatByExtension, getFormatByMimeType, getCompatibleFormats, } from '@/lib/media/utils/formatMappings'; import { convertWithFFmpeg } from '@/lib/media/converters/ffmpegService'; import { convertWithImageMagick } from '@/lib/media/converters/imagemagickService'; import { addToHistory } from '@/lib/media/storage/history'; import { downloadBlobsAsZip, generateOutputFilename } from '@/lib/media/utils/fileUtils'; import type { ConversionJob, ConversionFormat, ConversionOptions } from '@/types/media'; import { ShieldCheck, Download, RotateCcw, Loader2 } from 'lucide-react'; import { cn, actionBtn, cardBtn } from '@/lib/utils'; type MobileTab = 'upload' | 'convert'; const selectCls = 'w-full bg-transparent border border-border/40 rounded-lg px-2.5 py-1.5 text-xs font-mono outline-none focus:border-primary/50 transition-colors text-foreground/80 cursor-pointer disabled:opacity-40'; export function FileConverter() { const [selectedFiles, setSelectedFiles] = React.useState([]); const [inputFormat, setInputFormat] = React.useState(); const [outputFormat, setOutputFormat] = React.useState(); const [compatibleFormats, setCompatibleFormats] = React.useState([]); const [conversionJobs, setConversionJobs] = React.useState([]); const [conversionOptions, setConversionOptions] = React.useState({}); const [mobileTab, setMobileTab] = React.useState('upload'); const fileInputRef = React.useRef(null); // Detect format when files change React.useEffect(() => { if (selectedFiles.length === 0) { setInputFormat(undefined); setOutputFormat(undefined); setCompatibleFormats([]); setConversionJobs([]); return; } const first = selectedFiles[0]; const ext = first.name.split('.').pop()?.toLowerCase(); const fmt = (ext ? getFormatByExtension(ext) : undefined) ?? getFormatByMimeType(first.type); if (fmt) { setInputFormat(fmt); const compat = getCompatibleFormats(fmt); setCompatibleFormats(compat); if (compat.length > 0 && !outputFormat) setOutputFormat(compat[0]); toast.success(`Detected: ${fmt.name} · ${selectedFiles.length} file${selectedFiles.length > 1 ? 's' : ''}`); setMobileTab('convert'); } else { toast.error('Could not detect file format'); setInputFormat(undefined); setCompatibleFormats([]); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedFiles]); const runConversion = async ( jobIndex: number, jobs: ConversionJob[], outFmt: ConversionFormat ) => { const job = jobs[jobIndex]; const updateJob = (patch: Partial) => setConversionJobs((prev) => prev.map((j, i) => (i === jobIndex ? { ...j, ...patch } : j))); try { updateJob({ status: 'loading' }); updateJob({ status: 'processing', progress: 10 }); const onProgress = (progress: number) => updateJob({ progress }); const result = outFmt.converter === 'ffmpeg' ? await convertWithFFmpeg(job.inputFile, outFmt.extension, conversionOptions, onProgress) : await convertWithImageMagick(job.inputFile, outFmt.extension, conversionOptions, onProgress); if (result.success && result.blob) { updateJob({ status: 'completed', progress: 100, result: result.blob, endTime: Date.now() }); addToHistory({ inputFileName: job.inputFile.name, inputFormat: job.inputFormat.name, outputFormat: outFmt.name, outputFileName: generateOutputFilename(job.inputFile.name, outFmt.extension), fileSize: result.blob.size, result: result.blob, }); return true; } else { updateJob({ status: 'error', error: result.error || 'Unknown error', endTime: Date.now() }); return false; } } catch (err) { updateJob({ status: 'error', error: err instanceof Error ? err.message : 'Unknown error', endTime: Date.now() }); return false; } }; const handleConvert = async () => { if (!selectedFiles.length || !inputFormat || !outputFormat) { toast.error('Please select files and an output format'); return; } const jobs: ConversionJob[] = selectedFiles.map((file) => ({ id: Math.random().toString(36).slice(2, 9), inputFile: file, inputFormat: inputFormat!, outputFormat, options: conversionOptions, status: 'pending', progress: 0, startTime: Date.now(), })); setConversionJobs(jobs); let ok = 0; for (let i = 0; i < jobs.length; i++) { const success = await runConversion(i, jobs, outputFormat); if (success) ok++; } if (ok === jobs.length) toast.success(`All ${jobs.length} file${jobs.length > 1 ? 's' : ''} converted!`); else if (ok > 0) toast.info(`${ok}/${jobs.length} files converted`); else toast.error('All conversions failed'); }; const handleRetry = async (jobId: string) => { const idx = conversionJobs.findIndex((j) => j.id === jobId); if (idx === -1 || !outputFormat) return; setConversionJobs((prev) => prev.map((j, i) => i === idx ? { ...j, status: 'loading', progress: 0, error: undefined, startTime: Date.now() } : j ) ); const success = await runConversion(idx, conversionJobs, outputFormat); if (success) toast.success('Conversion completed!'); else toast.error('Retry failed'); }; const handleReset = () => { setSelectedFiles([]); setInputFormat(undefined); setOutputFormat(undefined); setCompatibleFormats([]); setConversionJobs([]); setConversionOptions({}); setMobileTab('upload'); }; const handleDownloadAll = async () => { if (!outputFormat) return; const done = conversionJobs.filter((j) => j.status === 'completed' && j.result); if (!done.length) { toast.error('No completed files'); return; } if (done.length === 1) { const url = URL.createObjectURL(done[0].result!); const a = document.createElement('a'); a.href = url; a.download = generateOutputFilename(done[0].inputFile.name, outputFormat.extension); a.click(); URL.revokeObjectURL(url); return; } await downloadBlobsAsZip( done.map((j) => ({ blob: j.result!, filename: generateOutputFilename(j.inputFile.name, outputFormat.extension) })), 'converted-files.zip' ); toast.success(`Downloaded ${done.length} files as ZIP`); }; const isConverting = conversionJobs.some((j) => j.status === 'loading' || j.status === 'processing'); const completedCount = conversionJobs.filter((j) => j.status === 'completed').length; const setOpt = (patch: Partial) => setConversionOptions((prev) => ({ ...prev, ...patch })); return (
setMobileTab(v as MobileTab)} /> {/* ── Main layout ─────────────────────────────────────── */}
{/* Left: upload zone */}
Upload Zero uploads
setSelectedFiles((prev) => [...prev, ...files])} onFileRemove={(i) => setSelectedFiles((prev) => prev.filter((_, idx) => idx !== i))} selectedFiles={selectedFiles} disabled={isConverting} inputRef={fileInputRef} inputFormat={inputFormat} />
{/* Right: options + results */}
{inputFormat && compatibleFormats.length > 0 ? (
{/* Detected format */}
Output Format {inputFormat && ( {inputFormat.name} )}
{/* Format pill grid */}
{compatibleFormats.map((fmt) => ( ))}
{outputFormat && ( <>
{/* Video options */} {outputFormat.category === 'video' && ( <>
Video Codec
setOpt({ videoBitrate: `${v}M` })} disabled={isConverting} />
Resolution
FPS
setOpt({ audioBitrate: `${v}k` })} disabled={isConverting} /> )} {/* Audio options */} {outputFormat.category === 'audio' && ( <>
Codec
setOpt({ audioBitrate: `${v}k` })} disabled={isConverting} />
Sample Rate
Channels
)} {/* Image options */} {outputFormat.category === 'image' && ( <> setOpt({ imageQuality: v })} disabled={isConverting} />
{(['imageWidth', 'imageHeight'] as const).map((key) => (
{key === 'imageWidth' ? 'Width (px)' : 'Height (px)'} setOpt({ [key]: e.target.value ? parseInt(e.target.value) : undefined })} placeholder="Original" disabled={isConverting} className="w-full bg-transparent border border-border/40 rounded-lg px-2.5 py-1.5 text-xs font-mono outline-none focus:border-primary/50 transition-colors placeholder:text-muted-foreground/30 disabled:opacity-40" />
))}
)}
{/* Action buttons */}
)}
) : ( /* No files yet — right panel placeholder */

Upload files to see conversion options

)} {/* Results panel */} {conversionJobs.length > 0 && (
Results {completedCount > 0 && ( )}
{conversionJobs.map((job) => ( handleRetry(job.id)} /> ))}
)}
); }