'use client'; import { RefObject, useCallback, useState, useEffect, useRef } from 'react'; import { Document, Page } from 'react-pdf'; import 'react-pdf/dist/Page/AnnotationLayer.css'; import 'react-pdf/dist/Page/TextLayer.css'; import { DocumentSkeleton } from '@/components/DocumentSkeleton'; import { useTTS } from '@/contexts/TTSContext'; import { usePDF } from '@/contexts/PDFContext'; import { useConfig } from '@/contexts/ConfigContext'; import { usePDFResize } from '@/hooks/pdf/usePDFResize'; interface PDFViewerProps { zoomLevel: number; } interface OnItemClickArgs { pageNumber?: number; dest?: unknown; } export function PDFViewer({ zoomLevel }: PDFViewerProps) { const containerRef = useRef(null); const scaleRef = useRef(1); const { containerWidth } = usePDFResize(containerRef); // Config context const { viewType } = useConfig(); // TTS context const { currentSentence, stopAndPlayFromIndex, isProcessing, skipToLocation, } = useTTS(); // PDF context const { highlightPattern, clearHighlights, handleTextClick, onDocumentLoadSuccess, currDocData, currDocPages, currDocText, currDocPage, } = usePDF(); // Add static styles once during component mount useEffect(() => { const styleElement = document.createElement('style'); styleElement.textContent = ` .react-pdf__Page__textContent span { cursor: pointer; transition: background-color 0.2s ease; } .react-pdf__Page__textContent span:hover { background-color: rgba(255, 255, 0, 0.2) !important; } `; document.head.appendChild(styleElement); return () => { styleElement.remove(); }; }, []); useEffect(() => { /* * Sets up click event listeners for text selection in the PDF. * Cleans up by removing the event listener when component unmounts. * * Dependencies: * - pdfText: Re-run when the extracted text content changes * - handleTextClick: Function from context that could change * - stopAndPlayFromIndex: Function from context that could change */ const container = containerRef.current; if (!container) return; if (!currDocText) return; const handleClick = (event: MouseEvent) => handleTextClick( event, currDocText, containerRef as RefObject, stopAndPlayFromIndex, isProcessing ); container.addEventListener('click', handleClick); return () => { container.removeEventListener('click', handleClick); }; }, [currDocText, handleTextClick, stopAndPlayFromIndex, isProcessing]); useEffect(() => { /* * Handles highlighting the current sentence being read by TTS. * Includes a small delay for smooth highlighting and cleans up on unmount. * * Dependencies: * - pdfText: Re-run when the text content changes * - currentSentence: Re-run when the TTS position changes * - highlightPattern: Function from context that could change * - clearHighlights: Function from context that could change */ if (!currDocText) return; const highlightTimeout = setTimeout(() => { if (containerRef.current) { highlightPattern(currDocText, currentSentence || '', containerRef as RefObject); } }, 200); return () => { clearTimeout(highlightTimeout); clearHighlights(); }; }, [currDocText, currentSentence, highlightPattern, clearHighlights]); // Add page dimensions state const [pageWidth, setPageWidth] = useState(595); // default A4 width const [pageHeight, setPageHeight] = useState(842); // default A4 height // Calculate which pages to show based on viewType const leftPage = viewType === 'dual' ? (currDocPage % 2 === 0 ? currDocPage - 1 : currDocPage) : currDocPage; const rightPage = viewType === 'dual' ? (currDocPage % 2 === 0 ? currDocPage : currDocPage + 1) : null; // Modify scale calculation to be more efficient const calculateScale = useCallback((width = pageWidth, height = pageHeight): number => { const margin = viewType === 'dual' ? 48 : 24; // adjust margin based on view type const containerHeight = (containerRef.current?.clientHeight ?? window.innerHeight); const targetWidth = viewType === 'dual' ? (containerWidth - margin) / 2 // divide by 2 for dual pages : containerWidth - margin; const targetHeight = containerHeight - margin; if (viewType === 'scroll') { // For scroll mode, use a more comfortable width-based scale // Use 75% of the width-based scale to make it less zoomed in const scaleByWidth = (targetWidth / width) * 0.75; return scaleByWidth * (zoomLevel / 100); } const scaleByWidth = targetWidth / width; const scaleByHeight = targetHeight / height; const baseScale = Math.min(scaleByWidth, scaleByHeight); return baseScale * (zoomLevel / 100); }, [containerWidth, zoomLevel, pageWidth, pageHeight, viewType]); // Add memoized scale to prevent unnecessary recalculations const currentScale = useCallback(() => { const newScale = calculateScale(); if (Math.abs(newScale - scaleRef.current) > 0.01) { scaleRef.current = newScale; } return scaleRef.current; }, [calculateScale]); return (
} noData={} file={currDocData} onLoadSuccess={(pdf) => { onDocumentLoadSuccess(pdf); }} onItemClick={(args: OnItemClickArgs) => { if (args?.pageNumber) { skipToLocation(args.pageNumber, true); } else if (args?.dest) { const destArray = Array.isArray(args.dest) ? args.dest : []; const pageNum = typeof destArray[0] === 'number' ? destArray[0] + 1 : undefined; if (pageNum) { skipToLocation(pageNum, true); } } }} className="flex flex-col items-center m-0 z-0" >
{viewType === 'scroll' ? ( // Scroll mode: render all pages
{currDocPages && [...Array(currDocPages)].map((_, i) => ( { setPageWidth(page.originalWidth); setPageHeight(page.originalHeight); }} /> ))}
) : ( // Single/Dual page mode
{currDocPages && leftPage > 0 && ( { setPageWidth(page.originalWidth); setPageHeight(page.originalHeight); }} /> )} {currDocPages && rightPage && rightPage <= currDocPages && viewType === 'dual' && ( { setPageWidth(page.originalWidth); setPageHeight(page.originalHeight); }} /> )}
)}
); }