'use client'; import * as React from 'react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Slider } from '@/components/ui/slider'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; 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'; 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 fileInputRef = React.useRef(null); // Detect input format when files are selected React.useEffect(() => { if (selectedFiles.length === 0) { setInputFormat(undefined); setOutputFormat(undefined); setCompatibleFormats([]); setConversionJobs([]); return; } // Use first file to detect format (assume all files same format for batch) const firstFile = selectedFiles[0]; // Try to detect format from extension const ext = firstFile.name.split('.').pop()?.toLowerCase(); let format = ext ? getFormatByExtension(ext) : undefined; // Fallback to MIME type if (!format) { format = getFormatByMimeType(firstFile.type); } if (format) { setInputFormat(format); const compatible = getCompatibleFormats(format); setCompatibleFormats(compatible); // Auto-select first compatible format if (compatible.length > 0 && !outputFormat) { setOutputFormat(compatible[0]); } toast.success(`Detected format: ${format.name} (${selectedFiles.length} file${selectedFiles.length > 1 ? 's' : ''})`); } else { toast.error('Could not detect file format'); setInputFormat(undefined); setCompatibleFormats([]); } }, [selectedFiles]); const handleConvert = async () => { if (selectedFiles.length === 0 || !inputFormat || !outputFormat) { toast.error('Please select files and output format'); return; } // Create conversion jobs for all files const jobs: ConversionJob[] = selectedFiles.map((file) => ({ id: Math.random().toString(36).substring(7), inputFile: file, inputFormat, outputFormat, options: conversionOptions, status: 'pending', progress: 0, startTime: Date.now(), })); setConversionJobs(jobs); // Track success/failure counts let successCount = 0; let failureCount = 0; // Convert files sequentially for (let i = 0; i < jobs.length; i++) { const job = jobs[i]; try { // Update job to loading setConversionJobs((prev) => prev.map((j, idx) => idx === i ? { ...j, status: 'loading' as const } : j) ); // Update job to processing setConversionJobs((prev) => prev.map((j, idx) => idx === i ? { ...j, status: 'processing' as const, progress: 10 } : j) ); // Call appropriate converter let result; if (!outputFormat) throw new Error('Output format not selected'); switch (outputFormat.converter) { case 'ffmpeg': result = await convertWithFFmpeg(job.inputFile, outputFormat.extension, conversionOptions, (progress) => { setConversionJobs((prev) => prev.map((j, idx) => idx === i ? { ...j, progress } : j) ); }); break; case 'imagemagick': result = await convertWithImageMagick( job.inputFile, outputFormat.extension, conversionOptions, (progress) => { setConversionJobs((prev) => prev.map((j, idx) => idx === i ? { ...j, progress } : j) ); } ); break; default: throw new Error(`Unknown converter: ${outputFormat.converter}`); } // Update job with result if (result.success && result.blob) { successCount++; setConversionJobs((prev) => prev.map((j, idx) => idx === i ? { ...j, status: 'completed' as const, progress: 100, result: result.blob, endTime: Date.now(), } : j) ); // Add to history addToHistory({ inputFileName: job.inputFile.name, inputFormat: inputFormat.name, outputFormat: outputFormat.name, outputFileName: `output.${outputFormat.extension}`, fileSize: result.blob.size, result: result.blob, }); } else { failureCount++; setConversionJobs((prev) => prev.map((j, idx) => idx === i ? { ...j, status: 'error' as const, error: result.error || 'Unknown error', endTime: Date.now(), } : j) ); } } catch (error) { failureCount++; const errorMessage = error instanceof Error ? error.message : 'Unknown error'; setConversionJobs((prev) => prev.map((j, idx) => idx === i ? { ...j, status: 'error' as const, error: errorMessage, endTime: Date.now(), } : j) ); } } // Show completion message if (successCount === jobs.length) { toast.success(`All ${jobs.length} files converted successfully!`); } else if (successCount > 0) { toast.info(`${successCount}/${jobs.length} files converted successfully`); } else { toast.error('All conversions failed'); } }; const handleReset = () => { setSelectedFiles([]); setInputFormat(undefined); setOutputFormat(undefined); setCompatibleFormats([]); setConversionJobs([]); setConversionOptions({}); }; const handleFileSelect = (files: File[]) => { setSelectedFiles((prev) => [...prev, ...files]); }; const handleFileRemove = (index: number) => { setSelectedFiles((prev) => prev.filter((_, i) => i !== index)); }; const handleDownloadAll = async () => { if (!outputFormat) return; const completedJobs = conversionJobs.filter(job => job.status === 'completed' && job.result); if (completedJobs.length === 0) { toast.error('No files to download'); return; } if (completedJobs.length === 1) { // Just download the single file const job = completedJobs[0]; const filename = generateOutputFilename(job.inputFile.name, outputFormat.extension); const url = URL.createObjectURL(job.result!); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); return; } // Download multiple files as ZIP const files = completedJobs.map(job => ({ blob: job.result!, filename: generateOutputFilename(job.inputFile.name, outputFormat.extension), })); await downloadBlobsAsZip(files, `converted-files.zip`); toast.success(`Downloaded ${files.length} files as ZIP`); }; const handleRetry = async (jobId: string) => { const jobIndex = conversionJobs.findIndex(j => j.id === jobId); if (jobIndex === -1 || !outputFormat) return; const job = conversionJobs[jobIndex]; try { // Reset job to loading setConversionJobs((prev) => prev.map((j, idx) => idx === jobIndex ? { ...j, status: 'loading' as const, progress: 0, error: undefined, startTime: Date.now(), } : j) ); // Update to processing setConversionJobs((prev) => prev.map((j, idx) => idx === jobIndex ? { ...j, status: 'processing' as const, progress: 10 } : j) ); // Call appropriate converter let result; switch (outputFormat.converter) { case 'ffmpeg': result = await convertWithFFmpeg(job.inputFile, outputFormat.extension, conversionOptions, (progress) => { setConversionJobs((prev) => prev.map((j, idx) => idx === jobIndex ? { ...j, progress } : j) ); }); break; case 'imagemagick': result = await convertWithImageMagick( job.inputFile, outputFormat.extension, conversionOptions, (progress) => { setConversionJobs((prev) => prev.map((j, idx) => idx === jobIndex ? { ...j, progress } : j) ); } ); break; default: throw new Error(`Unknown converter: ${outputFormat.converter}`); } // Update job with result if (result.success && result.blob) { setConversionJobs((prev) => prev.map((j, idx) => idx === jobIndex ? { ...j, status: 'completed' as const, progress: 100, result: result.blob, endTime: Date.now(), } : j) ); toast.success('Conversion completed successfully!'); // Add to history addToHistory({ inputFileName: job.inputFile.name, inputFormat: job.inputFormat.name, outputFormat: outputFormat.name, outputFileName: `output.${outputFormat.extension}`, fileSize: result.blob.size, result: result.blob, }); } else { setConversionJobs((prev) => prev.map((j, idx) => idx === jobIndex ? { ...j, status: 'error' as const, error: result.error || 'Unknown error', endTime: Date.now(), } : j) ); toast.error(result.error || 'Retry failed'); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; setConversionJobs((prev) => prev.map((j, idx) => idx === jobIndex ? { ...j, status: 'error' as const, error: errorMessage, endTime: Date.now(), } : j) ); toast.error(`Retry failed: ${errorMessage}`); } }; const isConverting = conversionJobs.some(job => job.status === 'loading' || job.status === 'processing'); const isConvertDisabled = selectedFiles.length === 0 || !outputFormat || isConverting; const completedCount = conversionJobs.filter(job => job.status === 'completed').length; return (
{/* Left Column: Upload and Conversion Options */}
{/* Upload Card */} {/* File upload */} {/* Conversion Options Card */} {inputFormat && compatibleFormats.length > 0 && ( Conversion Options {/* Output Format Select */}
{/* Advanced Options - Inline */} {outputFormat && ( <> {outputFormat.category === 'video' && (
{/* Video Codec */}
{/* Video Bitrate */}
{conversionOptions.videoBitrate || '2M'}
setConversionOptions({ ...conversionOptions, videoBitrate: `${vals[0]}M` })} disabled={isConverting} />

Higher bitrate = better quality, larger file

{/* Resolution */}
{/* FPS */}
{/* Audio Bitrate */}
{conversionOptions.audioBitrate || '128k'}
setConversionOptions({ ...conversionOptions, audioBitrate: `${vals[0]}k` })} disabled={isConverting} />
)} {outputFormat.category === 'audio' && (
{/* Audio Codec */}
{/* Bitrate */}
{conversionOptions.audioBitrate || '192k'}
setConversionOptions({ ...conversionOptions, audioBitrate: `${vals[0]}k` })} disabled={isConverting} />
{/* Sample Rate */}
{/* Channels */}
)} {outputFormat.category === 'image' && (
{/* Quality */}
{conversionOptions.imageQuality || 85}%
setConversionOptions({ ...conversionOptions, imageQuality: vals[0] })} disabled={isConverting} />
{/* Width */}
setConversionOptions({ ...conversionOptions, imageWidth: e.target.value ? parseInt(e.target.value) : undefined })} placeholder="Original" className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" disabled={isConverting} />

Leave empty to keep original

{/* Height */}
setConversionOptions({ ...conversionOptions, imageHeight: e.target.value ? parseInt(e.target.value) : undefined })} placeholder="Original" className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" disabled={isConverting} />

Leave empty to maintain aspect ratio

)} )} {/* Convert button */}
)}
{/* Right Column: Results */}
{/* Download All Button */} {completedCount > 0 && ( )} {/* Conversion previews */} {conversionJobs.length > 0 && (
{conversionJobs.map((job) => ( handleRetry(job.id)} /> ))}
)}
); }