feat: add comprehensive conversion options and enhanced UI
This major update adds powerful format-specific controls, quality presets, file metadata display, and enhanced progress feedback to significantly improve the user experience. New Components: - ConversionOptionsPanel: Format-specific controls with collapsible advanced options - Video options: codec selection (H.264, H.265, VP8, VP9), bitrate, resolution, FPS - Audio options: codec selection, bitrate, sample rate, channels - Image options: quality slider, width/height controls - Quality Presets: One-click presets (High Quality, Balanced, Small File, Web Optimized) - FileInfo: Displays file metadata including size, duration, dimensions - Slider: Reusable slider component for quality/bitrate controls - Select: Reusable dropdown component for codec/format selection Enhanced Features: - ConversionPreview improvements: - Real-time elapsed time display - Estimated time remaining calculation - File size comparison (input vs output with % reduction/increase) - Better visual status indicators with icons - Enhanced loading states with detailed progress - FileConverter integration: - Passes conversion options to converter services - Manages conversion options state - Resets options on file reset UI/UX Improvements: - Format-specific option panels that adapt to selected output format - Visual preset buttons with icons and descriptions - Collapsible advanced options to reduce clutter - Better progress feedback with time estimates - File size comparison badges showing compression results - Smooth animations and transitions (existing animations already in place) - Responsive design for all new components Technical Details: - Options are properly typed and integrated with ConversionOptions interface - All components support disabled states during conversion - Preview component calculates speed and estimates remaining time - Metadata extraction for video/audio/image files using browser APIs - Proper cleanup of object URLs and timers User Benefits: - Power users can fine-tune codec, bitrate, resolution settings - Beginners can use quality presets with confidence - Better understanding of conversion progress and file size impact - Informed decisions with file metadata display - Professional-grade control over output quality 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
210
components/converter/FileInfo.tsx
Normal file
210
components/converter/FileInfo.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { File, FileVideo, FileAudio, FileImage, Clock, HardDrive, Film, Music } from 'lucide-react';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import type { ConversionFormat } from '@/types/conversion';
|
||||
|
||||
interface FileInfoProps {
|
||||
file: File;
|
||||
format: ConversionFormat;
|
||||
}
|
||||
|
||||
interface FileMetadata {
|
||||
name: string;
|
||||
size: string;
|
||||
type: string;
|
||||
category: string;
|
||||
duration?: string;
|
||||
dimensions?: string;
|
||||
}
|
||||
|
||||
export function FileInfo({ file, format }: FileInfoProps) {
|
||||
const [metadata, setMetadata] = React.useState<FileMetadata | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
extractMetadata(file, format);
|
||||
}, [file, format]);
|
||||
|
||||
const extractMetadata = async (file: File, format: ConversionFormat) => {
|
||||
const sizeInMB = (file.size / (1024 * 1024)).toFixed(2);
|
||||
const baseMetadata: FileMetadata = {
|
||||
name: file.name,
|
||||
size: file.size < 1024 * 1024 ? `${(file.size / 1024).toFixed(2)} KB` : `${sizeInMB} MB`,
|
||||
type: format.name,
|
||||
category: format.category,
|
||||
};
|
||||
|
||||
// Try to extract media-specific metadata
|
||||
if (format.category === 'video' && file.type.startsWith('video/')) {
|
||||
try {
|
||||
const video = document.createElement('video');
|
||||
video.preload = 'metadata';
|
||||
|
||||
const promise = new Promise<FileMetadata>((resolve) => {
|
||||
video.onloadedmetadata = () => {
|
||||
const duration = video.duration;
|
||||
const minutes = Math.floor(duration / 60);
|
||||
const seconds = Math.floor(duration % 60);
|
||||
const durationStr = `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
|
||||
resolve({
|
||||
...baseMetadata,
|
||||
duration: durationStr,
|
||||
dimensions: `${video.videoWidth} × ${video.videoHeight}`,
|
||||
});
|
||||
|
||||
URL.revokeObjectURL(video.src);
|
||||
};
|
||||
|
||||
video.onerror = () => {
|
||||
resolve(baseMetadata);
|
||||
URL.revokeObjectURL(video.src);
|
||||
};
|
||||
});
|
||||
|
||||
video.src = URL.createObjectURL(file);
|
||||
const result = await promise;
|
||||
setMetadata(result);
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error('Failed to extract video metadata:', error);
|
||||
}
|
||||
} else if (format.category === 'audio' && file.type.startsWith('audio/')) {
|
||||
try {
|
||||
const audio = document.createElement('audio');
|
||||
audio.preload = 'metadata';
|
||||
|
||||
const promise = new Promise<FileMetadata>((resolve) => {
|
||||
audio.onloadedmetadata = () => {
|
||||
const duration = audio.duration;
|
||||
const minutes = Math.floor(duration / 60);
|
||||
const seconds = Math.floor(duration % 60);
|
||||
const durationStr = `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
|
||||
resolve({
|
||||
...baseMetadata,
|
||||
duration: durationStr,
|
||||
});
|
||||
|
||||
URL.revokeObjectURL(audio.src);
|
||||
};
|
||||
|
||||
audio.onerror = () => {
|
||||
resolve(baseMetadata);
|
||||
URL.revokeObjectURL(audio.src);
|
||||
};
|
||||
});
|
||||
|
||||
audio.src = URL.createObjectURL(file);
|
||||
const result = await promise;
|
||||
setMetadata(result);
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error('Failed to extract audio metadata:', error);
|
||||
}
|
||||
} else if (format.category === 'image' && file.type.startsWith('image/')) {
|
||||
try {
|
||||
const img = new Image();
|
||||
|
||||
const promise = new Promise<FileMetadata>((resolve) => {
|
||||
img.onload = () => {
|
||||
resolve({
|
||||
...baseMetadata,
|
||||
dimensions: `${img.width} × ${img.height}`,
|
||||
});
|
||||
|
||||
URL.revokeObjectURL(img.src);
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
resolve(baseMetadata);
|
||||
URL.revokeObjectURL(img.src);
|
||||
};
|
||||
});
|
||||
|
||||
img.src = URL.createObjectURL(file);
|
||||
const result = await promise;
|
||||
setMetadata(result);
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error('Failed to extract image metadata:', error);
|
||||
}
|
||||
}
|
||||
|
||||
setMetadata(baseMetadata);
|
||||
};
|
||||
|
||||
const getCategoryIcon = () => {
|
||||
switch (format.category) {
|
||||
case 'video':
|
||||
return <FileVideo className="h-5 w-5 text-primary" />;
|
||||
case 'audio':
|
||||
return <FileAudio className="h-5 w-5 text-primary" />;
|
||||
case 'image':
|
||||
return <FileImage className="h-5 w-5 text-primary" />;
|
||||
default:
|
||||
return <File className="h-5 w-5 text-primary" />;
|
||||
}
|
||||
};
|
||||
|
||||
if (!metadata) {
|
||||
return (
|
||||
<Card className="p-4 animate-pulse">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-5 h-5 bg-secondary rounded"></div>
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="h-4 bg-secondary rounded w-3/4"></div>
|
||||
<div className="h-3 bg-secondary rounded w-1/2"></div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-0.5">{getCategoryIcon()}</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-sm font-medium text-foreground truncate" title={metadata.name}>
|
||||
{metadata.name}
|
||||
</h3>
|
||||
<div className="mt-2 grid grid-cols-2 gap-2 text-xs">
|
||||
{/* File Size */}
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<HardDrive className="h-3.5 w-3.5" />
|
||||
<span>{metadata.size}</span>
|
||||
</div>
|
||||
|
||||
{/* Type */}
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<File className="h-3.5 w-3.5" />
|
||||
<span>{metadata.type}</span>
|
||||
</div>
|
||||
|
||||
{/* Duration (for video/audio) */}
|
||||
{metadata.duration && (
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
<span>{metadata.duration}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Dimensions */}
|
||||
{metadata.dimensions && (
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
{format.category === 'video' ? (
|
||||
<Film className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<FileImage className="h-3.5 w-3.5" />
|
||||
)}
|
||||
<span>{metadata.dimensions}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user