Refactors + epub location fixes

This commit is contained in:
Richard Roberson 2025-02-18 02:31:30 -07:00
parent 9ae6dd9e82
commit 0ce14f3aec
9 changed files with 239 additions and 205 deletions

View file

@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import nlp from 'compromise';
const MAX_BLOCK_LENGTH = 350;
const MAX_BLOCK_LENGTH = 300;
const preprocessSentenceForAudio = (text: string): string => {
return text

View file

@ -1,6 +1,6 @@
'use client';
import { useEffect, useRef, useCallback, useState, useMemo } from 'react';
import { useEffect, useRef, useCallback, useMemo } from 'react';
import { useParams } from 'next/navigation';
import dynamic from 'next/dynamic';
import { useEPUB } from '@/contexts/EPUBContext';
@ -24,18 +24,29 @@ interface EPUBViewerProps {
export function EPUBViewer({ className = '' }: EPUBViewerProps) {
const { id } = useParams();
const { currDocData, currDocName, currDocPage, extractPageText } = useEPUB();
const { setEPUBPageInChapter, registerLocationChangeHandler } = useTTS();
const { skipToLocation, registerLocationChangeHandler, setIsEPUB } = useTTS();
const { epubTheme } = useConfig();
const bookRef = useRef<Book | null>(null);
const rendition = useRef<Rendition | undefined>(undefined);
const toc = useRef<NavItem[]>([]);
const locationRef = useRef<string | number>(currDocPage);
const [initialPrevLocLoad, setInitialPrevLocLoad] = useState(false);
const { updateTheme } = useEPUBTheme(epubTheme, rendition.current);
const handleLocationChanged = useCallback((location: string | number, initial = false) => {
const isEPUBSetOnce = useRef(false);
const handleLocationChanged = useCallback((location: string | number) => {
// Set the EPUB flag once the location changes
if (!isEPUBSetOnce.current) {
setIsEPUB(true);
isEPUBSetOnce.current = true;
rendition.current?.display(location.toString());
return;
}
if (!bookRef.current?.isOpen || !rendition.current) return;
// Handle special 'next' and 'prev' cases, which
// Handle special 'next' and 'prev' cases
if (location === 'next' && rendition.current) {
rendition.current.next();
return;
@ -45,33 +56,18 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
return;
}
const { displayed, href } = rendition.current.location.start;
const chapter = toc.current.find((item) => item.href === href);
console.log('Displayed:', displayed, 'Chapter:', chapter);
if (locationRef.current !== 1) {
// Save the location to IndexedDB
if (id) {
console.log('Saving location:', location);
setLastDocumentLocation(id as string, location.toString());
}
// Save the location to IndexedDB if not initial
if (id && locationRef.current !== 1) {
console.log('Saving location:', location);
setLastDocumentLocation(id as string, location.toString());
}
setEPUBPageInChapter(displayed.page, displayed.total, chapter?.label || '');
skipToLocation(location);
// Add a small delay for initial load to ensure rendition is ready
if (initial) {
rendition.current.display(location.toString()).then(() => {
setInitialPrevLocLoad(true);
});
} else {
locationRef.current = location;
extractPageText(bookRef.current, rendition.current);
}
locationRef.current = location;
extractPageText(bookRef.current, rendition.current);
}, [id, setEPUBPageInChapter, extractPageText]);
}, [id, skipToLocation, extractPageText, setIsEPUB]);
// Replace the debounced text extraction with a proper implementation using useMemo
const debouncedExtractText = useMemo(() => {
@ -86,27 +82,27 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
// Load the initial location and setup resize handler
useEffect(() => {
if (bookRef.current && rendition.current) {
extractPageText(bookRef.current, rendition.current);
if (!bookRef.current || !rendition.current || isEPUBSetOnce.current) return;
// Add resize observer
const resizeObserver = new ResizeObserver(() => {
if (bookRef.current && rendition.current) {
debouncedExtractText(bookRef.current, rendition.current);
}
});
extractPageText(bookRef.current, rendition.current);
// Observe the container element
const container = document.querySelector('.epub-container');
if (container) {
resizeObserver.observe(container);
// Add resize observer
const resizeObserver = new ResizeObserver(() => {
if (bookRef.current && rendition.current) {
debouncedExtractText(bookRef.current, rendition.current);
}
});
return () => {
resizeObserver.disconnect();
};
// Observe the container element
const container = document.querySelector('.epub-container');
if (container) {
resizeObserver.observe(container);
}
}, [extractPageText, debouncedExtractText, initialPrevLocLoad]);
return () => {
resizeObserver.disconnect();
};
}, [extractPageText, debouncedExtractText]);
// Register the location change handler
useEffect(() => {

View file

@ -27,7 +27,7 @@ export function PDFViewer({ zoomLevel }: PDFViewerProps) {
const {
currentSentence,
stopAndPlayFromIndex,
isProcessing
isProcessing,
} = useTTS();
// PDF context

View file

@ -2,16 +2,16 @@
import { Button } from '@headlessui/react';
export const Navigator = ({ currentPage, numPages, skipToPage }: {
export const Navigator = ({ currentPage, numPages, skipToLocation }: {
currentPage: number;
numPages: number | undefined;
skipToPage: (page: number) => void;
skipToLocation: (location: string | number) => void;
}) => {
return (
<div className="flex items-center space-x-1">
{/* Page back */}
<Button
onClick={() => skipToPage(currentPage - 1)}
onClick={() => skipToLocation(currentPage - 1)}
disabled={currentPage <= 1}
className="relative p-2 rounded-full text-foreground hover:bg-offbase data-[hover]:bg-offbase data-[active]:bg-offbase/80 transition-colors duration-200 focus:outline-none disabled:opacity-50"
aria-label="Previous page"
@ -30,7 +30,7 @@ export const Navigator = ({ currentPage, numPages, skipToPage }: {
{/* Page forward */}
<Button
onClick={() => skipToPage(currentPage + 1)}
onClick={() => skipToLocation(currentPage + 1)}
disabled={currentPage >= (numPages || 1)}
className="relative p-2 rounded-full text-foreground hover:bg-offbase data-[hover]:bg-offbase data-[active]:bg-offbase/80 transition-colors duration-200 focus:outline-none disabled:opacity-50"
aria-label="Next page"

View file

@ -26,7 +26,7 @@ export default function TTSPlayer({ currentPage, numPages }: {
setSpeedAndRestart,
setVoiceAndRestart,
availableVoices,
skipToPage,
skipToLocation,
} = useTTS();
return (
@ -36,8 +36,13 @@ export default function TTSPlayer({ currentPage, numPages }: {
<SpeedControl setSpeedAndRestart={setSpeedAndRestart} />
{/* Page Navigation */}
{currentPage && numPages
&& <Navigator currentPage={currentPage} numPages={numPages} skipToPage={skipToPage} />}
{currentPage && numPages && (
<Navigator
currentPage={currentPage}
numPages={numPages}
skipToLocation={skipToLocation}
/>
)}
{/* Playback Controls */}
<Button

View file

@ -3,22 +3,10 @@
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
import { getItem, indexedDBService, setItem } from '@/utils/indexedDB';
/** Represents the possible view types for document display */
export type ViewType = 'single' | 'dual' | 'scroll';
interface ConfigContextType {
apiKey: string;
baseUrl: string;
viewType: ViewType;
voiceSpeed: number;
voice: string;
skipBlank: boolean;
epubTheme: boolean; // Add this line
updateConfig: (newConfig: Partial<{ apiKey: string; baseUrl: string; viewType: ViewType }>) => Promise<void>;
updateConfigKey: <K extends keyof ConfigValues>(key: K, value: ConfigValues[K]) => Promise<void>;
isLoading: boolean;
isDBReady: boolean;
}
// Add this type to help with type safety
/** Configuration values for the application */
type ConfigValues = {
apiKey: string;
baseUrl: string;
@ -26,11 +14,32 @@ type ConfigValues = {
voiceSpeed: number;
voice: string;
skipBlank: boolean;
epubTheme: boolean; // Add this line
epubTheme: boolean;
};
/** Interface defining the configuration context shape and functionality */
interface ConfigContextType {
apiKey: string;
baseUrl: string;
viewType: ViewType;
voiceSpeed: number;
voice: string;
skipBlank: boolean;
epubTheme: boolean;
updateConfig: (newConfig: Partial<{ apiKey: string; baseUrl: string; viewType: ViewType }>) => Promise<void>;
updateConfigKey: <K extends keyof ConfigValues>(key: K, value: ConfigValues[K]) => Promise<void>;
isLoading: boolean;
isDBReady: boolean;
}
const ConfigContext = createContext<ConfigContextType | undefined>(undefined);
/**
* Provider component for application configuration
* Manages global configuration state and persistence
* @param {Object} props - Component props
* @param {ReactNode} props.children - Child components to be wrapped by the provider
*/
export function ConfigProvider({ children }: { children: ReactNode }) {
// Config state
const [apiKey, setApiKey] = useState<string>('');
@ -108,6 +117,10 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
initializeDB();
}, []);
/**
* Updates multiple configuration values simultaneously
* @param {Partial<{apiKey: string; baseUrl: string}>} newConfig - Object containing new config values
*/
const updateConfig = async (newConfig: Partial<{ apiKey: string; baseUrl: string }>) => {
try {
if (newConfig.apiKey !== undefined) {
@ -124,6 +137,11 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
}
};
/**
* Updates a single configuration value by key
* @param {K} key - The configuration key to update
* @param {ConfigValues[K]} value - The new value for the configuration
*/
const updateConfigKey = async <K extends keyof ConfigValues>(key: K, value: ConfigValues[K]) => {
try {
await setItem(key, value.toString());
@ -175,6 +193,11 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
);
}
/**
* Custom hook to consume the configuration context
* @returns {ConfigContextType} The configuration context value
* @throws {Error} When used outside of ConfigProvider
*/
export function useConfig() {
const context = useContext(ConfigContext);
if (context === undefined) {

View file

@ -26,6 +26,12 @@ interface EPUBContextType {
const EPUBContext = createContext<EPUBContextType | undefined>(undefined);
/**
* Provider component for EPUB functionality
* Manages the state and operations for EPUB document handling
* @param {Object} props - Component props
* @param {ReactNode} props.children - Child components to be wrapped by the provider
*/
export function EPUBProvider({ children }: { children: ReactNode }) {
const { setText: setTTSText, currDocPage, currDocPages, setCurrDocPages, stop } = useTTS();
@ -35,7 +41,7 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
const [currDocText, setCurrDocText] = useState<string>();
/**
* Clears the current document state
* Clears all current document state and stops any active TTS
*/
const clearCurrDoc = useCallback(() => {
setCurrDocData(undefined);
@ -46,7 +52,9 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
}, [setCurrDocPages, stop]);
/**
* Sets the current document based on its ID
* Sets the current document based on its ID by fetching from IndexedDB
* @param {string} id - The unique identifier of the document
* @throws {Error} When document data is empty or retrieval fails
*/
const setCurrentDocument = useCallback(async (id: string): Promise<void> => {
try {
@ -73,6 +81,9 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
/**
* Extracts text content from the current EPUB page/location
* @param {Book} book - The EPUB.js Book instance
* @param {Rendition} rendition - The EPUB.js Rendition instance
* @returns {Promise<string>} The extracted text content
*/
const extractPageText = useCallback(async (book: Book, rendition: Rendition): Promise<string> => {
try {
@ -127,7 +138,8 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
/**
* Custom hook to consume the EPUB context
* Ensures the context is used within a provider
* @returns {EPUBContextType} The EPUB context value
* @throws {Error} When used outside of EPUBProvider
*/
export function useEPUB() {
const context = useContext(EPUBContext);

View file

@ -71,6 +71,9 @@ const PDFContext = createContext<PDFContextType | undefined>(undefined);
*
* Main provider component that manages PDF state and functionality.
* Handles document loading, text processing, and integration with TTS.
*
* @param {Object} props - Component props
* @param {ReactNode} props.children - Child components to be wrapped by the provider
*/
export function PDFProvider({ children }: { children: ReactNode }) {
const { setText: setTTSText, stop, currDocPageNumber: currDocPage, currDocPages, setCurrDocPages } = useTTS();
@ -82,9 +85,9 @@ export function PDFProvider({ children }: { children: ReactNode }) {
const [pdfDocument, setPdfDocument] = useState<PDFDocumentProxy>();
/**
* Handles successful document load
* Handles successful PDF document load
*
* @param {Object} param0 - Object containing numPages
* @param {PDFDocumentProxy} pdf - The loaded PDF document proxy object
*/
const onDocumentLoadSuccess = useCallback((pdf: PDFDocumentProxy) => {
console.log('Document loaded:', pdf.numPages);
@ -94,6 +97,9 @@ export function PDFProvider({ children }: { children: ReactNode }) {
/**
* Loads and processes text from the current document page
* Extracts text from the PDF and updates both document text and TTS text states
*
* @returns {Promise<void>}
*/
const loadCurrDocText = useCallback(async () => {
try {
@ -108,7 +114,8 @@ export function PDFProvider({ children }: { children: ReactNode }) {
}, [pdfDocument, currDocPage, setTTSText]);
/**
* Updates the current document text when the page changes
* Effect hook to update document text when the page changes
* Triggers text extraction and processing when either the document URL or page changes
*/
useEffect(() => {
if (currDocURL) {
@ -118,8 +125,10 @@ export function PDFProvider({ children }: { children: ReactNode }) {
/**
* Sets the current document based on its ID
* Retrieves document from IndexedDB and converts it to a viewable URL
*
* @param {string} id - The ID of the document to set
* @param {string} id - The unique identifier of the document to set
* @returns {Promise<void>}
*/
const setCurrentDocument = useCallback(async (id: string): Promise<void> => {
try {
@ -136,6 +145,7 @@ export function PDFProvider({ children }: { children: ReactNode }) {
/**
* Clears the current document state
* Resets all document-related states and stops any ongoing TTS playback
*/
const clearCurrDoc = useCallback(() => {
setCurrDocName(undefined);
@ -187,7 +197,7 @@ export function PDFProvider({ children }: { children: ReactNode }) {
* Ensures the context is used within a provider
*
* @throws {Error} If used outside of PDFProvider
* @returns {PDFContextType} The PDF context value
* @returns {PDFContextType} The PDF context value containing all PDF-related functionality
*/
export function usePDF() {
const context = useContext(PDFContext);

View file

@ -71,19 +71,21 @@ interface TTSContextType {
setCurrDocPages: (num: number | undefined) => void;
setSpeedAndRestart: (speed: number) => void;
setVoiceAndRestart: (voice: string) => void;
skipToPage: (page: number) => void;
setEPUBPageInChapter: (page: string | number, total: number, chapter: string | number) => void; // Add this line
registerLocationChangeHandler: (handler: (location: string | number, initial?: boolean) => void) => void;
skipToLocation: (location: string | number) => void;
registerLocationChangeHandler: (handler: (location: string | number) => void) => void; // EPUB-only: Handles chapter navigation
setIsEPUB: (isEPUB: boolean) => void;
}
// Create the context
const TTSContext = createContext<TTSContextType | undefined>(undefined);
/**
* TTSProvider Component
*
* Main provider component that manages the TTS state and functionality.
* Handles initialization of OpenAI client, audio context, and media session.
*
* @param {Object} props - Component props
* @param {ReactNode} props.children - Child components to be wrapped by the provider
* @returns {JSX.Element} TTSProvider component
*/
export function TTSProvider({ children }: { children: ReactNode }) {
// Configuration context consumption
@ -106,10 +108,15 @@ export function TTSProvider({ children }: { children: ReactNode }) {
const { availableVoices, fetchVoices } = useVoiceManagement(openApiKey, openApiBaseUrl);
// Add ref for location change handler
const locationChangeHandlerRef = useRef<((location: string | number, initial?: boolean) => void) | null>(null);
const locationChangeHandlerRef = useRef<((location: string | number) => void) | null>(null);
// Add method to register location change handler
const registerLocationChangeHandler = useCallback((handler: (location: string | number, initial?: boolean) => void) => {
/**
* Registers a handler function for location changes in EPUB documents
* This is only used for EPUB documents to handle chapter navigation
*
* @param {Function} handler - Function to handle location changes
*/
const registerLocationChangeHandler = useCallback((handler: (location: string | number) => void) => {
locationChangeHandlerRef.current = handler;
}, []);
@ -140,68 +147,97 @@ export function TTSProvider({ children }: { children: ReactNode }) {
* Changes the current page by a specified amount
*
* @param {number} [num=1] - The number of pages to increment by
* @returns {void}
*/
const incrementPage = useCallback((num = 1) => {
setNextPageLoading(true);
setCurrDocPage(currDocPageNumber + num);
}, [currDocPageNumber]);
/**
* Processes text through the NLP API to split it into sentences
*
* @param {string} text - The text to be processed
* @returns {Promise<string[]>} Array of processed sentences
*/
const processTextToSentences = useCallback(async (text: string): Promise<string[]> => {
const response = await fetch('/api/nlp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text }),
});
if (!response.ok) {
throw new Error('Failed to process text');
}
const { sentences } = await response.json();
return sentences;
}, []);
/**
* Handles blank text sections based on document type
*
* @param {string[]} sentences - Array of processed sentences
* @returns {boolean} - True if blank section was handled
*/
const handleBlankSection = useCallback((sentences: string[]): boolean => {
if (!isPlaying || !skipBlank || sentences.length > 0) {
return false;
}
if (isEPUB && locationChangeHandlerRef.current) {
locationChangeHandlerRef.current('next');
toast.success('Skipping blank section', {
id: `epub-section-skip`,
iconTheme: {
primary: 'var(--accent)',
secondary: 'var(--background)',
},
style: {
background: 'var(--background)',
color: 'var(--accent)',
},
duration: 1000,
position: 'top-center',
});
return true;
}
if (currDocPageNumber < currDocPages!) {
incrementPage();
toast.success(`Skipping blank page ${currDocPageNumber}`, {
id: `page-${currDocPageNumber}`,
iconTheme: {
primary: 'var(--accent)',
secondary: 'var(--background)',
},
style: {
background: 'var(--background)',
color: 'var(--accent)',
},
duration: 1000,
position: 'top-center',
});
return true;
}
return false;
}, [isPlaying, skipBlank, isEPUB, currDocPageNumber, currDocPages, incrementPage]);
/**
* Sets the current text and splits it into sentences
*
* @param {string} text - The text to be processed
*/
const setText = useCallback((text: string) => {
console.log('Setting page text:', text);
fetch('/api/nlp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text }),
})
.then(response => {
if (!response.ok) {
throw new Error('Failed to process text');
}
return response.json();
})
.then(({ sentences: newSentences }) => {
// If skipBlank is enabled and there's no text
if (isPlaying && skipBlank && newSentences.length === 0) {
if (isEPUB && locationChangeHandlerRef.current) {
locationChangeHandlerRef.current('next');
toast.success('Skipping blank section', {
id: `epub-section-skip`,
iconTheme: {
primary: 'var(--accent)',
secondary: 'var(--background)',
},
style: {
background: 'var(--background)',
color: 'var(--accent)',
},
duration: 1000,
position: 'top-center',
});
return;
} else if (currDocPageNumber < currDocPages!) {
incrementPage();
toast.success(`Skipping blank page ${currDocPageNumber}`, {
id: `page-${currDocPageNumber}`,
iconTheme: {
primary: 'var(--accent)',
secondary: 'var(--background)',
},
style: {
background: 'var(--background)',
color: 'var(--accent)',
},
duration: 1000,
position: 'top-center',
});
return;
}
processTextToSentences(text)
.then(newSentences => {
if (handleBlankSection(newSentences)) {
return;
}
setSentences(newSentences);
@ -217,12 +253,10 @@ export function TTSProvider({ children }: { children: ReactNode }) {
duration: 3000,
});
});
}, [isPlaying, skipBlank, currDocPageNumber, currDocPages, incrementPage, isEPUB]);
}, [processTextToSentences, handleBlankSection]);
/**
* Stops the current audio playback and clears the active Howl instance
*
* @returns {void}
*/
const abortAudio = useCallback(() => {
if (activeHowl) {
@ -233,8 +267,6 @@ export function TTSProvider({ children }: { children: ReactNode }) {
/**
* Toggles the playback state between playing and paused
*
* @returns {void}
*/
const togglePlay = useCallback(() => {
setIsPlaying((prev) => {
@ -248,61 +280,28 @@ export function TTSProvider({ children }: { children: ReactNode }) {
}, [abortAudio]);
/**
* Navigates to a specific page in the document
* Navigates to a specific location in the document
* Works for both PDF pages and EPUB locations
*
* @param {number} page - The target page number
* @returns {void}
* @param {string | number} location - The target location to navigate to
*/
const skipToPage = useCallback((page: number) => {
abortAudio();
setIsPlaying(false);
const skipToLocation = useCallback((location: string | number) => {
setNextPageLoading(true);
setCurrentIndex(0);
setCurrDocPage(page);
}, [abortAudio]);
/**
* Navigates to a specific location in the EPUB document
* Similar to skipToPage but for EPUB locations
*/
const [currChapter, setCurrChapter] = useState<string | number>('');
const setEPUBPageInChapter = useCallback((page: string | number, total: number, chapter: string | number) => {
const alreadyPlaying = isPlaying;
const isNewChapter = chapter !== currChapter;
// Mark that we're in EPUB mode
setIsEPUB(true);
// Update chapter info
if (isNewChapter) {
setCurrDocPages(total);
setCurrChapter(chapter);
console.log('Changed to chapter:', chapter);
}
// Reset state for new content
abortAudio();
setIsPlaying(false);
setNextPageLoading(true);
setCurrentIndex(0);
setSentences([]);
// Update current page
setCurrDocPage(Number(page));
// Resume playback if it was playing before
// Only auto-resume if this was triggered by automatic navigation (not manual page turns)
if (alreadyPlaying) {
setIsPlaying(true);
}
}, [abortAudio, currChapter, isPlaying]);
// Update current page/location
setCurrDocPage(location);
}, [abortAudio]);
/**
* Moves to the next or previous sentence
*
* @param {boolean} [backwards=false] - Whether to move backwards
* @returns {Promise<void>}
*/
const advance = useCallback(async (backwards = false) => {
const nextIndex = currentIndex + (backwards ? -1 : 1);
@ -345,8 +344,6 @@ export function TTSProvider({ children }: { children: ReactNode }) {
/**
* Moves forward one sentence in the text
*
* @returns {void}
*/
const skipForward = useCallback(() => {
setIsProcessing(true);
@ -360,8 +357,6 @@ export function TTSProvider({ children }: { children: ReactNode }) {
/**
* Moves backward one sentence in the text
*
* @returns {void}
*/
const skipBackward = useCallback(() => {
setIsProcessing(true);
@ -375,8 +370,6 @@ export function TTSProvider({ children }: { children: ReactNode }) {
/**
* Updates the voice and speed settings from the configuration
*
* @returns {void}
*/
const updateVoiceAndSpeed = useCallback(() => {
setVoice(configVoice);
@ -401,7 +394,8 @@ export function TTSProvider({ children }: { children: ReactNode }) {
/**
* Generates and plays audio for the current sentence
*
* @returns {Promise<void>}
* @param {string} sentence - The sentence to generate audio for
* @returns {Promise<AudioBuffer | undefined>} The generated audio buffer
*/
const getAudio = useCallback(async (sentence: string): Promise<AudioBuffer | undefined> => {
// Check if the audio is already cached
@ -448,7 +442,9 @@ export function TTSProvider({ children }: { children: ReactNode }) {
/**
* Processes and plays the current sentence
*
* @returns {Promise<void>}
* @param {string} sentence - The sentence to process
* @param {boolean} [preload=false] - Whether this is a preload request
* @returns {Promise<string>} The URL of the processed audio
*/
const processSentence = useCallback(async (sentence: string, preload = false): Promise<string> => {
if (isProcessing && !preload) throw new Error('Audio is already being processed');
@ -465,7 +461,7 @@ export function TTSProvider({ children }: { children: ReactNode }) {
/**
* Plays the current sentence with Howl
*
* @returns {Promise<void>}
* @param {string} sentence - The sentence to play
*/
const playSentenceWithHowl = useCallback(async (sentence: string) => {
try {
@ -530,8 +526,6 @@ export function TTSProvider({ children }: { children: ReactNode }) {
/**
* Preloads the next sentence's audio
*
* @returns {void}
*/
const preloadNextAudio = useCallback(() => {
try {
@ -545,8 +539,6 @@ export function TTSProvider({ children }: { children: ReactNode }) {
/**
* Plays the current sentence's audio
*
* @returns {Promise<void>}
*/
const playAudio = useCallback(async () => {
await playSentenceWithHowl(sentences[currentIndex]);
@ -585,9 +577,7 @@ export function TTSProvider({ children }: { children: ReactNode }) {
]);
/**
* Stops the current audio playback
*
* @returns {void}
* Stops the current audio playback and resets all state
*/
const stop = useCallback(() => {
// Cancel any ongoing request
@ -596,7 +586,6 @@ export function TTSProvider({ children }: { children: ReactNode }) {
setIsPlaying(false);
setCurrentIndex(0);
setSentences([]);
setCurrChapter('');
setCurrDocPage(1);
setCurrDocPages(undefined);
setNextPageLoading(false);
@ -608,7 +597,6 @@ export function TTSProvider({ children }: { children: ReactNode }) {
* Stops the current audio playback and starts playing from a specified index
*
* @param {number} index - The index to start playing from
* @returns {void}
*/
const stopAndPlayFromIndex = useCallback((index: number) => {
abortAudio();
@ -621,7 +609,6 @@ export function TTSProvider({ children }: { children: ReactNode }) {
* Sets the speed and restarts the playback
*
* @param {number} newSpeed - The new speed to set
* @returns {void}
*/
const setSpeedAndRestart = useCallback((newSpeed: number) => {
setSpeed(newSpeed);
@ -640,7 +627,6 @@ export function TTSProvider({ children }: { children: ReactNode }) {
* Sets the voice and restarts the playback
*
* @param {string} newVoice - The new voice to set
* @returns {void}
*/
const setVoiceAndRestart = useCallback((newVoice: string) => {
setVoice(newVoice);
@ -675,9 +661,9 @@ export function TTSProvider({ children }: { children: ReactNode }) {
setCurrDocPages,
setSpeedAndRestart,
setVoiceAndRestart,
skipToPage,
setEPUBPageInChapter, // Add this line
skipToLocation,
registerLocationChangeHandler,
setIsEPUB
}), [
isPlaying,
isProcessing,
@ -696,9 +682,9 @@ export function TTSProvider({ children }: { children: ReactNode }) {
setCurrDocPages,
setSpeedAndRestart,
setVoiceAndRestart,
skipToPage,
setEPUBPageInChapter, // Add this line
skipToLocation,
registerLocationChangeHandler,
setIsEPUB
]);
// Use media session hook
@ -714,9 +700,8 @@ export function TTSProvider({ children }: { children: ReactNode }) {
getLastDocumentLocation(id as string).then(lastLocation => {
if (lastLocation) {
console.log('Setting last location:', lastLocation);
setCurrDocPage(lastLocation);
if (locationChangeHandlerRef.current) {
locationChangeHandlerRef.current(lastLocation, true);
locationChangeHandlerRef.current(lastLocation);
}
}
});
@ -739,6 +724,9 @@ export function TTSProvider({ children }: { children: ReactNode }) {
/**
* Custom hook to consume the TTS context
* Ensures the context is used within a provider
*
* @throws {Error} If used outside of TTSProvider
* @returns {TTSContextType} The TTS context value
*/
export function useTTS() {
const context = useContext(TTSContext);