- {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)) {