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>
211 lines
6.4 KiB
TypeScript
211 lines
6.4 KiB
TypeScript
'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>
|
||
);
|
||
}
|