Files

211 lines
6.4 KiB
TypeScript
Raw Permalink Normal View History

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
'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>
);
}