diff --git a/src/components/HTMLViewer.tsx b/src/components/HTMLViewer.tsx index 1f0cbc6..69b1e76 100644 --- a/src/components/HTMLViewer.tsx +++ b/src/components/HTMLViewer.tsx @@ -11,7 +11,16 @@ interface HTMLViewerProps { } export function HTMLViewer({ className = '' }: HTMLViewerProps) { - const { currDocData, currDocName } = useHTML(); + const { + currDocData, + currDocName, + chapters, + currentChapterIndex, + totalChapters, + goToNextChapter, + goToPreviousChapter, + goToChapter, + } = useHTML(); const containerRef = useRef(null); if (!currDocData) { @@ -19,21 +28,61 @@ export function HTMLViewer({ className = '' }: HTMLViewerProps) { } // Check if the file is a txt file - const isTxtFile = currDocName?.toLowerCase().endsWith('.txt'); + const isTxtFile = currDocName?.toLowerCase().endsWith('.txt'); + const hasChapters = totalChapters > 1; + const currentChapter = hasChapters ? chapters[currentChapterIndex] : null; - return ( -
-
-
- {isTxtFile ? ( - currDocData - ) : ( - - {currDocData} - - )} + return ( +
+ {/* Chapter navigation header */} + {hasChapters && ( +
+ + +
+ + {currentChapter?.title || `Chapter ${currentChapterIndex + 1}`} + + + ({currentChapterIndex + 1} of {totalChapters}) +
+ + +
+ )} + +
+
+ {isTxtFile ? ( + currDocData + ) : ( + + {currDocData} + + )}
+
); } diff --git a/src/contexts/HTMLContext.tsx b/src/contexts/HTMLContext.tsx index 66fd4df..0e023f1 100644 --- a/src/contexts/HTMLContext.tsx +++ b/src/contexts/HTMLContext.tsx @@ -10,6 +10,7 @@ import { } from 'react'; import { getHtmlDocument } from '@/lib/dexie'; import { useTTS } from '@/contexts/TTSContext'; +import { detectChapters, type Chapter } from '@/lib/chapterDetection'; interface HTMLContextType { currDocData: string | undefined; @@ -17,6 +18,16 @@ interface HTMLContextType { currDocText: string | undefined; setCurrentDocument: (id: string) => Promise; clearCurrDoc: () => void; + + // Chapter navigation + chapters: Chapter[]; + currentChapterIndex: number; + totalChapters: number; + goToNextChapter: () => void; + goToPreviousChapter: () => void; + goToChapter: (index: number) => void; + + // Audiobook generation createFullAudioBook: ( onProgress: (progress: number) => void, signal: AbortSignal, @@ -41,13 +52,16 @@ const HTMLContext = createContext(undefined); * @param {ReactNode} props.children - Child components to be wrapped by the provider */ export function HTMLProvider({ children }: { children: ReactNode }) { - const { setText: setTTSText, stop } = useTTS(); + const { setText: setTTSText, stop, registerLocationChangeHandler } = useTTS(); // Current document state const [currDocData, setCurrDocData] = useState(); const [currDocName, setCurrDocName] = useState(); const [currDocText, setCurrDocText] = useState(); + // Chapter state + const [chapters, setChapters] = useState([]); + const [currentChapterIndex, setCurrentChapterIndex] = useState(0); /** * Clears all current document state and stops any active TTS @@ -56,11 +70,14 @@ export function HTMLProvider({ children }: { children: ReactNode }) { setCurrDocData(undefined); setCurrDocName(undefined); setCurrDocText(undefined); + setChapters([]); + setCurrentChapterIndex(0); stop(); }, [stop]); /** * Sets the current document based on its ID + * Automatically detects chapters and displays only the first chapter * @param {string} id - The unique identifier of the document * @throws {Error} When document data is empty or retrieval fails */ @@ -69,9 +86,28 @@ export function HTMLProvider({ children }: { children: ReactNode }) { const doc = await getHtmlDocument(id); if (doc) { setCurrDocName(doc.name); - setCurrDocData(doc.data); - setCurrDocText(doc.data); // Use the same text for TTS - setTTSText(doc.data); + + // Detect chapters in the document (splits large files) + const detectedChapters = detectChapters(doc.data, 50000); // 50KB max per chapter + + if (detectedChapters.length > 1) { + // Document has been split into chapters + console.log(`Document split into ${detectedChapters.length} chapters`); + setChapters(detectedChapters); + setCurrentChapterIndex(0); + + // Display only the first chapter + const firstChapter = detectedChapters[0]; + setCurrDocData(firstChapter.text); + setCurrDocText(firstChapter.text); + setTTSText(firstChapter.text); + } else { + // Small document, no chapters needed + setChapters([]); + setCurrDocData(doc.data); + setCurrDocText(doc.data); + setTTSText(doc.data); + } } else { console.error('Document not found in IndexedDB'); } @@ -115,12 +151,90 @@ export function HTMLProvider({ children }: { children: ReactNode }) { console.log('regenerateChapter called:', chapterIndex, bookId, format); }, []); + /** + * Navigate to a specific chapter + */ + const goToChapter = useCallback((index: number) => { + if (chapters.length === 0) return; + + const safeIndex = Math.max(0, Math.min(index, chapters.length - 1)); + setCurrentChapterIndex(safeIndex); + + const chapter = chapters[safeIndex]; + setCurrDocData(chapter.text); + setCurrDocText(chapter.text); + setTTSText(chapter.text, true); // Pause TTS when changing chapters + }, [chapters, setTTSText]); + + /** + * Navigate to the next chapter + */ + const goToNextChapter = useCallback(() => { + if (currentChapterIndex < chapters.length - 1) { + goToChapter(currentChapterIndex + 1); + } + }, [currentChapterIndex, chapters.length, goToChapter]); + + /** + * Navigate to the previous chapter + */ + const goToPreviousChapter = useCallback(() => { + if (currentChapterIndex > 0) { + goToChapter(currentChapterIndex - 1); + } + }, [currentChapterIndex, goToChapter]); + + /** + * Register handler for TTS auto-advance through chapters + * When TTS reaches the end of current chapter, automatically load next chapter + */ + const handleLocationChange = useCallback((location: string | number) => { + if (location === 'next') { + // TTS wants to advance to next chapter + if (currentChapterIndex < chapters.length - 1) { + const nextIndex = currentChapterIndex + 1; + setCurrentChapterIndex(nextIndex); + + const nextChapter = chapters[nextIndex]; + setCurrDocData(nextChapter.text); + setCurrDocText(nextChapter.text); + setTTSText(nextChapter.text); // Continue playing + } + } else if (location === 'prev') { + // TTS wants to go to previous chapter + if (currentChapterIndex > 0) { + const prevIndex = currentChapterIndex - 1; + setCurrentChapterIndex(prevIndex); + + const prevChapter = chapters[prevIndex]; + setCurrDocData(prevChapter.text); + setCurrDocText(prevChapter.text); + setTTSText(prevChapter.text); + } + } + }, [currentChapterIndex, chapters, setTTSText]); + + // Register the handler with TTS context when chapters exist + useMemo(() => { + if (chapters.length > 1) { + registerLocationChangeHandler(handleLocationChange); + } + }, [chapters.length, registerLocationChangeHandler, handleLocationChange]); + + const totalChapters = chapters.length; + const contextValue = useMemo(() => ({ currDocData, currDocName, currDocText, setCurrentDocument, clearCurrDoc, + chapters, + currentChapterIndex, + totalChapters, + goToNextChapter, + goToPreviousChapter, + goToChapter, createFullAudioBook, regenerateChapter, }), [ @@ -129,6 +243,12 @@ export function HTMLProvider({ children }: { children: ReactNode }) { currDocText, setCurrentDocument, clearCurrDoc, + chapters, + currentChapterIndex, + totalChapters, + goToNextChapter, + goToPreviousChapter, + goToChapter, createFullAudioBook, regenerateChapter, ]); diff --git a/src/contexts/TTSContext.tsx b/src/contexts/TTSContext.tsx index 8ea3e6e..b553bd3 100644 --- a/src/contexts/TTSContext.tsx +++ b/src/contexts/TTSContext.tsx @@ -450,7 +450,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement /** * Moves to the next or previous sentence - * + * * @param {boolean} [backwards=false] - Whether to move backwards */ const advance = useCallback(async (backwards = false) => { @@ -462,14 +462,15 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement return; } - // For EPUB documents, always try to advance to next/prev section - if (isEPUB && locationChangeHandlerRef.current) { + // For documents with registered location handlers (EPUB, HTML with chapters), + // always try to advance to next/prev section + if (locationChangeHandlerRef.current) { locationChangeHandlerRef.current(nextIndex >= sentences.length ? 'next' : 'prev'); return; } - // For PDFs and other documents, check page bounds - if (!isEPUB) { + // For PDFs and other documents without handlers, check page bounds + if (!isEPUB && !locationChangeHandlerRef.current) { // Handle next/previous page transitions if ((nextIndex >= sentences.length && currDocPageNumber < currDocPages!) || (nextIndex < 0 && currDocPageNumber > 1)) {