194 lines
4.9 KiB
TypeScript
194 lines
4.9 KiB
TypeScript
import { useState, useEffect } from 'react';
|
||
import {
|
||
Card,
|
||
CardMedia,
|
||
CardActionArea,
|
||
Box,
|
||
IconButton,
|
||
CircularProgress,
|
||
Typography,
|
||
Checkbox,
|
||
} from '@mui/material';
|
||
import {
|
||
PlayCircleOutline as VideoIcon,
|
||
CheckCircle as CheckedIcon,
|
||
} from '@mui/icons-material';
|
||
import type { Asset } from '../types';
|
||
import api from '../services/api';
|
||
|
||
interface MediaCardProps {
|
||
asset: Asset;
|
||
selected?: boolean;
|
||
onSelect?: (assetId: string, selected: boolean) => void;
|
||
onClick?: () => void;
|
||
}
|
||
|
||
export default function MediaCard({ asset, selected, onSelect, onClick }: MediaCardProps) {
|
||
const [thumbnailUrl, setThumbnailUrl] = useState<string>('');
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState(false);
|
||
|
||
useEffect(() => {
|
||
loadThumbnail();
|
||
}, [asset.id]);
|
||
|
||
const loadThumbnail = async () => {
|
||
try {
|
||
setLoading(true);
|
||
setError(false);
|
||
// Try to get thumbnail first, fallback to original for photos
|
||
const url = asset.storage_key_thumb
|
||
? await api.getDownloadUrl(asset.id, 'thumb')
|
||
: asset.type === 'photo'
|
||
? await api.getDownloadUrl(asset.id, 'original')
|
||
: '';
|
||
setThumbnailUrl(url);
|
||
} catch (err) {
|
||
console.error('Failed to load thumbnail:', err);
|
||
setError(true);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const formatFileSize = (bytes: number): string => {
|
||
if (bytes < 1024) return bytes + ' B';
|
||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||
if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||
return (bytes / (1024 * 1024 * 1024)).toFixed(1) + ' GB';
|
||
};
|
||
|
||
const formatDate = (dateString: string): string => {
|
||
const date = new Date(dateString);
|
||
return date.toLocaleDateString('ru-RU', {
|
||
day: 'numeric',
|
||
month: 'short',
|
||
year: 'numeric',
|
||
});
|
||
};
|
||
|
||
const handleSelect = (e: React.MouseEvent) => {
|
||
e.stopPropagation();
|
||
if (onSelect) {
|
||
onSelect(asset.id, !selected);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<Card
|
||
sx={{
|
||
position: 'relative',
|
||
aspectRatio: '1',
|
||
borderRadius: 2,
|
||
overflow: 'hidden',
|
||
transition: 'transform 0.2s, box-shadow 0.2s',
|
||
'&:hover': {
|
||
transform: 'translateY(-4px)',
|
||
boxShadow: 4,
|
||
},
|
||
}}
|
||
>
|
||
<CardActionArea onClick={onClick} sx={{ height: '100%' }}>
|
||
{loading && (
|
||
<Box
|
||
sx={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
height: '100%',
|
||
bgcolor: 'grey.200',
|
||
}}
|
||
>
|
||
<CircularProgress />
|
||
</Box>
|
||
)}
|
||
|
||
{error && (
|
||
<Box
|
||
sx={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
height: '100%',
|
||
bgcolor: 'grey.300',
|
||
}}
|
||
>
|
||
<Typography color="error">Ошибка загрузки</Typography>
|
||
</Box>
|
||
)}
|
||
|
||
{!loading && !error && thumbnailUrl && (
|
||
<CardMedia
|
||
component="img"
|
||
image={thumbnailUrl}
|
||
alt={asset.original_filename}
|
||
sx={{
|
||
width: '100%',
|
||
height: '100%',
|
||
objectFit: 'cover',
|
||
}}
|
||
/>
|
||
)}
|
||
|
||
{asset.type === 'video' && (
|
||
<Box
|
||
sx={{
|
||
position: 'absolute',
|
||
top: 8,
|
||
right: 8,
|
||
color: 'white',
|
||
bgcolor: 'rgba(0,0,0,0.5)',
|
||
borderRadius: '50%',
|
||
}}
|
||
>
|
||
<VideoIcon fontSize="large" />
|
||
</Box>
|
||
)}
|
||
|
||
<Box
|
||
sx={{
|
||
position: 'absolute',
|
||
bottom: 0,
|
||
left: 0,
|
||
right: 0,
|
||
bgcolor: 'rgba(0,0,0,0.6)',
|
||
color: 'white',
|
||
p: 1,
|
||
}}
|
||
>
|
||
<Typography variant="caption" display="block" noWrap>
|
||
{asset.original_filename}
|
||
</Typography>
|
||
<Typography variant="caption" display="block">
|
||
{formatFileSize(asset.size_bytes)} • {formatDate(asset.created_at)}
|
||
</Typography>
|
||
</Box>
|
||
|
||
{onSelect && (
|
||
<Checkbox
|
||
checked={selected}
|
||
onClick={handleSelect}
|
||
icon={
|
||
<Box
|
||
sx={{
|
||
width: 24,
|
||
height: 24,
|
||
borderRadius: '50%',
|
||
border: '2px solid white',
|
||
bgcolor: 'rgba(0,0,0,0.3)',
|
||
}}
|
||
/>
|
||
}
|
||
checkedIcon={<CheckedIcon sx={{ color: 'primary.main' }} />}
|
||
sx={{
|
||
position: 'absolute',
|
||
top: 8,
|
||
left: 8,
|
||
}}
|
||
/>
|
||
)}
|
||
</CardActionArea>
|
||
</Card>
|
||
);
|
||
}
|