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