295 lines
8.9 KiB
JavaScript
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; |