Files
convert-ui/components/converter/FileInfo.tsx
Sebastian Krüger f253285c25 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>
2025-11-17 11:46:10 +01:00

211 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
);
}