Files
kit-ui/components/media/ConversionOptions.tsx

457 lines
16 KiB
TypeScript
Raw Normal View History

'use client';
import * as React from 'react';
import { ChevronDown, ChevronUp, Sparkles } from 'lucide-react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Slider } from '@/components/ui/slider';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import type { ConversionOptions, ConversionFormat } from '@/types/media';
interface ConversionOptionsProps {
inputFormat: ConversionFormat;
outputFormat: ConversionFormat;
options: ConversionOptions;
onOptionsChange: (options: ConversionOptions) => void;
disabled?: boolean;
}
interface QualityPreset {
id: string;
name: string;
description: string;
icon: string;
options: ConversionOptions;
}
export function ConversionOptionsPanel({
inputFormat,
outputFormat,
options,
onOptionsChange,
disabled = false,
}: ConversionOptionsProps) {
const [isExpanded, setIsExpanded] = React.useState(false);
const [selectedPreset, setSelectedPreset] = React.useState<string | null>(null);
// Quality presets based on output format category
const getPresets = (): QualityPreset[] => {
const category = outputFormat.category;
if (category === 'video') {
return [
{
id: 'high-quality',
name: 'High Quality',
description: 'Best quality, larger file size',
icon: '⭐',
options: {
videoBitrate: '5M',
videoCodec: outputFormat.extension === 'webm' ? 'libvpx' : 'libx264',
audioBitrate: '192k',
audioCodec: outputFormat.extension === 'webm' ? 'libvorbis' : 'aac',
},
},
{
id: 'balanced',
name: 'Balanced',
description: 'Good quality, moderate size',
icon: '⚖️',
options: {
videoBitrate: '2M',
videoCodec: outputFormat.extension === 'webm' ? 'libvpx' : 'libx264',
audioBitrate: '128k',
audioCodec: outputFormat.extension === 'webm' ? 'libvorbis' : 'aac',
},
},
{
id: 'small-file',
name: 'Small File',
description: 'Smaller size, lower quality',
icon: '📦',
options: {
videoBitrate: '1M',
videoCodec: outputFormat.extension === 'webm' ? 'libvpx' : 'libx264',
audioBitrate: '96k',
audioCodec: outputFormat.extension === 'webm' ? 'libvorbis' : 'aac',
},
},
{
id: 'web-optimized',
name: 'Web Optimized',
description: 'Fast loading for web',
icon: '🌐',
options: {
videoBitrate: '1.5M',
videoCodec: 'libvpx',
audioBitrate: '128k',
audioCodec: 'libvorbis',
videoResolution: '720x-1',
},
},
];
} else if (category === 'audio') {
return [
{
id: 'high-quality',
name: 'High Quality',
description: 'Best audio quality',
icon: '⭐',
options: {
audioBitrate: '320k',
audioCodec: outputFormat.extension === 'mp3' ? 'libmp3lame' : 'default',
},
},
{
id: 'balanced',
name: 'Balanced',
description: 'Good quality, smaller size',
icon: '⚖️',
options: {
audioBitrate: '192k',
audioCodec: outputFormat.extension === 'mp3' ? 'libmp3lame' : 'default',
},
},
{
id: 'small-file',
name: 'Small File',
description: 'Minimum file size',
icon: '📦',
options: {
audioBitrate: '128k',
audioCodec: outputFormat.extension === 'mp3' ? 'libmp3lame' : 'default',
},
},
];
} else if (category === 'image') {
return [
{
id: 'high-quality',
name: 'High Quality',
description: 'Best image quality',
icon: '⭐',
options: {
imageQuality: 95,
},
},
{
id: 'balanced',
name: 'Balanced',
description: 'Good quality',
icon: '⚖️',
options: {
imageQuality: 85,
},
},
{
id: 'web-optimized',
name: 'Web Optimized',
description: 'Optimized for web',
icon: '🌐',
options: {
imageQuality: 75,
},
},
];
}
return [];
};
const presets = getPresets();
const handlePresetClick = (preset: QualityPreset) => {
setSelectedPreset(preset.id);
onOptionsChange({ ...options, ...preset.options });
};
const handleOptionChange = (key: string, value: any) => {
setSelectedPreset(null); // Clear preset when manual change
onOptionsChange({ ...options, [key]: value });
};
const renderVideoOptions = () => (
<div className="space-y-4">
{/* Video Codec */}
<div className="space-y-2">
<label className="text-sm font-medium text-foreground block">Video Codec</label>
<Select
value={options.videoCodec || 'default'}
onValueChange={(value) => handleOptionChange('videoCodec', value === 'default' ? undefined : value)}
disabled={disabled}
>
<SelectTrigger>
<SelectValue placeholder="Select video codec" />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">Auto (Recommended)</SelectItem>
<SelectItem value="libx264">H.264 (MP4, AVI, MOV)</SelectItem>
<SelectItem value="libx265">H.265 (MP4)</SelectItem>
<SelectItem value="libvpx">VP8 (WebM)</SelectItem>
<SelectItem value="libvpx-vp9">VP9 (WebM)</SelectItem>
</SelectContent>
</Select>
</div>
{/* Video Bitrate */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-sm font-medium text-foreground">Video Bitrate</label>
<span className="text-xs text-muted-foreground">{options.videoBitrate || '2M'}</span>
</div>
<Slider
min={0.5}
max={10}
step={0.5}
value={[parseFloat(options.videoBitrate?.replace('M', '') || '2')]}
onValueChange={(vals) => handleOptionChange('videoBitrate', `${vals[0]}M`)}
disabled={disabled}
/>
<p className="text-xs text-muted-foreground">Higher bitrate = better quality, larger file</p>
</div>
{/* Resolution */}
<div className="space-y-2">
<label className="text-sm font-medium text-foreground block">Resolution</label>
<Select
value={options.videoResolution || 'original'}
onValueChange={(value) => handleOptionChange('videoResolution', value === 'original' ? undefined : value)}
disabled={disabled}
>
<SelectTrigger>
<SelectValue placeholder="Select resolution" />
</SelectTrigger>
<SelectContent>
<SelectItem value="original">Original</SelectItem>
<SelectItem value="1920x-1">1080p (1920x1080)</SelectItem>
<SelectItem value="1280x-1">720p (1280x720)</SelectItem>
<SelectItem value="854x-1">480p (854x480)</SelectItem>
<SelectItem value="640x-1">360p (640x360)</SelectItem>
</SelectContent>
</Select>
</div>
{/* FPS */}
<div className="space-y-2">
<label className="text-sm font-medium text-foreground block">Frame Rate (FPS)</label>
<Select
value={options.videoFps?.toString() || 'original'}
onValueChange={(value) => handleOptionChange('videoFps', value === 'original' ? undefined : parseInt(value))}
disabled={disabled}
>
<SelectTrigger>
<SelectValue placeholder="Select frame rate" />
</SelectTrigger>
<SelectContent>
<SelectItem value="original">Original</SelectItem>
<SelectItem value="60">60 fps</SelectItem>
<SelectItem value="30">30 fps</SelectItem>
<SelectItem value="24">24 fps</SelectItem>
<SelectItem value="15">15 fps</SelectItem>
</SelectContent>
</Select>
</div>
{/* Audio Bitrate */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-sm font-medium text-foreground">Audio Bitrate</label>
<span className="text-xs text-muted-foreground">{options.audioBitrate || '128k'}</span>
</div>
<Slider
min={64}
max={320}
step={32}
value={[parseInt(options.audioBitrate?.replace('k', '') || '128')]}
onValueChange={(vals) => handleOptionChange('audioBitrate', `${vals[0]}k`)}
disabled={disabled}
/>
</div>
</div>
);
const renderAudioOptions = () => (
<div className="space-y-4">
{/* Audio Codec */}
<div className="space-y-2">
<label className="text-sm font-medium text-foreground block">Audio Codec</label>
<Select
value={options.audioCodec || 'default'}
onValueChange={(value) => handleOptionChange('audioCodec', value === 'default' ? undefined : value)}
disabled={disabled}
>
<SelectTrigger>
<SelectValue placeholder="Select audio codec" />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">Auto (Recommended)</SelectItem>
<SelectItem value="libmp3lame">MP3 (LAME)</SelectItem>
<SelectItem value="aac">AAC</SelectItem>
<SelectItem value="libvorbis">Vorbis (OGG)</SelectItem>
<SelectItem value="libopus">Opus</SelectItem>
<SelectItem value="flac">FLAC (Lossless)</SelectItem>
</SelectContent>
</Select>
</div>
{/* Bitrate */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-sm font-medium text-foreground">Bitrate</label>
<span className="text-xs text-muted-foreground">{options.audioBitrate || '192k'}</span>
</div>
<Slider
min={64}
max={320}
step={32}
value={[parseInt(options.audioBitrate?.replace('k', '') || '192')]}
onValueChange={(vals) => handleOptionChange('audioBitrate', `${vals[0]}k`)}
disabled={disabled}
/>
</div>
{/* Sample Rate */}
<div className="space-y-2">
<label className="text-sm font-medium text-foreground block">Sample Rate</label>
<Select
value={options.audioSampleRate?.toString() || 'original'}
onValueChange={(value) => handleOptionChange('audioSampleRate', value === 'original' ? undefined : parseInt(value))}
disabled={disabled}
>
<SelectTrigger>
<SelectValue placeholder="Select sample rate" />
</SelectTrigger>
<SelectContent>
<SelectItem value="original">Original</SelectItem>
<SelectItem value="48000">48 kHz (Studio)</SelectItem>
<SelectItem value="44100">44.1 kHz (CD Quality)</SelectItem>
<SelectItem value="22050">22.05 kHz</SelectItem>
</SelectContent>
</Select>
</div>
{/* Channels */}
<div className="space-y-2">
<label className="text-sm font-medium text-foreground block">Channels</label>
<Select
value={options.audioChannels?.toString() || 'original'}
onValueChange={(value) => handleOptionChange('audioChannels', value === 'original' ? undefined : parseInt(value))}
disabled={disabled}
>
<SelectTrigger>
<SelectValue placeholder="Select channels" />
</SelectTrigger>
<SelectContent>
<SelectItem value="original">Original</SelectItem>
<SelectItem value="2">Stereo (2 channels)</SelectItem>
<SelectItem value="1">Mono (1 channel)</SelectItem>
</SelectContent>
</Select>
</div>
</div>
);
const renderImageOptions = () => (
<div className="space-y-4">
{/* Quality */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-sm font-medium text-foreground">Quality</label>
<span className="text-xs text-muted-foreground">{options.imageQuality || 85}%</span>
</div>
<Slider
min={1}
max={100}
step={1}
value={[options.imageQuality || 85]}
onValueChange={(vals) => handleOptionChange('imageQuality', vals[0])}
disabled={disabled}
/>
</div>
{/* Width */}
<div>
<label className="text-sm font-medium text-foreground mb-2 block">Width (px)</label>
<input
type="number"
value={options.imageWidth || ''}
onChange={(e) => handleOptionChange('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={disabled}
/>
<p className="text-xs text-muted-foreground mt-1">Leave empty to keep original</p>
</div>
{/* Height */}
<div>
<label className="text-sm font-medium text-foreground mb-2 block">Height (px)</label>
<input
type="number"
value={options.imageHeight || ''}
onChange={(e) => handleOptionChange('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={disabled}
/>
<p className="text-xs text-muted-foreground mt-1">Leave empty to maintain aspect ratio</p>
</div>
</div>
);
return (
<Card className="p-4">
{/* Presets Section */}
{presets.length > 0 && (
<div className="mb-4">
<div className="flex items-center gap-2 mb-3">
<Sparkles className="h-4 w-4 text-primary" />
<h3 className="text-sm font-semibold">Quality Presets</h3>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
{presets.map((preset) => (
<Button
key={preset.id}
onClick={() => handlePresetClick(preset)}
variant={selectedPreset === preset.id ? 'default' : 'outline'}
size="sm"
className="flex flex-col h-auto py-3 px-3 text-left items-start"
disabled={disabled}
>
<span className="text-base mb-1">{preset.icon}</span>
<span className="text-xs font-medium">{preset.name}</span>
<span className="text-xs text-muted-foreground mt-1">{preset.description}</span>
</Button>
))}
</div>
</div>
)}
{/* Advanced Options Toggle */}
<button
onClick={() => setIsExpanded(!isExpanded)}
className="w-full flex items-center justify-between text-sm font-medium text-foreground hover:text-primary transition-colors mb-3"
disabled={disabled}
>
<span>Advanced Options</span>
{isExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</button>
{/* Advanced Options Panel */}
{isExpanded && (
<div className="pt-3 border-t border-border">
{outputFormat.category === 'video' && renderVideoOptions()}
{outputFormat.category === 'audio' && renderAudioOptions()}
{outputFormat.category === 'image' && renderImageOptions()}
</div>
)}
</Card>
);
}