This commit is contained in:
Richard Roberson 2025-02-08 21:43:39 -07:00
parent a3137ed88f
commit fd6a370bce
4 changed files with 104 additions and 28 deletions

View file

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef, useCallback } from 'react';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useEPUB } from '@/contexts/EPUBContext'; import { useEPUB } from '@/contexts/EPUBContext';
import { useTTS } from '@/contexts/TTSContext'; import { useTTS } from '@/contexts/TTSContext';
@ -19,28 +19,40 @@ interface EPUBViewerProps {
export function EPUBViewer({ className = '' }: EPUBViewerProps) { export function EPUBViewer({ className = '' }: EPUBViewerProps) {
const { currDocData, currDocName, currDocPage, currDocPages, extractPageText } = useEPUB(); const { currDocData, currDocName, currDocPage, currDocPages, extractPageText } = useEPUB();
const { skipToLocation } = useTTS(); const { skipToLocation, registerLocationChangeHandler } = useTTS();
const bookRef = useRef<Book | null>(null); const bookRef = useRef<Book | null>(null);
const rendition = useRef<Rendition | undefined>(undefined); const rendition = useRef<Rendition | undefined>(undefined);
const toc = useRef<NavItem[]>([]); const toc = useRef<NavItem[]>([]);
const locationRef = useRef<string | number>(currDocPage); const locationRef = useRef<string | number>(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) { if (bookRef.current && rendition.current) {
const { displayed, href } = rendition.current.location.start const { displayed, href } = rendition.current.location.start;
const chapter = toc.current.find((item) => item.href === href) const chapter = toc.current.find((item) => item.href === href);
console.log('Displayed:', displayed, 'Chapter:', chapter); console.log('Displayed:', displayed, 'Chapter:', chapter);
// Update the current location
locationRef.current = location; locationRef.current = location;
// Skip to the current location in the TTS
skipToLocation(displayed.page, displayed.total); skipToLocation(displayed.page, displayed.total);
// Extract text using the current rendition extractPageText(bookRef.current, rendition.current);
await extractPageText(bookRef.current, rendition.current);
} }
}; }, [skipToLocation, extractPageText]);
// Register the location change handler
useEffect(() => {
registerLocationChangeHandler(handleLocationChanged);
}, [registerLocationChangeHandler, handleLocationChanged]);
if (!currDocData) { if (!currDocData) {
return <DocumentSkeleton />; return <DocumentSkeleton />;

View file

@ -11,6 +11,7 @@ import {
import { indexedDBService } from '@/utils/indexedDB'; import { indexedDBService } from '@/utils/indexedDB';
import { useTTS } from '@/contexts/TTSContext'; import { useTTS } from '@/contexts/TTSContext';
import { Book, Contents, Rendition } from 'epubjs'; import { Book, Contents, Rendition } from 'epubjs';
import { createRangeCfi } from '@/utils/epub';
interface EPUBContextType { interface EPUBContextType {
currDocData: ArrayBuffer | undefined; currDocData: ArrayBuffer | undefined;
@ -86,14 +87,14 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
try { try {
console.log('Extracting EPUB text from current location'); console.log('Extracting EPUB text from current location');
const { start } = rendition.location; const { start, end } = rendition.location;
if (!start) return ''; if (!start?.cfi || !end?.cfi) return '';
const rangeCfi = createRangeCfi(start.cfi, end.cfi);
const section = await book.spine.get(start.cfi); const range = await book.getRange(rangeCfi);
if (!section) return ''; const textContent = range.toString();
const content = await section.load();
const textContent = content.textContent || '';
setTTSText(textContent); setTTSText(textContent);
setCurrDocText(textContent); setCurrDocText(textContent);

View file

@ -70,6 +70,7 @@ interface TTSContextType {
setVoiceAndRestart: (voice: string) => void; setVoiceAndRestart: (voice: string) => void;
skipToPage: (page: number) => void; skipToPage: (page: number) => void;
skipToLocation: (page: string | number, total: number) => void; skipToLocation: (page: string | number, total: number) => void;
registerLocationChangeHandler: (handler: (location: string | number) => void) => void;
} }
// Create the context // Create the context
@ -101,6 +102,14 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
const audioCache = useAudioCache(50); const audioCache = useAudioCache(50);
const { availableVoices, fetchVoices } = useVoiceManagement(openApiKey, openApiBaseUrl); 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 * State Management
*/ */
@ -111,7 +120,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
const [isProcessing, setIsProcessing] = useState(false); const [isProcessing, setIsProcessing] = useState(false);
const [speed, setSpeed] = useState(voiceSpeed); const [speed, setSpeed] = useState(voiceSpeed);
const [voice, setVoice] = useState(configVoice); const [voice, setVoice] = useState(configVoice);
const [currDocPage, setCurrDocPage] = useState<number>(1); const [currDocPage, setCurrDocPage] = useState<number>(0);
const [currDocPages, setCurrDocPages] = useState<number>(); const [currDocPages, setCurrDocPages] = useState<number>();
const [nextPageLoading, setNextPageLoading] = useState(false); const [nextPageLoading, setNextPageLoading] = useState(false);
@ -209,13 +218,17 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
* Similar to skipToPage but for EPUB locations * Similar to skipToPage but for EPUB locations
*/ */
const skipToLocation = useCallback((page: string | number, total: number) => { const skipToLocation = useCallback((page: string | number, total: number) => {
setCurrDocPages(total);
setCurrDocPage(Number(page));
abortAudio(); abortAudio();
setIsPlaying(false); setIsPlaying(false);
setNextPageLoading(true); setNextPageLoading(true);
setCurrentIndex(0); setCurrentIndex(0);
setCurrDocPage(Number(page));
setCurrDocPages(total);
// if (locationChangeHandlerRef.current) {
// setIsPlaying(true);
// }
}, [abortAudio]); }, [abortAudio]);
/** /**
@ -232,15 +245,25 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
return nextIndex; return nextIndex;
} else if (nextIndex >= sentences.length && currDocPage < currDocPages!) { } else if (nextIndex >= sentences.length && currDocPage < currDocPages!) {
console.log('Advancing to next page:', currDocPage + 1); 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; return 0;
} else if (nextIndex < 0 && currDocPage > 1) { } else if (nextIndex < 0 && currDocPage > 1) {
console.log('Advancing to previous page:', 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; return 0;
} else if (nextIndex >= sentences.length && currDocPage >= currDocPages!) { } else if (nextIndex >= sentences.length && currDocPage >= currDocPages!) {
console.log('Reached end of document'); console.log('Reached end of document');
@ -249,7 +272,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
} }
return prev; return prev;
}); });
}, [sentences, currDocPage, currDocPages, incrementPage]); }, [sentences, currDocPage, currDocPages]);
/** /**
* Moves forward one sentence in the text * Moves forward one sentence in the text
@ -557,6 +580,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
setVoiceAndRestart, setVoiceAndRestart,
skipToPage, skipToPage,
skipToLocation, // Add this line skipToLocation, // Add this line
registerLocationChangeHandler,
}), [ }), [
isPlaying, isPlaying,
isProcessing, isProcessing,
@ -576,6 +600,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
setVoiceAndRestart, setVoiceAndRestart,
skipToPage, skipToPage,
skipToLocation, // Add this line skipToLocation, // Add this line
registerLocationChangeHandler,
]); ]);
// Use media session hook // Use media session hook

38
src/utils/epub.ts Normal file
View file

@ -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})`;
}