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:
2025-11-17 11:46:10 +01:00
parent 594a0ca314
commit f253285c25
6 changed files with 864 additions and 22 deletions

View File

@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { Download, CheckCircle, XCircle, Loader2 } from 'lucide-react';
import { Download, CheckCircle, XCircle, Loader2, Clock, TrendingUp, FileCheck2, ArrowRight } from 'lucide-react';
import { cn } from '@/lib/utils/cn';
import { Button } from '@/components/ui/Button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
@@ -16,6 +16,32 @@ export interface ConversionPreviewProps {
export function ConversionPreview({ job, onDownload }: ConversionPreviewProps) {
const [previewUrl, setPreviewUrl] = React.useState<string | null>(null);
const [elapsedTime, setElapsedTime] = React.useState(0);
const [estimatedTimeRemaining, setEstimatedTimeRemaining] = React.useState<number | null>(null);
// Timer for elapsed time and estimation
React.useEffect(() => {
if (job.status === 'processing' || job.status === 'loading') {
const interval = setInterval(() => {
if (job.startTime) {
const elapsed = Date.now() - job.startTime;
setElapsedTime(elapsed);
// Estimate time remaining based on progress
if (job.progress > 5 && job.progress < 100) {
const progressRate = job.progress / elapsed;
const remainingProgress = 100 - job.progress;
const estimated = remainingProgress / progressRate;
setEstimatedTimeRemaining(estimated);
}
}
}, 100);
return () => clearInterval(interval);
} else {
setEstimatedTimeRemaining(null);
}
}, [job.status, job.startTime, job.progress]);
// Create preview URL for result
React.useEffect(() => {
@@ -77,44 +103,109 @@ export function ConversionPreview({ job, onDownload }: ConversionPreviewProps) {
}
};
const formatTime = (ms: number) => {
const seconds = Math.floor(ms / 1000);
if (seconds < 60) return `${seconds}s`;
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes}m ${remainingSeconds}s`;
};
const renderStatus = () => {
switch (job.status) {
case 'loading':
return (
<div className="flex items-center gap-2 text-info">
<Loader2 className="h-4 w-4 animate-spin" />
<span className="text-sm font-medium">Loading converter...</span>
<div className="space-y-3">
<div className="flex items-center gap-2 text-info">
<Loader2 className="h-4 w-4 animate-spin" />
<span className="text-sm font-medium">Loading WASM converter...</span>
</div>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Clock className="h-3.5 w-3.5" />
<span>Elapsed: {formatTime(elapsedTime)}</span>
</div>
</div>
);
case 'processing':
return (
<div>
<div className="flex items-center gap-2 text-info mb-2">
<Loader2 className="h-4 w-4 animate-spin" />
<span className="text-sm font-medium">Converting...</span>
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-info">
<Loader2 className="h-4 w-4 animate-spin" />
<span className="text-sm font-medium">Converting...</span>
</div>
<span className="text-xs text-muted-foreground">{job.progress}%</span>
</div>
<Progress value={job.progress} showLabel={false} />
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<div className="flex items-center gap-2">
<Clock className="h-3.5 w-3.5" />
<span>Elapsed: {formatTime(elapsedTime)}</span>
</div>
{estimatedTimeRemaining && (
<div className="flex items-center gap-2">
<TrendingUp className="h-3.5 w-3.5" />
<span>~{formatTime(estimatedTimeRemaining)} remaining</span>
</div>
)}
</div>
<Progress value={job.progress} showLabel />
</div>
);
case 'completed':
const inputSize = job.inputFile.size;
const outputSize = job.result?.size || 0;
const sizeReduction = inputSize > 0 ? ((inputSize - outputSize) / inputSize) * 100 : 0;
return (
<div className="flex items-center gap-2 text-success">
<CheckCircle className="h-4 w-4" />
<span className="text-sm font-medium">Conversion complete!</span>
{job.result && (
<span className="text-xs text-muted-foreground ml-auto">
{formatFileSize(job.result.size)}
</span>
)}
<div className="space-y-3">
<div className="flex items-center gap-2 text-success">
<CheckCircle className="h-5 w-5" />
<span className="text-sm font-medium">Conversion complete!</span>
</div>
{/* File size comparison */}
<div className="bg-muted/50 rounded-lg p-3 space-y-2">
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2">
<FileCheck2 className="h-4 w-4 text-muted-foreground" />
<span className="text-muted-foreground">Input:</span>
</div>
<span className="font-medium">{formatFileSize(inputSize)}</span>
</div>
<div className="flex items-center justify-center py-1">
<ArrowRight className="h-4 w-4 text-muted-foreground" />
</div>
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2">
<FileCheck2 className="h-4 w-4 text-muted-foreground" />
<span className="text-muted-foreground">Output:</span>
</div>
<div className="flex items-center gap-2">
<span className="font-medium">{formatFileSize(outputSize)}</span>
{Math.abs(sizeReduction) > 1 && (
<span className={cn(
"text-xs px-2 py-0.5 rounded-full",
sizeReduction > 0
? "bg-success/10 text-success"
: "bg-info/10 text-info"
)}>
{sizeReduction > 0 ? '-' : '+'}{Math.abs(sizeReduction).toFixed(0)}%
</span>
)}
</div>
</div>
</div>
</div>
);
case 'error':
return (
<div className="flex items-center gap-2 text-destructive">
<XCircle className="h-4 w-4" />
<XCircle className="h-5 w-5" />
<span className="text-sm font-medium">Conversion failed</span>
</div>
);