From fd6a370bce1f92cea28cfa88426aa8920d75ce81 Mon Sep 17 00:00:00 2001 From: Richard Roberson Date: Sat, 8 Feb 2025 21:43:39 -0700 Subject: [PATCH] WIP --- src/components/EPUBViewer.tsx | 32 ++++++++++++++++-------- src/contexts/EPUBContext.tsx | 15 +++++------ src/contexts/TTSContext.tsx | 47 +++++++++++++++++++++++++++-------- src/utils/epub.ts | 38 ++++++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 28 deletions(-) create mode 100644 src/utils/epub.ts diff --git a/src/components/EPUBViewer.tsx b/src/components/EPUBViewer.tsx index c0eddc2..fe78fc8 100644 --- a/src/components/EPUBViewer.tsx +++ b/src/components/EPUBViewer.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef, useCallback } from 'react'; import dynamic from 'next/dynamic'; import { useEPUB } from '@/contexts/EPUBContext'; import { useTTS } from '@/contexts/TTSContext'; @@ -19,28 +19,40 @@ interface EPUBViewerProps { export function EPUBViewer({ className = '' }: EPUBViewerProps) { const { currDocData, currDocName, currDocPage, currDocPages, extractPageText } = useEPUB(); - const { skipToLocation } = useTTS(); + const { skipToLocation, registerLocationChangeHandler } = useTTS(); const bookRef = useRef(null); const rendition = useRef(undefined); const toc = useRef([]); const locationRef = useRef(currDocPage); - const handleLocationChanged = async (location: string | number) => { + const handleLocationChanged = useCallback((location: string | number) => { + // Handle special 'next' and 'prev' cases + if (location === 'next' && rendition.current) { + rendition.current.next(); + return; + } + if (location === 'prev' && rendition.current) { + rendition.current.prev(); + return; + } + if (bookRef.current && rendition.current) { - const { displayed, href } = rendition.current.location.start - const chapter = toc.current.find((item) => item.href === href) + const { displayed, href } = rendition.current.location.start; + const chapter = toc.current.find((item) => item.href === href); console.log('Displayed:', displayed, 'Chapter:', chapter); - // Update the current location locationRef.current = location; - // Skip to the current location in the TTS skipToLocation(displayed.page, displayed.total); - // Extract text using the current rendition - await extractPageText(bookRef.current, rendition.current); + extractPageText(bookRef.current, rendition.current); } - }; + }, [skipToLocation, extractPageText]); + + // Register the location change handler + useEffect(() => { + registerLocationChangeHandler(handleLocationChanged); + }, [registerLocationChangeHandler, handleLocationChanged]); if (!currDocData) { return ; diff --git a/src/contexts/EPUBContext.tsx b/src/contexts/EPUBContext.tsx index 232e607..df96d4b 100644 --- a/src/contexts/EPUBContext.tsx +++ b/src/contexts/EPUBContext.tsx @@ -11,6 +11,7 @@ import { import { indexedDBService } from '@/utils/indexedDB'; import { useTTS } from '@/contexts/TTSContext'; import { Book, Contents, Rendition } from 'epubjs'; +import { createRangeCfi } from '@/utils/epub'; interface EPUBContextType { currDocData: ArrayBuffer | undefined; @@ -86,14 +87,14 @@ export function EPUBProvider({ children }: { children: ReactNode }) { try { console.log('Extracting EPUB text from current location'); - const { start } = rendition.location; - if (!start) return ''; + const { start, end } = rendition.location; + if (!start?.cfi || !end?.cfi) return ''; + + + const rangeCfi = createRangeCfi(start.cfi, end.cfi); - const section = await book.spine.get(start.cfi); - if (!section) return ''; - - const content = await section.load(); - const textContent = content.textContent || ''; + const range = await book.getRange(rangeCfi); + const textContent = range.toString(); setTTSText(textContent); setCurrDocText(textContent); diff --git a/src/contexts/TTSContext.tsx b/src/contexts/TTSContext.tsx index cbeeb55..189fab8 100644 --- a/src/contexts/TTSContext.tsx +++ b/src/contexts/TTSContext.tsx @@ -70,6 +70,7 @@ interface TTSContextType { setVoiceAndRestart: (voice: string) => void; skipToPage: (page: number) => void; skipToLocation: (page: string | number, total: number) => void; + registerLocationChangeHandler: (handler: (location: string | number) => void) => void; } // Create the context @@ -101,6 +102,14 @@ export function TTSProvider({ children }: { children: React.ReactNode }) { const audioCache = useAudioCache(50); const { availableVoices, fetchVoices } = useVoiceManagement(openApiKey, openApiBaseUrl); + // Add ref for location change handler + const locationChangeHandlerRef = useRef<((location: string | number) => void) | null>(null); + + // Add method to register location change handler + const registerLocationChangeHandler = useCallback((handler: (location: string | number) => void) => { + locationChangeHandlerRef.current = handler; + }, []); + /** * State Management */ @@ -111,7 +120,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) { const [isProcessing, setIsProcessing] = useState(false); const [speed, setSpeed] = useState(voiceSpeed); const [voice, setVoice] = useState(configVoice); - const [currDocPage, setCurrDocPage] = useState(1); + const [currDocPage, setCurrDocPage] = useState(0); const [currDocPages, setCurrDocPages] = useState(); const [nextPageLoading, setNextPageLoading] = useState(false); @@ -209,13 +218,17 @@ export function TTSProvider({ children }: { children: React.ReactNode }) { * Similar to skipToPage but for EPUB locations */ const skipToLocation = useCallback((page: string | number, total: number) => { - setCurrDocPages(total); - setCurrDocPage(Number(page)); - abortAudio(); setIsPlaying(false); setNextPageLoading(true); setCurrentIndex(0); + + setCurrDocPage(Number(page)); + setCurrDocPages(total); + + // if (locationChangeHandlerRef.current) { + // setIsPlaying(true); + // } }, [abortAudio]); /** @@ -232,15 +245,25 @@ export function TTSProvider({ children }: { children: React.ReactNode }) { return nextIndex; } else if (nextIndex >= sentences.length && currDocPage < currDocPages!) { console.log('Advancing to next page:', currDocPage + 1); - - incrementPage(); - + + // Trigger page turn in React Reader + if (locationChangeHandlerRef.current) { + locationChangeHandlerRef.current('next'); + } else { + incrementPage(); + } + return 0; } else if (nextIndex < 0 && currDocPage > 1) { console.log('Advancing to previous page:', currDocPage - 1); - - incrementPage(-1); - + + // Trigger page turn in React Reader + if (locationChangeHandlerRef.current) { + locationChangeHandlerRef.current('prev'); + } else { + incrementPage(-1); + } + return 0; } else if (nextIndex >= sentences.length && currDocPage >= currDocPages!) { console.log('Reached end of document'); @@ -249,7 +272,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) { } return prev; }); - }, [sentences, currDocPage, currDocPages, incrementPage]); + }, [sentences, currDocPage, currDocPages]); /** * Moves forward one sentence in the text @@ -557,6 +580,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) { setVoiceAndRestart, skipToPage, skipToLocation, // Add this line + registerLocationChangeHandler, }), [ isPlaying, isProcessing, @@ -576,6 +600,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) { setVoiceAndRestart, skipToPage, skipToLocation, // Add this line + registerLocationChangeHandler, ]); // Use media session hook diff --git a/src/utils/epub.ts b/src/utils/epub.ts new file mode 100644 index 0000000..0644276 --- /dev/null +++ b/src/utils/epub.ts @@ -0,0 +1,38 @@ +import { EpubCFI } from 'epubjs'; + +export function findCommonBase(startCfi: string, endCfi: string): string { + // Remove 'epubcfi(' prefix and ')' suffix + const start = startCfi.replace(/^epubcfi\(|\)$/g, ''); + const end = endCfi.replace(/^epubcfi\(|\)$/g, ''); + + const startParts = start.split('/'); + const endParts = end.split('/'); + + const commonParts: string[] = []; + + for (let i = 0; i < startParts.length && i < endParts.length; i++) { + if (startParts[i] === endParts[i]) { + commonParts.push(startParts[i]); + } else { + break; + } + } + + return commonParts.join('/'); +} + +export function createRangeCfi(startCfi: string, endCfi: string): string { + // Clean the CFIs + const start = startCfi.replace(/^epubcfi\(|\)$/g, ''); + const end = endCfi.replace(/^epubcfi\(|\)$/g, ''); + + // Find the common base path + const base = findCommonBase(startCfi, endCfi); + + // Get the unique parts of start and end + const startUnique = start.substring(base.length); + const endUnique = end.substring(base.length); + + // Construct the range CFI + return `epubcfi(${base},${startUnique},${endUnique})`; +}