'use client'; import { useEffect, useRef, useCallback, useState } from 'react'; import dynamic from 'next/dynamic'; import { useEPUB } from '@/contexts/EPUBContext'; import { useTTS } from '@/contexts/TTSContext'; import { useConfig } from '@/contexts/ConfigContext'; import { DocumentSkeleton } from '@/components/documents/DocumentSkeleton'; import { useEPUBTheme, getThemeStyles } from '@/hooks/epub/useEPUBTheme'; import { useEPUBResize } from '@/hooks/epub/useEPUBResize'; import { DotsVerticalIcon, ChevronLeftIcon, ChevronRightIcon } from '@/components/icons/Icons'; const ReactReader = dynamic(() => import('react-reader').then(mod => mod.ReactReader), { ssr: false, loading: () => }); interface EPUBViewerProps { className?: string; } export function EPUBViewer({ className = '' }: EPUBViewerProps) { const [isTocOpen, setIsTocOpen] = useState(false); const { currDocData, currDocName, currDocPage, currDocPages, locationRef, handleLocationChanged, bookRef, renditionRef, tocRef, setRendition, extractPageText, highlightSegment, clearHighlights, highlightWordIndex, clearWordHighlights, walkUpcomingRenderedLocations, resolveEpubLocator, } = useEPUB(); const { registerLocationChangeHandler, registerEpubLocationWalker, registerEpubLocatorResolver, pause, currentSegment, currentSentenceAlignment, currentWordIndex } = useTTS(); const { epubTheme, epubHighlightEnabled, epubWordHighlightEnabled } = useConfig(); const { updateTheme } = useEPUBTheme(epubTheme, renditionRef.current); const containerRef = useRef(null); const { isResizing, setIsResizing, dimensions } = useEPUBResize(containerRef); const checkResize = useCallback(() => { if (isResizing && dimensions && bookRef.current?.isOpen && renditionRef.current) { pause(); // Only extract text when we have dimensions, ensuring the resize is complete extractPageText(bookRef.current, renditionRef.current, true); setIsResizing(false); return true; } else { return false; } }, [isResizing, setIsResizing, dimensions, pause, bookRef, renditionRef, extractPageText]); // Check for isResizing to pause TTS and re-extract text useEffect(() => { if (checkResize()) return; }, [checkResize]); // Register the location change handler useEffect(() => { registerLocationChangeHandler(handleLocationChanged); registerEpubLocationWalker(walkUpcomingRenderedLocations); registerEpubLocatorResolver(resolveEpubLocator); return () => { registerLocationChangeHandler(null); registerEpubLocationWalker(null); registerEpubLocatorResolver(null); }; }, [ registerLocationChangeHandler, registerEpubLocationWalker, registerEpubLocatorResolver, handleLocationChanged, walkUpcomingRenderedLocations, resolveEpubLocator, ]); // Handle highlighting useEffect(() => { if (currentSegment) { highlightSegment(currentSegment); } else { clearHighlights(); } }, [currentSegment, highlightSegment, clearHighlights]); // Word-level highlight layered on top of the block highlight useEffect(() => { if (!epubHighlightEnabled || !epubWordHighlightEnabled) { clearWordHighlights(); return; } if (currentWordIndex === null || currentWordIndex === undefined || currentWordIndex < 0) { clearWordHighlights(); return; } if (!currentSentenceAlignment) { clearWordHighlights(); return; } highlightWordIndex( currentSentenceAlignment, currentWordIndex, currentSegment ); }, [ currentWordIndex, currentSegment, currentSentenceAlignment, epubHighlightEnabled, epubWordHighlightEnabled, clearWordHighlights, highlightWordIndex ]); if (!currDocData) { return ; } return (
{currDocPages !== undefined && typeof currDocPage === 'number' && ( {currDocPage} / {currDocPages} )}
{isTocOpen && tocRef.current && tocRef.current.length > 0 && (
Skip to chapters
{tocRef.current.map((item, index) => ( ))}
)}
} key={'epub-reader'} location={locationRef.current} locationChanged={handleLocationChanged} url={currDocData} title={currDocName} tocChanged={(_toc) => (tocRef.current = _toc)} showToc={false} readerStyles={getThemeStyles(epubTheme)} getRendition={(_rendition) => { setRendition(_rendition); updateTheme(); }} />
); }