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:
@@ -9,6 +9,7 @@ import { FormatSelector } from './FormatSelector';
|
||||
import { ConversionPreview } from './ConversionPreview';
|
||||
import { ConversionOptionsPanel } from './ConversionOptions';
|
||||
import { FileInfo } from './FileInfo';
|
||||
import { FormatPresets } from './FormatPresets';
|
||||
import { useToast } from '@/components/ui/Toast';
|
||||
import {
|
||||
SUPPORTED_FORMATS,
|
||||
@@ -20,6 +21,7 @@ import { convertWithFFmpeg } from '@/lib/converters/ffmpegService';
|
||||
import { convertWithImageMagick } from '@/lib/converters/imagemagickService';
|
||||
import { addToHistory } from '@/lib/storage/history';
|
||||
import { downloadBlobsAsZip, generateOutputFilename } from '@/lib/utils/fileUtils';
|
||||
import { getPresetById, type FormatPreset } from '@/lib/utils/formatPresets';
|
||||
import type { ConversionJob, ConversionFormat, ConversionOptions } from '@/types/conversion';
|
||||
|
||||
export function FileConverter() {
|
||||
@@ -209,6 +211,17 @@ export function FileConverter() {
|
||||
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 () => {
|
||||
if (!outputFormat) return;
|
||||
|
||||
@@ -375,6 +388,15 @@ export function FileConverter() {
|
||||
<FileInfo file={selectedFiles[0]} format={inputFormat} />
|
||||
)}
|
||||
|
||||
{/* Format Presets */}
|
||||
{inputFormat && (
|
||||
<FormatPresets
|
||||
inputFormat={inputFormat}
|
||||
onPresetSelect={handlePresetSelect}
|
||||
disabled={isConverting}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Format selection */}
|
||||
{inputFormat && compatibleFormats.length > 0 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-[1fr_auto_1fr] gap-4 items-start">
|
||||
|
||||
67
components/converter/FormatPresets.tsx
Normal file
67
components/converter/FormatPresets.tsx
Normal 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
188
lib/utils/formatPresets.ts
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user