348 lines
11 KiB
TypeScript
348 lines
11 KiB
TypeScript
import { useState, useEffect } from 'react';
|
||
import { IoIosDownload } from 'react-icons/io';
|
||
import { Popup, PopupAction } from '../UiComponents/Popup/Popup';
|
||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||
import { PdfRenderer, PdfJsRenderer, LoadingRenderer, ErrorRenderer } from './renderers';
|
||
import styles from './ContentPreview.module.css';
|
||
|
||
export interface UrlContentPreviewProps {
|
||
isOpen: boolean;
|
||
onClose: () => void;
|
||
url: string;
|
||
fileName: string;
|
||
mimeType?: string;
|
||
}
|
||
|
||
export function UrlContentPreview({
|
||
isOpen,
|
||
onClose,
|
||
url,
|
||
fileName,
|
||
mimeType = 'application/pdf'
|
||
}: UrlContentPreviewProps) {
|
||
const { t } = useLanguage();
|
||
const [isLoading, setIsLoading] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [hasLoaded, setHasLoaded] = useState(false);
|
||
const [warning, setWarning] = useState<string | null>(null);
|
||
const [showPdfAnyway, setShowPdfAnyway] = useState(false);
|
||
const [usePdfJs, setUsePdfJs] = useState(false);
|
||
|
||
// Reset state when modal opens/closes
|
||
useEffect(() => {
|
||
if (isOpen && url) {
|
||
setIsLoading(true);
|
||
setError(null);
|
||
setWarning(null);
|
||
setHasLoaded(false);
|
||
setShowPdfAnyway(false);
|
||
setUsePdfJs(false); // Start with iframe
|
||
} else {
|
||
setIsLoading(false);
|
||
setError(null);
|
||
setWarning(null);
|
||
setHasLoaded(false);
|
||
setShowPdfAnyway(false);
|
||
setUsePdfJs(false);
|
||
}
|
||
}, [isOpen, url]);
|
||
|
||
const handleDownload = () => {
|
||
try {
|
||
const link = document.createElement('a');
|
||
link.href = url;
|
||
link.download = fileName;
|
||
link.target = '_blank';
|
||
link.rel = 'noopener noreferrer';
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
} catch (err) {
|
||
console.error('Failed to download file:', err);
|
||
// Fallback: open in new tab
|
||
window.open(url, '_blank', 'noopener,noreferrer');
|
||
}
|
||
};
|
||
|
||
const handlePdfLoad = () => {
|
||
setIsLoading(false);
|
||
setHasLoaded(true);
|
||
setError(null);
|
||
};
|
||
|
||
const handlePdfError = () => {
|
||
// Try PDF.js as fallback instead of showing error immediately
|
||
if (!usePdfJs) {
|
||
console.log('Iframe failed, switching to PDF.js fallback');
|
||
setUsePdfJs(true);
|
||
setIsLoading(true); // Restart loading with PDF.js
|
||
setError(null);
|
||
setWarning(null);
|
||
return;
|
||
}
|
||
// If PDF.js also fails, show error
|
||
setIsLoading(false);
|
||
setError('Failed to load PDF. This might be due to CORS restrictions. You can try downloading the file or opening it in a new tab.');
|
||
setShowPdfAnyway(true);
|
||
};
|
||
|
||
const handleOpenInNewTab = () => {
|
||
window.open(url, '_blank', 'noopener,noreferrer');
|
||
};
|
||
|
||
// Set up progressive timeout for loading (schnellerer Fallback)
|
||
useEffect(() => {
|
||
if (isOpen && isLoading && !hasLoaded) {
|
||
// Schnellerer Timeout für externe PDFs: Warning after 3s, Error after 5s
|
||
const QUICK_TIMEOUT = 5000; // 5 Sekunden
|
||
const WARNING_TIMEOUT = 3000; // 3 Sekunden Warnung
|
||
|
||
const warningTimeout = setTimeout(() => {
|
||
if (isLoading && !hasLoaded) {
|
||
setWarning('PDF lädt langsam. Sie können es auch direkt herunterladen oder in einem neuen Tab öffnen.');
|
||
// Don't set isLoading to false - let it continue
|
||
}
|
||
}, WARNING_TIMEOUT);
|
||
|
||
const errorTimeout = setTimeout(() => {
|
||
if (isLoading && !hasLoaded && !usePdfJs) {
|
||
// Try PDF.js as fallback after 5 seconds
|
||
console.log('PDF loading timeout, switching to PDF.js fallback');
|
||
setUsePdfJs(true);
|
||
setIsLoading(true); // Restart loading with PDF.js
|
||
setWarning('PDF lädt langsam. Versuche alternative Anzeigemethode...');
|
||
} else if (isLoading && !hasLoaded && usePdfJs) {
|
||
// PDF.js also failed, show error
|
||
setShowPdfAnyway(true);
|
||
setError('PDF lädt langsam. Bitte verwenden Sie den Download-Button oder öffnen Sie es in einem neuen Tab.');
|
||
setIsLoading(false);
|
||
}
|
||
}, QUICK_TIMEOUT);
|
||
|
||
return () => {
|
||
clearTimeout(warningTimeout);
|
||
clearTimeout(errorTimeout);
|
||
};
|
||
}
|
||
}, [isOpen, isLoading, hasLoaded, usePdfJs]);
|
||
|
||
// Validate URL
|
||
useEffect(() => {
|
||
if (isOpen && url) {
|
||
try {
|
||
new URL(url);
|
||
} catch (e) {
|
||
setError('Invalid URL');
|
||
setIsLoading(false);
|
||
}
|
||
}
|
||
}, [isOpen, url]);
|
||
|
||
// Create action buttons for the popup header
|
||
const actions: PopupAction[] = [
|
||
{
|
||
label: String(''),
|
||
icon: <IoIosDownload />,
|
||
onClick: handleDownload,
|
||
disabled: false,
|
||
variant: 'success' as const
|
||
}
|
||
];
|
||
|
||
const renderPreview = () => {
|
||
// Show warning but continue loading
|
||
const showWarning = warning && !error;
|
||
|
||
// For PDF files, always try to show PDF (even if there's an error)
|
||
if (mimeType === 'application/pdf' && (hasLoaded || showPdfAnyway || !error)) {
|
||
return (
|
||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||
{showWarning && (
|
||
<div style={{
|
||
padding: '0.75rem 1rem',
|
||
background: 'var(--color-warning-bg, #fef3c7)',
|
||
borderBottom: '1px solid var(--color-border, #e5e7eb)',
|
||
color: 'var(--color-warning-text, #92400e)',
|
||
fontSize: '0.875rem'
|
||
}}>
|
||
⚠️ {warning}
|
||
</div>
|
||
)}
|
||
{error && (
|
||
<div style={{
|
||
padding: '0.75rem 1rem',
|
||
background: 'var(--color-error-bg, #fee2e2)',
|
||
borderBottom: '1px solid var(--color-border, #e5e7eb)',
|
||
color: 'var(--color-error-text, #991b1b)',
|
||
fontSize: '0.875rem'
|
||
}}>
|
||
⚠️ {error}
|
||
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '0.5rem', flexWrap: 'wrap' }}>
|
||
<button
|
||
onClick={handleOpenInNewTab}
|
||
className={styles.retryButton}
|
||
style={{
|
||
background: 'var(--color-primary, #3b82f6)',
|
||
fontSize: '0.75rem',
|
||
padding: '0.5rem 1rem'
|
||
}}
|
||
>
|
||
In neuem Tab öffnen
|
||
</button>
|
||
<button
|
||
onClick={handleDownload}
|
||
className={styles.retryButton}
|
||
style={{
|
||
background: 'var(--color-success, #10b981)',
|
||
fontSize: '0.75rem',
|
||
padding: '0.5rem 1rem'
|
||
}}
|
||
>
|
||
Download
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
<div style={{ flex: 1, position: 'relative' }}>
|
||
<PdfRenderer
|
||
previewUrl={url}
|
||
fileName={fileName}
|
||
onError={handlePdfError}
|
||
onLoad={handlePdfLoad}
|
||
/>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// Show error only if we're not showing PDF anyway
|
||
if (error && !showPdfAnyway) {
|
||
return (
|
||
<div className={styles.errorContainer}>
|
||
<div className={styles.errorIcon}>⚠️</div>
|
||
<p>{error}</p>
|
||
<div style={{ display: 'flex', gap: '1rem', marginTop: '1rem', flexWrap: 'wrap' }}>
|
||
<button
|
||
onClick={() => {
|
||
setError(null);
|
||
setWarning(null);
|
||
setIsLoading(true);
|
||
setHasLoaded(false);
|
||
setShowPdfAnyway(false);
|
||
setUsePdfJs(false); // Reset to iframe
|
||
}}
|
||
className={styles.retryButton}
|
||
>
|
||
{t('common.retry', 'Retry')}
|
||
</button>
|
||
<button
|
||
onClick={handleOpenInNewTab}
|
||
className={styles.retryButton}
|
||
style={{
|
||
background: 'var(--color-primary, #3b82f6)',
|
||
fontSize: '0.875rem',
|
||
padding: '0.625rem 1.25rem',
|
||
fontWeight: '500'
|
||
}}
|
||
>
|
||
In neuem Tab öffnen
|
||
</button>
|
||
<button
|
||
onClick={handleDownload}
|
||
className={styles.retryButton}
|
||
style={{
|
||
background: 'var(--color-success, #10b981)',
|
||
fontSize: '0.875rem',
|
||
padding: '0.625rem 1.25rem',
|
||
fontWeight: '500'
|
||
}}
|
||
>
|
||
Download File
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (isLoading && !hasLoaded && !showPdfAnyway) {
|
||
return (
|
||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||
{warning && (
|
||
<div style={{
|
||
padding: '1rem',
|
||
background: 'var(--color-warning-bg, #fef3c7)',
|
||
borderBottom: '1px solid var(--color-border, #e5e7eb)',
|
||
color: 'var(--color-warning-text, #92400e)',
|
||
fontSize: '0.875rem',
|
||
marginBottom: '1rem'
|
||
}}>
|
||
⚠️ {warning}
|
||
<div style={{ display: 'flex', gap: '0.75rem', marginTop: '0.75rem', flexWrap: 'wrap' }}>
|
||
<button
|
||
onClick={handleOpenInNewTab}
|
||
className={styles.retryButton}
|
||
style={{
|
||
background: 'var(--color-primary, #3b82f6)',
|
||
fontSize: '0.875rem',
|
||
padding: '0.625rem 1.25rem',
|
||
fontWeight: '500'
|
||
}}
|
||
>
|
||
In neuem Tab öffnen
|
||
</button>
|
||
<button
|
||
onClick={handleDownload}
|
||
className={styles.retryButton}
|
||
style={{
|
||
background: 'var(--color-success, #10b981)',
|
||
fontSize: '0.875rem',
|
||
padding: '0.625rem 1.25rem',
|
||
fontWeight: '500'
|
||
}}
|
||
>
|
||
Download
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||
<LoadingRenderer />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// For other file types, show unsupported message
|
||
if (mimeType !== 'application/pdf') {
|
||
return (
|
||
<div className={styles.unsupportedContainer}>
|
||
<div className={styles.unsupportedIcon}>📄</div>
|
||
<div className={styles.fileName}>{fileName}</div>
|
||
<p>Preview not supported for this file type. Please download the file to view it.</p>
|
||
<button onClick={handleDownload} className={styles.retryButton}>
|
||
Download File
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return null;
|
||
};
|
||
|
||
return (
|
||
<Popup
|
||
isOpen={isOpen}
|
||
onClose={onClose}
|
||
title={`${t('files.preview.title', 'Content Preview')}: ${fileName}`}
|
||
size="fullscreen"
|
||
className={styles.contentPreviewPopup}
|
||
actions={actions}
|
||
>
|
||
<div className={styles.previewContainer}>
|
||
{renderPreview()}
|
||
</div>
|
||
</Popup>
|
||
);
|
||
}
|
||
|
||
export default UrlContentPreview;
|