248 lines
6.3 KiB
TypeScript
248 lines
6.3 KiB
TypeScript
'use client';
|
||||
|
|
|
|||
|
|
import { useEffect, useRef, useState } from 'react';
|
|||
|
|
import { usePdf } from '@/app/contexts/PdfContext';
|
|||
|
|
|
|||
|
|
interface PdfViewerProps {
|
|||
|
|
fileId: string;
|
|||
|
|
fileName?: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default function PdfViewer({ fileId, fileName = 'document.pdf' }: PdfViewerProps) {
|
|||
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|||
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|||
|
|
const [totalPages, setTotalPages] = useState(0);
|
|||
|
|
const [loading, setLoading] = useState(true);
|
|||
|
|
const [error, setError] = useState<string | null>(null);
|
|||
|
|
const [fullscreen, setFullscreen] = useState(false);
|
|||
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|||
|
|
const pdfDocumentRef = useRef<any>(null);
|
|||
|
|
const { setPdfData } = usePdf();
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
let isMounted = true;
|
|||
|
|
let objectUrl: string | null = null;
|
|||
|
|
|
|||
|
|
const loadAndDisplayPdf = async () => {
|
|||
|
|
try {
|
|||
|
|
setLoading(true);
|
|||
|
|
|
|||
|
|
// 1. Получаем данные документа как ArrayBuffer (а не blob URL)
|
|||
|
|
const { requestFileAsBuffer } = await import('@/app/actions/trackingActions');
|
|||
|
|
const pdfData = await requestFileAsBuffer(fileId);
|
|||
|
|
|
|||
|
|
if (!pdfData) {
|
|||
|
|
throw new Error('Failed to load document');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setPdfData(pdfData, fileName);
|
|||
|
|
|
|||
|
|
if (!isMounted) return;
|
|||
|
|
|
|||
|
|
const pdfjs = await import('pdfjs-dist');
|
|||
|
|
|
|||
|
|
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
|
|||
|
|
'pdfjs-dist/build/pdf.worker.mjs',
|
|||
|
|
import.meta.url
|
|||
|
|
).toString();
|
|||
|
|
|
|||
|
|
const loadingTask = pdfjs.getDocument({
|
|||
|
|
data: pdfData,
|
|||
|
|
useSystemFonts: true,
|
|||
|
|
disableFontFace: false,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const pdfDocument = await loadingTask.promise;
|
|||
|
|
|
|||
|
|
if (!isMounted) return;
|
|||
|
|
|
|||
|
|
pdfDocumentRef.current = pdfDocument;
|
|||
|
|
setTotalPages(pdfDocument.numPages);
|
|||
|
|
setCurrentPage(1);
|
|||
|
|
|
|||
|
|
await renderPage(pdfDocument, 1);
|
|||
|
|
|
|||
|
|
} catch (err) {
|
|||
|
|
console.error('Error loading PDF:', err);
|
|||
|
|
if (isMounted) {
|
|||
|
|
setError('Не удалось загрузить документ');
|
|||
|
|
}
|
|||
|
|
} finally {
|
|||
|
|
if (isMounted) {
|
|||
|
|
setLoading(false);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
loadAndDisplayPdf();
|
|||
|
|
|
|||
|
|
return () => {
|
|||
|
|
isMounted = false;
|
|||
|
|
if (pdfDocumentRef.current) {
|
|||
|
|
pdfDocumentRef.current.destroy();
|
|||
|
|
}
|
|||
|
|
if (objectUrl) {
|
|||
|
|
URL.revokeObjectURL(objectUrl);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
}, [fileId, setPdfData]);
|
|||
|
|
|
|||
|
|
const renderPage = async (pdfDocument: any, pageNum: number) => {
|
|||
|
|
if (!canvasRef.current) return;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const page = await pdfDocument.getPage(pageNum);
|
|||
|
|
|
|||
|
|
const container = canvasRef.current.parentElement;
|
|||
|
|
|
|||
|
|
if (!container) return;
|
|||
|
|
|
|||
|
|
const containerWidth = container.clientWidth || 800;
|
|||
|
|
|
|||
|
|
// Если ширина контейнера 0, откладываем рендер
|
|||
|
|
if (containerWidth === 0) {
|
|||
|
|
console.log('Container width is 0, waiting...');
|
|||
|
|
setTimeout(() => renderPage(pdfDocument, pageNum), 100);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const originalViewport = page.getViewport({ scale: 1 });
|
|||
|
|
const scale = containerWidth / originalViewport.width;
|
|||
|
|
const viewport = page.getViewport({ scale: Math.min(scale, 1.5) });
|
|||
|
|
|
|||
|
|
const canvas = canvasRef.current;
|
|||
|
|
const context = canvas.getContext('2d');
|
|||
|
|
|
|||
|
|
canvas.height = viewport.height;
|
|||
|
|
canvas.width = viewport.width;
|
|||
|
|
|
|||
|
|
const renderContext = {
|
|||
|
|
canvasContext: context!,
|
|||
|
|
viewport: viewport,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
await page.render(renderContext).promise;
|
|||
|
|
} catch (err) {
|
|||
|
|
console.log('Error rendering page:', err);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (!pdfDocumentRef.current || loading) return;
|
|||
|
|
|
|||
|
|
const checkContainer = () => {
|
|||
|
|
const container = canvasRef.current?.parentElement;
|
|||
|
|
if (container && container.clientWidth > 0) {
|
|||
|
|
renderPage(pdfDocumentRef.current, currentPage);
|
|||
|
|
} else {
|
|||
|
|
setTimeout(checkContainer, 100);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
checkContainer();
|
|||
|
|
}, [pdfDocumentRef.current, loading]);
|
|||
|
|
|
|||
|
|
const goToPrevPage = async () => {
|
|||
|
|
if (pdfDocumentRef.current && currentPage > 1) {
|
|||
|
|
const newPage = currentPage - 1;
|
|||
|
|
setCurrentPage(newPage);
|
|||
|
|
await renderPage(pdfDocumentRef.current, newPage);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const goToNextPage = async () => {
|
|||
|
|
if (pdfDocumentRef.current && currentPage < totalPages) {
|
|||
|
|
const newPage = currentPage + 1;
|
|||
|
|
setCurrentPage(newPage);
|
|||
|
|
await renderPage(pdfDocumentRef.current, newPage);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const toggleFullscreen = () => {
|
|||
|
|
if (!containerRef.current) return;
|
|||
|
|
|
|||
|
|
if (!document.fullscreenElement) {
|
|||
|
|
containerRef.current.requestFullscreen();
|
|||
|
|
setFullscreen(true);
|
|||
|
|
} else {
|
|||
|
|
document.exitFullscreen();
|
|||
|
|
setFullscreen(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
const handleFullscreenChange = () => {
|
|||
|
|
setFullscreen(!!document.fullscreenElement);
|
|||
|
|
if (pdfDocumentRef.current && !document.fullscreenElement) {
|
|||
|
|
setTimeout(() => renderPage(pdfDocumentRef.current, currentPage), 100);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
|||
|
|
return () => document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
|||
|
|
}, [currentPage]);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
const handleResize = () => {
|
|||
|
|
if (pdfDocumentRef.current && !loading) {
|
|||
|
|
renderPage(pdfDocumentRef.current, currentPage);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
window.addEventListener('resize', handleResize);
|
|||
|
|
return () => window.removeEventListener('resize', handleResize);
|
|||
|
|
}, [currentPage, loading]);
|
|||
|
|
|
|||
|
|
if (loading) {
|
|||
|
|
return (
|
|||
|
|
<div className="prev-wrap">
|
|||
|
|
<div className="pdf-loading">Загрузка документа...</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (error) {
|
|||
|
|
return (
|
|||
|
|
<div className="prev-wrap">
|
|||
|
|
<div className="pdf-error">{error}</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div ref={containerRef} className="pdf-container">
|
|||
|
|
<div className="prev-wrap">
|
|||
|
|
<canvas ref={canvasRef} className="pdf-canvas" />
|
|||
|
|
</div>
|
|||
|
|
{totalPages > 0 && (
|
|||
|
|
<div className="document-controls">
|
|||
|
|
<div className="document-controls__nav">
|
|||
|
|
<button
|
|||
|
|
className="document-controls__prev"
|
|||
|
|
onClick={goToPrevPage}
|
|||
|
|
disabled={currentPage === 1}
|
|||
|
|
>
|
|||
|
|
← Пред.
|
|||
|
|
</button>
|
|||
|
|
<span className="document-controls__current-page">
|
|||
|
|
{currentPage} / {totalPages}
|
|||
|
|
</span>
|
|||
|
|
<button
|
|||
|
|
className="document-controls__next"
|
|||
|
|
onClick={goToNextPage}
|
|||
|
|
disabled={currentPage === totalPages}
|
|||
|
|
>
|
|||
|
|
След. →
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
<button
|
|||
|
|
className="document-controls__fullscreen"
|
|||
|
|
onClick={toggleFullscreen}
|
|||
|
|
>
|
|||
|
|
{fullscreen ? 'Выйти' : 'На весь экран'}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|