itcloud/frontend/src/components/ViewerModal.tsx

282 lines
6.9 KiB
TypeScript
Raw Normal View History

2025-12-30 13:35:19 +01:00
import { useState, useEffect } from 'react';
import {
Dialog,
Box,
IconButton,
CircularProgress,
Typography,
} from '@mui/material';
import {
Close as CloseIcon,
NavigateBefore as PrevIcon,
NavigateNext as NextIcon,
Download as DownloadIcon,
Share as ShareIcon,
Delete as DeleteIcon,
} from '@mui/icons-material';
import type { Asset } from '../types';
import api from '../services/api';
interface ViewerModalProps {
asset: Asset | null;
assets: Asset[];
onClose: () => void;
onDelete?: (assetId: string) => void;
onShare?: (assetId: string) => void;
}
export default function ViewerModal({
asset,
assets,
onClose,
onDelete,
onShare,
}: ViewerModalProps) {
const [currentUrl, setCurrentUrl] = useState<string>('');
const [loading, setLoading] = useState(true);
const [currentIndex, setCurrentIndex] = useState(-1);
useEffect(() => {
if (asset) {
const index = assets.findIndex((a) => a.id === asset.id);
setCurrentIndex(index);
loadMedia(asset);
}
}, [asset]);
useEffect(() => {
const handleKeyPress = (e: KeyboardEvent) => {
if (!asset) return;
if (e.key === 'Escape') {
onClose();
} else if (e.key === 'ArrowLeft') {
2025-12-30 14:00:44 +01:00
if (currentIndex > 0) {
const prevAsset = assets[currentIndex - 1];
loadMedia(prevAsset);
setCurrentIndex(currentIndex - 1);
}
2025-12-30 13:35:19 +01:00
} else if (e.key === 'ArrowRight') {
2025-12-30 14:00:44 +01:00
if (currentIndex < assets.length - 1) {
const nextAsset = assets[currentIndex + 1];
loadMedia(nextAsset);
setCurrentIndex(currentIndex + 1);
}
2025-12-30 13:35:19 +01:00
}
};
window.addEventListener('keydown', handleKeyPress);
return () => window.removeEventListener('keydown', handleKeyPress);
2025-12-30 14:00:44 +01:00
}, [asset, currentIndex, assets, onClose]);
2025-12-30 13:35:19 +01:00
const loadMedia = async (asset: Asset) => {
try {
setLoading(true);
const url = await api.getDownloadUrl(asset.id, 'original');
setCurrentUrl(url);
} catch (error) {
console.error('Failed to load media:', error);
} finally {
setLoading(false);
}
};
const handlePrev = () => {
if (currentIndex > 0) {
const prevAsset = assets[currentIndex - 1];
loadMedia(prevAsset);
setCurrentIndex(currentIndex - 1);
}
};
const handleNext = () => {
if (currentIndex < assets.length - 1) {
const nextAsset = assets[currentIndex + 1];
loadMedia(nextAsset);
setCurrentIndex(currentIndex + 1);
}
};
const handleDownload = () => {
if (currentUrl && asset) {
const link = document.createElement('a');
link.href = currentUrl;
link.download = asset.original_filename;
link.click();
}
};
const handleDelete = () => {
if (asset && onDelete) {
onDelete(asset.id);
onClose();
}
};
const handleShare = () => {
if (asset && onShare) {
onShare(asset.id);
}
};
if (!asset) return null;
return (
<Dialog
open={!!asset}
onClose={onClose}
maxWidth={false}
fullWidth
PaperProps={{
sx: {
bgcolor: 'black',
m: 0,
maxWidth: '100vw',
maxHeight: '100vh',
height: '100vh',
},
}}
>
<Box
sx={{
position: 'relative',
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{/* Top bar */}
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
p: 2,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
bgcolor: 'rgba(0,0,0,0.5)',
zIndex: 1,
}}
>
<Typography variant="h6" color="white" noWrap sx={{ flex: 1, mr: 2 }}>
{asset.original_filename}
</Typography>
<Box>
<IconButton color="inherit" onClick={handleDownload} sx={{ color: 'white' }}>
<DownloadIcon />
</IconButton>
{onShare && (
<IconButton color="inherit" onClick={handleShare} sx={{ color: 'white' }}>
<ShareIcon />
</IconButton>
)}
{onDelete && (
<IconButton color="inherit" onClick={handleDelete} sx={{ color: 'white' }}>
<DeleteIcon />
</IconButton>
)}
<IconButton onClick={onClose} sx={{ color: 'white' }}>
<CloseIcon />
</IconButton>
</Box>
</Box>
{/* Navigation buttons */}
{currentIndex > 0 && (
<IconButton
onClick={handlePrev}
sx={{
position: 'absolute',
left: 16,
color: 'white',
bgcolor: 'rgba(0,0,0,0.5)',
'&:hover': { bgcolor: 'rgba(0,0,0,0.7)' },
}}
>
<PrevIcon fontSize="large" />
</IconButton>
)}
{currentIndex < assets.length - 1 && (
<IconButton
onClick={handleNext}
sx={{
position: 'absolute',
right: 16,
color: 'white',
bgcolor: 'rgba(0,0,0,0.5)',
'&:hover': { bgcolor: 'rgba(0,0,0,0.7)' },
}}
>
<NextIcon fontSize="large" />
</IconButton>
)}
{/* Content */}
{loading && (
<CircularProgress sx={{ color: 'white' }} />
)}
{!loading && asset.type === 'photo' && (
<Box
component="img"
src={currentUrl}
alt={asset.original_filename}
sx={{
maxWidth: '95%',
maxHeight: '85%',
objectFit: 'contain',
}}
/>
)}
{!loading && asset.type === 'video' && (
<Box
component="video"
src={currentUrl}
controls
autoPlay
sx={{
maxWidth: '95%',
maxHeight: '85%',
}}
/>
)}
{/* Bottom info */}
<Box
sx={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
p: 2,
bgcolor: 'rgba(0,0,0,0.5)',
color: 'white',
display: 'flex',
justifyContent: 'center',
gap: 2,
}}
>
<Typography variant="body2">
{currentIndex + 1} / {assets.length}
</Typography>
<Typography variant="body2">
{(asset.size_bytes / 1024 / 1024).toFixed(2)} MB
</Typography>
{asset.width && asset.height && (
<Typography variant="body2">
{asset.width} × {asset.height}
</Typography>
)}
</Box>
</Box>
</Dialog>
);
}