Chapter by chapter auto-play

This commit is contained in:
Richard Roberson 2025-02-09 02:55:49 -07:00
parent fd6a370bce
commit 25f3375b2f
4 changed files with 73 additions and 57 deletions

View file

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useState, useEffect, useRef, useCallback } from 'react'; import { 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';
@ -18,8 +18,8 @@ interface EPUBViewerProps {
} }
export function EPUBViewer({ className = '' }: EPUBViewerProps) { export function EPUBViewer({ className = '' }: EPUBViewerProps) {
const { currDocData, currDocName, currDocPage, currDocPages, extractPageText } = useEPUB(); const { currDocData, currDocName, currDocPage, extractPageText } = useEPUB();
const { skipToLocation, registerLocationChangeHandler } = useTTS(); const { setEPUBPageInChapter, 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[]>([]);
@ -43,11 +43,11 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
console.log('Displayed:', displayed, 'Chapter:', chapter); console.log('Displayed:', displayed, 'Chapter:', chapter);
locationRef.current = location; locationRef.current = location;
skipToLocation(displayed.page, displayed.total); setEPUBPageInChapter(displayed.page, displayed.total, chapter?.label || '');
extractPageText(bookRef.current, rendition.current); extractPageText(bookRef.current, rendition.current);
} }
}, [skipToLocation, extractPageText]); }, [setEPUBPageInChapter, extractPageText]);
// Register the location change handler // Register the location change handler
useEffect(() => { useEffect(() => {

View file

@ -10,7 +10,7 @@ import {
} from 'react'; } from 'react';
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, Rendition } from 'epubjs';
import { createRangeCfi } from '@/utils/epub'; import { createRangeCfi } from '@/utils/epub';
interface EPUBContextType { interface EPUBContextType {
@ -28,7 +28,7 @@ interface EPUBContextType {
const EPUBContext = createContext<EPUBContextType | undefined>(undefined); const EPUBContext = createContext<EPUBContextType | undefined>(undefined);
export function EPUBProvider({ children }: { children: ReactNode }) { export function EPUBProvider({ children }: { children: ReactNode }) {
const { setText: setTTSText, currDocPage, currDocPages, setCurrDocPages } = useTTS(); const { setText: setTTSText, stop, currDocPage, currDocPages, setCurrDocPages } = useTTS();
// Current document state // Current document state
const [currDocData, setCurrDocData] = useState<ArrayBuffer>(); const [currDocData, setCurrDocData] = useState<ArrayBuffer>();
@ -51,8 +51,8 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
setCurrDocName(undefined); setCurrDocName(undefined);
setCurrDocText(undefined); setCurrDocText(undefined);
setCurrDocPages(undefined); setCurrDocPages(undefined);
setTTSText(''); stop();
}, [setCurrDocPages, setTTSText]); }, [setCurrDocPages, stop]);
/** /**
* Sets the current document based on its ID * Sets the current document based on its ID
@ -90,7 +90,6 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
const { start, end } = rendition.location; const { start, end } = rendition.location;
if (!start?.cfi || !end?.cfi) return ''; if (!start?.cfi || !end?.cfi) return '';
const rangeCfi = createRangeCfi(start.cfi, end.cfi); const rangeCfi = createRangeCfi(start.cfi, end.cfi);
const range = await book.getRange(rangeCfi); const range = await book.getRange(rangeCfi);

View file

@ -69,7 +69,7 @@ interface TTSContextType {
setSpeedAndRestart: (speed: number) => void; setSpeedAndRestart: (speed: number) => void;
setVoiceAndRestart: (voice: string) => void; setVoiceAndRestart: (voice: string) => void;
skipToPage: (page: number) => void; skipToPage: (page: number) => void;
skipToLocation: (page: string | number, total: number) => void; setEPUBPageInChapter: (page: string | number, total: number, chapter: string | number) => void; // Add this line
registerLocationChangeHandler: (handler: (location: string | number) => void) => void; registerLocationChangeHandler: (handler: (location: string | number) => void) => void;
} }
@ -120,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>(0); const [currDocPage, setCurrDocPage] = useState<number>(1);
const [currDocPages, setCurrDocPages] = useState<number>(); const [currDocPages, setCurrDocPages] = useState<number>();
const [nextPageLoading, setNextPageLoading] = useState(false); const [nextPageLoading, setNextPageLoading] = useState(false);
@ -217,19 +217,25 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
* Navigates to a specific location in the EPUB document * Navigates to a specific location in the EPUB document
* Similar to skipToPage but for EPUB locations * Similar to skipToPage but for EPUB locations
*/ */
const skipToLocation = useCallback((page: string | number, total: number) => { const [currChapter, setCurrChapter] = useState<string | number>('');
const setEPUBPageInChapter = useCallback((page: string | number, total: number, chapter: string | number) => {
const alreadyPlaying = isPlaying;
if (chapter !== currChapter) {
setCurrDocPages(total);
setCurrChapter(chapter);
}
abortAudio(); abortAudio();
setIsPlaying(false); setIsPlaying(false);
setNextPageLoading(true); setNextPageLoading(true);
setCurrentIndex(0); setCurrentIndex(0);
setSentences([]);
setCurrDocPage(Number(page)); setCurrDocPage(Number(page));
setCurrDocPages(total);
// if (locationChangeHandlerRef.current) { setIsPlaying(alreadyPlaying);
// setIsPlaying(true);
// } }, [abortAudio, currChapter]);
}, [abortAudio]);
/** /**
* Moves to the next or previous sentence * Moves to the next or previous sentence
@ -238,41 +244,49 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
const advance = useCallback(async (backwards = false) => { const advance = useCallback(async (backwards = false) => {
setCurrentIndex((prev) => { const nextIndex = currentIndex + (backwards ? -1 : 1);
const nextIndex = prev + (backwards ? -1 : 1);
if (nextIndex < sentences.length && nextIndex >= 0) { // Handle within current page bounds
console.log('Advancing to next sentence:', sentences[nextIndex]); if (nextIndex < sentences.length && nextIndex >= 0) {
return nextIndex; console.log('Advancing to next sentence:', sentences[nextIndex]);
} else if (nextIndex >= sentences.length && currDocPage < currDocPages!) { setCurrentIndex(nextIndex);
console.log('Advancing to next page:', currDocPage + 1); return;
}
// Trigger page turn in React Reader
if (locationChangeHandlerRef.current) { // Handle next page
locationChangeHandlerRef.current('next'); if (nextIndex >= sentences.length && currDocPage < currDocPages!) {
} else { console.log('Advancing to next page:', currDocPage + 1);
incrementPage(); setCurrentIndex(0);
}
// Trigger page turn in React Reader
return 0; if (locationChangeHandlerRef.current) {
} else if (nextIndex < 0 && currDocPage > 1) { locationChangeHandlerRef.current('next');
console.log('Advancing to previous page:', currDocPage - 1); } else {
incrementPage();
// 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');
setIsPlaying(false);
return prev;
} }
return prev; return;
}); }
}, [sentences, currDocPage, currDocPages]);
// Handle previous page
if (nextIndex < 0 && currDocPage > 1) {
console.log('Advancing to previous page:', currDocPage - 1);
setCurrentIndex(0);
// Trigger page turn in React Reader
if (locationChangeHandlerRef.current) {
locationChangeHandlerRef.current('prev');
} else {
incrementPage(-1);
}
return;
}
// Handle end of document
if (nextIndex >= sentences.length && currDocPage >= currDocPages!) {
console.log('Reached end of document');
setIsPlaying(false);
}
}, [currentIndex, incrementPage, sentences, currDocPage, currDocPages]);
/** /**
* Moves forward one sentence in the text * Moves forward one sentence in the text
@ -502,9 +516,14 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
const stop = useCallback(() => { const stop = useCallback(() => {
// Cancel any ongoing request // Cancel any ongoing request
abortAudio(); abortAudio();
locationChangeHandlerRef.current = null;
setIsPlaying(false); setIsPlaying(false);
setCurrentIndex(0); setCurrentIndex(0);
setSentences([]);
setCurrChapter('');
setCurrDocPage(1);
setCurrDocPages(undefined);
setNextPageLoading(false);
setIsProcessing(false); setIsProcessing(false);
}, [abortAudio]); }, [abortAudio]);
@ -579,7 +598,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
setSpeedAndRestart, setSpeedAndRestart,
setVoiceAndRestart, setVoiceAndRestart,
skipToPage, skipToPage,
skipToLocation, // Add this line setEPUBPageInChapter, // Add this line
registerLocationChangeHandler, registerLocationChangeHandler,
}), [ }), [
isPlaying, isPlaying,
@ -599,7 +618,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
setSpeedAndRestart, setSpeedAndRestart,
setVoiceAndRestart, setVoiceAndRestart,
skipToPage, skipToPage,
skipToLocation, // Add this line setEPUBPageInChapter, // Add this line
registerLocationChangeHandler, registerLocationChangeHandler,
]); ]);

View file

@ -1,5 +1,3 @@
import { EpubCFI } from 'epubjs';
export function findCommonBase(startCfi: string, endCfi: string): string { export function findCommonBase(startCfi: string, endCfi: string): string {
// Remove 'epubcfi(' prefix and ')' suffix // Remove 'epubcfi(' prefix and ')' suffix
const start = startCfi.replace(/^epubcfi\(|\)$/g, ''); const start = startCfi.replace(/^epubcfi\(|\)$/g, '');