feat: add format presets for common conversion scenarios

Format presets included:
**Video Presets:**
- YouTube Video (1080p, H.264, optimized)
- Instagram Video (Square format, 1:1)
- Twitter Video (720p, small file)
- Web Optimized (Streaming, low bitrate)

**Audio Presets:**
- Podcast (MP3, voice optimized, 128k)
- High Quality Music (MP3, 320k)
- Audiobook (Mono, 64k, small size)

**Image Presets:**
- Web Thumbnail (JPG, 800px width)
- HD Image (PNG, lossless, high quality)
- Social Media (JPG, 1200px, optimized)
- Web Optimized (WebP, small size)

Implementation:
- Created formatPresets.ts with 11 predefined presets
- FormatPresets component with responsive grid layout
- One-click preset application
- Automatically configures format and options
- Toast notification on preset selection
- Visual feedback with ring highlight
- Organized by category (video/audio/image)
- Emoji icons for visual appeal

User experience:
- Quick access to common conversion scenarios
- No need to manually configure complex settings
- Preset descriptions explain use case
- Disabled during active conversion
- Clear visual indication of selected preset

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-17 13:30:35 +01:00
parent ba2e011f4a
commit 6b0bda05db
3 changed files with 277 additions and 0 deletions

View File

@@ -9,6 +9,7 @@ import { FormatSelector } from './FormatSelector';
import { ConversionPreview } from './ConversionPreview'; import { ConversionPreview } from './ConversionPreview';
import { ConversionOptionsPanel } from './ConversionOptions'; import { ConversionOptionsPanel } from './ConversionOptions';
import { FileInfo } from './FileInfo'; import { FileInfo } from './FileInfo';
import { FormatPresets } from './FormatPresets';
import { useToast } from '@/components/ui/Toast'; import { useToast } from '@/components/ui/Toast';
import { import {
SUPPORTED_FORMATS, SUPPORTED_FORMATS,
@@ -20,6 +21,7 @@ import { convertWithFFmpeg } from '@/lib/converters/ffmpegService';
import { convertWithImageMagick } from '@/lib/converters/imagemagickService'; import { convertWithImageMagick } from '@/lib/converters/imagemagickService';
import { addToHistory } from '@/lib/storage/history'; import { addToHistory } from '@/lib/storage/history';
import { downloadBlobsAsZip, generateOutputFilename } from '@/lib/utils/fileUtils'; import { downloadBlobsAsZip, generateOutputFilename } from '@/lib/utils/fileUtils';
import { getPresetById, type FormatPreset } from '@/lib/utils/formatPresets';
import type { ConversionJob, ConversionFormat, ConversionOptions } from '@/types/conversion'; import type { ConversionJob, ConversionFormat, ConversionOptions } from '@/types/conversion';
export function FileConverter() { export function FileConverter() {
@@ -209,6 +211,17 @@ export function FileConverter() {
setSelectedFiles((prev) => prev.filter((_, i) => i !== index)); setSelectedFiles((prev) => prev.filter((_, i) => i !== index));
}; };
const handlePresetSelect = (preset: FormatPreset) => {
// Find the output format that matches the preset
const format = compatibleFormats.find(f => f.extension === preset.outputFormat);
if (format) {
setOutputFormat(format);
setConversionOptions(preset.options);
addToast(`Applied ${preset.name} preset`, 'success');
}
};
const handleDownloadAll = async () => { const handleDownloadAll = async () => {
if (!outputFormat) return; if (!outputFormat) return;
@@ -375,6 +388,15 @@ export function FileConverter() {
<FileInfo file={selectedFiles[0]} format={inputFormat} /> <FileInfo file={selectedFiles[0]} format={inputFormat} />
)} )}
{/* Format Presets */}
{inputFormat && (
<FormatPresets
inputFormat={inputFormat}
onPresetSelect={handlePresetSelect}
disabled={isConverting}
/>
)}
{/* Format selection */} {/* Format selection */}
{inputFormat && compatibleFormats.length > 0 && ( {inputFormat && compatibleFormats.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-[1fr_auto_1fr] gap-4 items-start"> <div className="grid grid-cols-1 md:grid-cols-[1fr_auto_1fr] gap-4 items-start">

View File

@@ -0,0 +1,67 @@
'use client';
import * as React from 'react';
import { Sparkles } from 'lucide-react';
import { Card } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { cn } from '@/lib/utils/cn';
import { getPresetsByCategory, type FormatPreset } from '@/lib/utils/formatPresets';
import type { ConversionFormat } from '@/types/conversion';
interface FormatPresetsProps {
inputFormat: ConversionFormat;
onPresetSelect: (preset: FormatPreset) => void;
disabled?: boolean;
}
export function FormatPresets({ inputFormat, onPresetSelect, disabled = false }: FormatPresetsProps) {
const [selectedPresetId, setSelectedPresetId] = React.useState<string | null>(null);
// Get presets for the input format's category
const presets = getPresetsByCategory(inputFormat.category);
if (presets.length === 0) {
return null;
}
const handlePresetClick = (preset: FormatPreset) => {
setSelectedPresetId(preset.id);
onPresetSelect(preset);
};
return (
<Card className="p-4">
<div className="flex items-center gap-2 mb-4">
<Sparkles className="h-5 w-5 text-primary" />
<h3 className="text-sm font-semibold">Quick Presets</h3>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{presets.map((preset) => (
<Button
key={preset.id}
onClick={() => handlePresetClick(preset)}
variant={selectedPresetId === preset.id ? 'default' : 'outline'}
className={cn(
'h-auto py-4 px-4 flex flex-col items-start text-left gap-2',
selectedPresetId === preset.id && 'ring-2 ring-primary ring-offset-2'
)}
disabled={disabled}
>
<div className="flex items-center gap-2 w-full">
<span className="text-2xl">{preset.icon}</span>
<div className="flex-1 min-w-0">
<div className="font-semibold text-sm">{preset.name}</div>
</div>
</div>
<p className="text-xs text-muted-foreground w-full">{preset.description}</p>
</Button>
))}
</div>
<div className="mt-3 text-xs text-muted-foreground">
Select a preset to automatically configure optimal settings for your use case
</div>
</Card>
);
}

188
lib/utils/formatPresets.ts Normal file
View File

@@ -0,0 +1,188 @@
import type { ConversionOptions } from '@/types/conversion';
export interface FormatPreset {
id: string;
name: string;
description: string;
icon: string;
category: 'video' | 'audio' | 'image';
outputFormat: string;
options: ConversionOptions;
}
/**
* Predefined format presets for common use cases
*/
export const FORMAT_PRESETS: FormatPreset[] = [
// Video Presets
{
id: 'youtube-video',
name: 'YouTube Video',
description: '1080p MP4, optimized for YouTube',
icon: '🎬',
category: 'video',
outputFormat: 'mp4',
options: {
videoCodec: 'libx264',
videoBitrate: '5M',
videoResolution: '1920x-1',
videoFps: 30,
audioCodec: 'aac',
audioBitrate: '192k',
},
},
{
id: 'instagram-video',
name: 'Instagram Video',
description: 'Square 1:1 format for Instagram',
icon: '📸',
category: 'video',
outputFormat: 'mp4',
options: {
videoCodec: 'libx264',
videoBitrate: '3M',
videoResolution: '1080x-1', // Will be cropped to square
videoFps: 30,
audioCodec: 'aac',
audioBitrate: '128k',
},
},
{
id: 'twitter-video',
name: 'Twitter Video',
description: '720p, optimized for Twitter',
icon: '🐦',
category: 'video',
outputFormat: 'mp4',
options: {
videoCodec: 'libx264',
videoBitrate: '2M',
videoResolution: '1280x-1',
videoFps: 30,
audioCodec: 'aac',
audioBitrate: '128k',
},
},
{
id: 'web-video',
name: 'Web Optimized',
description: 'Small file size for web streaming',
icon: '🌐',
category: 'video',
outputFormat: 'mp4',
options: {
videoCodec: 'libx264',
videoBitrate: '1.5M',
videoResolution: '854x-1',
videoFps: 24,
audioCodec: 'aac',
audioBitrate: '96k',
},
},
// Audio Presets
{
id: 'podcast-audio',
name: 'Podcast',
description: 'MP3, optimized for voice',
icon: '🎙️',
category: 'audio',
outputFormat: 'mp3',
options: {
audioCodec: 'libmp3lame',
audioBitrate: '128k',
audioSampleRate: 44100,
audioChannels: 2,
},
},
{
id: 'music-high-quality',
name: 'High Quality Music',
description: 'MP3, 320kbps for music',
icon: '🎵',
category: 'audio',
outputFormat: 'mp3',
options: {
audioCodec: 'libmp3lame',
audioBitrate: '320k',
audioSampleRate: 48000,
audioChannels: 2,
},
},
{
id: 'audiobook',
name: 'Audiobook',
description: 'Mono, small file size',
icon: '📚',
category: 'audio',
outputFormat: 'mp3',
options: {
audioCodec: 'libmp3lame',
audioBitrate: '64k',
audioSampleRate: 22050,
audioChannels: 1,
},
},
// Image Presets
{
id: 'web-thumbnail',
name: 'Web Thumbnail',
description: 'JPG, 800px width, optimized',
icon: '🖼️',
category: 'image',
outputFormat: 'jpg',
options: {
imageQuality: 85,
imageWidth: 800,
},
},
{
id: 'hd-image',
name: 'HD Image',
description: 'PNG, high quality, lossless',
icon: '🎨',
category: 'image',
outputFormat: 'png',
options: {
imageQuality: 100,
},
},
{
id: 'social-media-image',
name: 'Social Media',
description: 'JPG, 1200px, optimized',
icon: '📱',
category: 'image',
outputFormat: 'jpg',
options: {
imageQuality: 90,
imageWidth: 1200,
},
},
{
id: 'web-optimized-image',
name: 'Web Optimized',
description: 'WebP, small file size',
icon: '⚡',
category: 'image',
outputFormat: 'webp',
options: {
imageQuality: 80,
},
},
];
/**
* Get presets by category
*/
export function getPresetsByCategory(category: 'video' | 'audio' | 'image'): FormatPreset[] {
return FORMAT_PRESETS.filter(preset => preset.category === category);
}
/**
* Get preset by ID
*/
export function getPresetById(id: string): FormatPreset | undefined {
return FORMAT_PRESETS.find(preset => preset.id === id);
}