Chapter by chapter auto-play
This commit is contained in:
parent
fd6a370bce
commit
25f3375b2f
4 changed files with 73 additions and 57 deletions
|
|
@ -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(() => {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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, '');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue