Files
dashfront/src/components/manifest/PdfViewerPanel.jsx
2026-10-05 01:01:03 +02:00

295 lines
8.9 KiB
JavaScript

import { PureComponent } from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import CircularProgress from '@mui/material/CircularProgress';
import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import Tooltip from '@mui/material/Tooltip';
import CloseIcon from '@mui/icons-material/Close';
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import DownloadOutlinedIcon from '@mui/icons-material/DownloadOutlined';
import VerticalAlignTopIcon from '@mui/icons-material/VerticalAlignTop';
import VerticalAlignBottomIcon from '@mui/icons-material/VerticalAlignBottom';
import ViewSidebarIcon from '@mui/icons-material/ViewSidebar';
import { enqueueSnackbar } from 'notistack';
import { PanelTile } from 'dashgui/panels';
import { delivery as deliveryApi } from '../../api';
import {
SURFACE_BASE,
SURFACE_WASH,
WHITE,
} from '../../colors';
/**
* Inline PDF viewer rendered as a panel tile, mirroring TextEditorPanel's
* "window" behaviour: it can sit above, right of, or below the article list,
* and stays sticky when docked to the right. Mount it only while a PDF
* attachment is selected; unmounting closes it.
*/
export class PdfViewerPanel extends PureComponent {
constructor(props) {
super(props);
this.state = {
url: null,
loading: false,
error: null,
};
this.handleOpenExternal = this.handleOpenExternal.bind(this);
this.handleDownload = this.handleDownload.bind(this);
}
componentDidMount() {
this.loadUrl();
}
componentDidUpdate(prevProps) {
if (this.props.attachment !== prevProps.attachment) {
this.revokeUrl();
this.loadUrl();
}
}
componentWillUnmount() {
this.revokeUrl();
}
revokeUrl() {
if (this._objectUrl) {
URL.revokeObjectURL(this._objectUrl);
this._objectUrl = null;
}
}
async loadUrl() {
const { attachment, docId } = this.props;
if (!attachment) return;
if (
(attachment.file instanceof Blob || attachment.file instanceof File) &&
attachment.file.size > 0
) {
this._objectUrl = URL.createObjectURL(attachment.file);
this.setState({ url: this._objectUrl, loading: false, error: null });
return;
}
const effectiveDocId = docId ?? attachment.manifestId;
const fileId = attachment.fileId ?? attachment.id;
if (effectiveDocId != null && fileId != null) {
this.setState({ loading: true, error: null });
try {
const blob = await deliveryApi.getFile(effectiveDocId, fileId);
this._objectUrl = URL.createObjectURL(blob);
this.setState({ url: this._objectUrl, loading: false, error: null });
return;
} catch (err) {
console.error('Fehler beim Laden der PDF-Datei:', err);
enqueueSnackbar('Fehler beim Laden der Datei', { variant: 'error' });
this.setState({ loading: false, error: 'Fehler beim Laden der Datei' });
return;
}
}
this.setState({ url: null, loading: false, error: null });
}
handleOpenExternal() {
const { url } = this.state;
if (!url) return;
window.open(url, '_blank');
}
handleDownload() {
const { attachment } = this.props;
const { url } = this.state;
if (!url) return;
const link = document.createElement('a');
link.href = url;
link.download = attachment?.name || 'Dokument.pdf';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
render() {
const {
onClose,
attachment,
wide = 3,
position = 'top',
onPositionChange,
height = '60vh',
} = this.props;
if (!attachment) return null;
const { url, loading, error } = this.state;
const filename = attachment?.name || 'Dokument.pdf';
return (
<PanelTile
wide={wide}
minHeight={320}
color={SURFACE_BASE}
sx={{ height, minHeight: 320 }}
>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
height: '100%',
minHeight: 0,
minWidth: 0,
}}
>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 1.5,
mb: 1.5,
flexShrink: 0,
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, minWidth: 0 }}>
<PictureAsPdfIcon color="error" />
<Box sx={{ minWidth: 0 }}>
<Typography variant="subtitle1" fontWeight={600} noWrap>
{filename}
</Typography>
<Typography variant="caption" color="text.secondary">
PDF-Vorschau
</Typography>
</Box>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, flexShrink: 0 }}>
<Tooltip title="Über der Artikelliste">
<IconButton
size="small"
color={position === 'top' ? 'primary' : 'default'}
aria-label="Über der Artikelliste"
onClick={() => onPositionChange?.('top')}
>
<VerticalAlignTopIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Rechts neben der Artikelliste">
<IconButton
size="small"
color={position === 'right' ? 'primary' : 'default'}
aria-label="Rechts neben der Artikelliste"
onClick={() => onPositionChange?.('right')}
>
<ViewSidebarIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Unter der Artikelliste">
<IconButton
size="small"
color={position === 'below' ? 'primary' : 'default'}
aria-label="Unter der Artikelliste"
onClick={() => onPositionChange?.('below')}
>
<VerticalAlignBottomIcon fontSize="small" />
</IconButton>
</Tooltip>
<IconButton size="small" onClick={onClose} sx={{ color: 'text.secondary' }}>
<CloseIcon fontSize="small" />
</IconButton>
</Box>
</Box>
<Box
sx={{
position: 'relative',
flex: 1,
display: 'flex',
flexDirection: 'column',
minHeight: 0,
border: '1px solid',
borderColor: 'divider',
borderRadius: 1,
bgcolor: WHITE,
overflow: 'hidden',
}}
>
{loading && (
<Box
sx={{
position: 'absolute',
inset: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
bgcolor: SURFACE_WASH,
zIndex: 2,
}}
>
<CircularProgress size={36} />
</Box>
)}
{error ? (
<Box
sx={{
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
p: 2,
}}
>
<Typography color="text.secondary">{error}</Typography>
</Box>
) : (
url && (
<Box
component="iframe"
title={filename}
src={url}
sx={{ flex: 1, width: '100%', border: 'none' }}
/>
)
)}
</Box>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
gap: 1,
mt: 1.5,
flexShrink: 0,
}}
>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button
size="small"
variant="outlined"
startIcon={<OpenInNewIcon />}
onClick={this.handleOpenExternal}
disabled={loading || !url}
>
In neuem Tab
</Button>
<Button
size="small"
variant="outlined"
startIcon={<DownloadOutlinedIcon />}
onClick={this.handleDownload}
disabled={loading || !url}
>
Download
</Button>
</Box>
<Button size="small" onClick={onClose}>
Schließen
</Button>
</Box>
</Box>
</PanelTile>
);
}
}
export default PdfViewerPanel;