WIP
This commit is contained in:
parent
a3137ed88f
commit
fd6a370bce
4 changed files with 104 additions and 28 deletions
|
|
@ -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 />;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
38
src/utils/epub.ts
Normal 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})`;
|
||||
}
|
||||
Loading…
Reference in a new issue