diff --git a/src/components/EPUBViewer.tsx b/src/components/EPUBViewer.tsx index 094ee57..fc30dff 100644 --- a/src/components/EPUBViewer.tsx +++ b/src/components/EPUBViewer.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useRef, useCallback, useMemo } from 'react'; +import { useEffect, useRef, useCallback } from 'react'; import { useParams } from 'next/navigation'; import dynamic from 'next/dynamic'; import { useEPUB } from '@/contexts/EPUBContext'; @@ -10,7 +10,8 @@ import { DocumentSkeleton } from '@/components/DocumentSkeleton'; import TTSPlayer from '@/components/player/TTSPlayer'; import { setLastDocumentLocation } from '@/utils/indexedDB'; import type { Rendition, Book, NavItem } from 'epubjs'; -import { useEPUBTheme, getThemeStyles } from '@/hooks/useEPUBTheme'; +import { useEPUBTheme, getThemeStyles } from '@/hooks/epub/useEPUBTheme'; +import { useEPUBResize } from '@/hooks/epub/useEPUBResize'; const ReactReader = dynamic(() => import('react-reader').then(mod => mod.ReactReader), { ssr: false, @@ -31,8 +32,13 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) { const toc = useRef([]); const locationRef = useRef(currDocPage); const { updateTheme } = useEPUBTheme(epubTheme, rendition.current); + const containerRef = useRef(null); const isEPUBSetOnce = useRef(false); + const isResizing = useRef(false); + + useEPUBResize(containerRef, isResizing); + const handleLocationChanged = useCallback((location: string | number) => { // Set the EPUB flag once the location changes if (!isEPUBSetOnce.current) { @@ -62,47 +68,23 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) { setLastDocumentLocation(id as string, location.toString()); } - skipToLocation(location); + if (isResizing.current) { + skipToLocation(location, false); + isResizing.current = false; + } else { + skipToLocation(location, true); + } locationRef.current = location; extractPageText(bookRef.current, rendition.current); - }, [id, skipToLocation, extractPageText, setIsEPUB]); - // Replace the debounced text extraction with a proper implementation using useMemo - const debouncedExtractText = useMemo(() => { - let timeout: NodeJS.Timeout; - return (book: Book, rendition: Rendition) => { - clearTimeout(timeout); - timeout = setTimeout(() => { - extractPageText(book, rendition); - }, 150); - }; - }, [extractPageText]); - - // Load the initial location and setup resize handler + // Load the initial location useEffect(() => { if (!bookRef.current || !rendition.current || isEPUBSetOnce.current) return; extractPageText(bookRef.current, rendition.current); - - // Add resize observer - const resizeObserver = new ResizeObserver(() => { - if (bookRef.current && rendition.current) { - debouncedExtractText(bookRef.current, rendition.current); - } - }); - - // Observe the container element - const container = document.querySelector('.epub-container'); - if (container) { - resizeObserver.observe(container); - } - - return () => { - resizeObserver.disconnect(); - }; - }, [extractPageText, debouncedExtractText]); + }, [extractPageText]); // Register the location change handler useEffect(() => { @@ -114,7 +96,7 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) { } return ( -
+
diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 8a45f84..d2d0914 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -1,5 +1,5 @@ import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react' -import { GithubIcon } from './icons/Icons' +import { GithubIcon } from '@/components/icons/Icons' export function Footer() { return ( diff --git a/src/components/PDFViewer.tsx b/src/components/PDFViewer.tsx index 9e995d0..c160cf2 100644 --- a/src/components/PDFViewer.tsx +++ b/src/components/PDFViewer.tsx @@ -9,16 +9,16 @@ import { useTTS } from '@/contexts/TTSContext'; import { usePDF } from '@/contexts/PDFContext'; import TTSPlayer from '@/components/player/TTSPlayer'; import { useConfig } from '@/contexts/ConfigContext'; -import { debounce } from '@/utils/pdf'; +import { usePDFResize } from '@/hooks/pdf/usePDFResize'; interface PDFViewerProps { zoomLevel: number; } export function PDFViewer({ zoomLevel }: PDFViewerProps) { - const [containerWidth, setContainerWidth] = useState(0); const containerRef = useRef(null); const scaleRef = useRef(1); + const { containerWidth } = usePDFResize(containerRef); // Config context const { viewType } = useConfig(); @@ -158,27 +158,6 @@ export function PDFViewer({ zoomLevel }: PDFViewerProps) { return scaleRef.current; }, [calculateScale]); - // Modify resize observer effect to use debouncing - useEffect(() => { - if (!containerRef.current) return; - - const debouncedResize = debounce((width: unknown) => { - setContainerWidth(Number(width)); - }, 150); // 150ms debounce - - const observer = new ResizeObserver(entries => { - const width = entries[0]?.contentRect.width; - if (width) { - debouncedResize(width); - } - }); - - observer.observe(containerRef.current); - return () => { - observer.disconnect(); - }; - }, []); - return (
void; setSpeedAndRestart: (speed: number) => void; setVoiceAndRestart: (voice: string) => void; - skipToLocation: (location: string | number) => void; + skipToLocation: (location: string | number, keepPlaying?: boolean) => void; registerLocationChangeHandler: (handler: (location: string | number) => void) => void; // EPUB-only: Handles chapter navigation setIsEPUB: (isEPUB: boolean) => void; } @@ -160,6 +160,10 @@ export function TTSProvider({ children }: { children: ReactNode }) { * @returns {Promise} Array of processed sentences */ const processTextToSentences = useCallback(async (text: string): Promise => { + if (text.length === 0) { + return []; + } + const response = await fetch('/api/nlp', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -174,14 +178,45 @@ export function TTSProvider({ children }: { children: ReactNode }) { return sentences; }, []); + /** + * Stops the current audio playback and clears the active Howl instance + */ + const abortAudio = useCallback(() => { + if (activeHowl) { + activeHowl.stop(); + setActiveHowl(null); + } + }, [activeHowl]); + + /** + * Navigates to a specific location in the document + * Works for both PDF pages and EPUB locations + * + * @param {string | number} location - The target location to navigate to + */ + const skipToLocation = useCallback((location: string | number, keepPlaying = false) => { + setNextPageLoading(true); + + // Reset state for new content + abortAudio(); + if (!keepPlaying) { + setIsPlaying(false); + } + setCurrentIndex(0); + setSentences([]); + + // Update current page/location + setCurrDocPage(location); + }, [abortAudio]); + /** * Handles blank text sections based on document type * * @param {string[]} sentences - Array of processed sentences * @returns {boolean} - True if blank section was handled */ - const handleBlankSection = useCallback((sentences: string[]): boolean => { - if (!isPlaying || !skipBlank || sentences.length > 0) { + const handleBlankSection = useCallback((text: string): boolean => { + if (!isPlaying || !skipBlank || text.length > 0) { return false; } @@ -205,7 +240,8 @@ export function TTSProvider({ children }: { children: ReactNode }) { } if (currDocPageNumber < currDocPages!) { - incrementPage(); + // Pass true to keep playing when skipping blank pages + skipToLocation(currDocPageNumber + 1, true); toast.success(`Skipping blank page ${currDocPageNumber}`, { id: `page-${currDocPageNumber}`, @@ -224,7 +260,7 @@ export function TTSProvider({ children }: { children: ReactNode }) { } return false; - }, [isPlaying, skipBlank, isEPUB, currDocPageNumber, currDocPages, incrementPage]); + }, [isPlaying, skipBlank, isEPUB, currDocPageNumber, currDocPages, skipToLocation]); /** * Sets the current text and splits it into sentences @@ -233,18 +269,21 @@ export function TTSProvider({ children }: { children: ReactNode }) { */ const setText = useCallback((text: string) => { console.log('Setting page text:', text); + + if (handleBlankSection(text)) return; processTextToSentences(text) .then(newSentences => { - if (handleBlankSection(newSentences)) { + if (newSentences.length === 0) { + console.warn('No sentences found in text'); return; } - + setSentences(newSentences); setNextPageLoading(false); }) .catch(error => { - console.error('Error processing text:', error); + console.warn('Error processing text:', error); toast.error('Failed to process text', { style: { background: 'var(--background)', @@ -255,16 +294,6 @@ export function TTSProvider({ children }: { children: ReactNode }) { }); }, [processTextToSentences, handleBlankSection]); - /** - * Stops the current audio playback and clears the active Howl instance - */ - const abortAudio = useCallback(() => { - if (activeHowl) { - activeHowl.stop(); - setActiveHowl(null); - } - }, [activeHowl]); - /** * Toggles the playback state between playing and paused */ @@ -279,25 +308,6 @@ export function TTSProvider({ children }: { children: ReactNode }) { }); }, [abortAudio]); - /** - * Navigates to a specific location in the document - * Works for both PDF pages and EPUB locations - * - * @param {string | number} location - The target location to navigate to - */ - const skipToLocation = useCallback((location: string | number) => { - setNextPageLoading(true); - - // Reset state for new content - abortAudio(); - setIsPlaying(false); - setCurrentIndex(0); - setSentences([]); - - // Update current page/location - setCurrDocPage(location); - }, [abortAudio]); - /** * Moves to the next or previous sentence * diff --git a/src/hooks/useEPUBDocuments.ts b/src/hooks/epub/useEPUBDocuments.ts similarity index 100% rename from src/hooks/useEPUBDocuments.ts rename to src/hooks/epub/useEPUBDocuments.ts diff --git a/src/hooks/epub/useEPUBResize.ts b/src/hooks/epub/useEPUBResize.ts new file mode 100644 index 0000000..59b1e0e --- /dev/null +++ b/src/hooks/epub/useEPUBResize.ts @@ -0,0 +1,52 @@ +import { useEffect, RefObject } from 'react'; + +export function useEPUBResize( + containerRef: RefObject, + isResizing: RefObject +) { + useEffect(() => { + let resizeTimeout: NodeJS.Timeout; + + const resizeObserver = new ResizeObserver((entries) => { + clearTimeout(resizeTimeout); + resizeTimeout = setTimeout(() => { + console.log('Resizing detected (debounced)', entries[0].contentRect); + isResizing.current = true; + }, 250); + }); + + const mutationObserver = new MutationObserver((mutations) => { + for (const mutation of mutations) { + if (mutation.addedNodes.length) { + const container = containerRef.current?.querySelector('.epub-container'); + if (container) { + console.log('Observer attached to epub-container'); + resizeObserver.observe(container); + mutationObserver.disconnect(); + break; + } + } + } + }); + + if (containerRef.current) { + mutationObserver.observe(containerRef.current, { + childList: true, + subtree: true + }); + + const container = containerRef.current.querySelector('.epub-container'); + if (container) { + console.log('Container already exists, attaching observer'); + resizeObserver.observe(container); + mutationObserver.disconnect(); + } + } + + return () => { + clearTimeout(resizeTimeout); + mutationObserver.disconnect(); + resizeObserver.disconnect(); + }; + }, [containerRef, isResizing]); +} \ No newline at end of file diff --git a/src/hooks/useEPUBTheme.ts b/src/hooks/epub/useEPUBTheme.ts similarity index 100% rename from src/hooks/useEPUBTheme.ts rename to src/hooks/epub/useEPUBTheme.ts diff --git a/src/hooks/usePDFDocuments.ts b/src/hooks/pdf/usePDFDocuments.ts similarity index 100% rename from src/hooks/usePDFDocuments.ts rename to src/hooks/pdf/usePDFDocuments.ts diff --git a/src/hooks/pdf/usePDFResize.ts b/src/hooks/pdf/usePDFResize.ts new file mode 100644 index 0000000..94fa291 --- /dev/null +++ b/src/hooks/pdf/usePDFResize.ts @@ -0,0 +1,35 @@ +import { RefObject, useState, useEffect } from 'react'; +import { debounce } from '@/utils/pdf'; + +interface UsePDFResizeResult { + containerWidth: number; + setContainerWidth: (width: number) => void; +} + +export function usePDFResize( + containerRef: RefObject +): UsePDFResizeResult { + const [containerWidth, setContainerWidth] = useState(0); + + useEffect(() => { + if (!containerRef.current) return; + + const debouncedResize = debounce((width: unknown) => { + setContainerWidth(Number(width)); + }, 150); + + const observer = new ResizeObserver(entries => { + const width = entries[0]?.contentRect.width; + if (width) { + debouncedResize(width); + } + }); + + observer.observe(containerRef.current); + return () => { + observer.disconnect(); + }; + }, [containerRef]); + + return { containerWidth, setContainerWidth }; +} \ No newline at end of file