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:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user