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';
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<Book | null>(null);
const rendition = useRef<Rendition | undefined>(undefined);
const toc = useRef<NavItem[]>([]);
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) {
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 <DocumentSkeleton />;

View file

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

View file

@ -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<number>(1);
const [currDocPage, setCurrDocPage] = useState<number>(0);
const [currDocPages, setCurrDocPages] = useState<number>();
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

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