Refactors + epub location fixes
This commit is contained in:
parent
9ae6dd9e82
commit
0ce14f3aec
9 changed files with 239 additions and 205 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import nlp from 'compromise';
|
import nlp from 'compromise';
|
||||||
|
|
||||||
const MAX_BLOCK_LENGTH = 350;
|
const MAX_BLOCK_LENGTH = 300;
|
||||||
|
|
||||||
const preprocessSentenceForAudio = (text: string): string => {
|
const preprocessSentenceForAudio = (text: string): string => {
|
||||||
return text
|
return text
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useRef, useCallback, useState, useMemo } from 'react';
|
import { useEffect, useRef, useCallback, useMemo } from 'react';
|
||||||
import { useParams } from 'next/navigation';
|
import { useParams } from 'next/navigation';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import { useEPUB } from '@/contexts/EPUBContext';
|
import { useEPUB } from '@/contexts/EPUBContext';
|
||||||
|
|
@ -24,18 +24,29 @@ interface EPUBViewerProps {
|
||||||
export function EPUBViewer({ className = '' }: EPUBViewerProps) {
|
export function EPUBViewer({ className = '' }: EPUBViewerProps) {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const { currDocData, currDocName, currDocPage, extractPageText } = useEPUB();
|
const { currDocData, currDocName, currDocPage, extractPageText } = useEPUB();
|
||||||
const { setEPUBPageInChapter, registerLocationChangeHandler } = useTTS();
|
const { skipToLocation, registerLocationChangeHandler, setIsEPUB } = useTTS();
|
||||||
const { epubTheme } = useConfig();
|
const { epubTheme } = useConfig();
|
||||||
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[]>([]);
|
||||||
const locationRef = useRef<string | number>(currDocPage);
|
const locationRef = useRef<string | number>(currDocPage);
|
||||||
const [initialPrevLocLoad, setInitialPrevLocLoad] = useState(false);
|
|
||||||
const { updateTheme } = useEPUBTheme(epubTheme, rendition.current);
|
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;
|
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) {
|
if (location === 'next' && rendition.current) {
|
||||||
rendition.current.next();
|
rendition.current.next();
|
||||||
return;
|
return;
|
||||||
|
|
@ -45,33 +56,18 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save the location to IndexedDB if not initial
|
||||||
const { displayed, href } = rendition.current.location.start;
|
if (id && locationRef.current !== 1) {
|
||||||
const chapter = toc.current.find((item) => item.href === href);
|
console.log('Saving location:', location);
|
||||||
|
setLastDocumentLocation(id as string, location.toString());
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setEPUBPageInChapter(displayed.page, displayed.total, chapter?.label || '');
|
skipToLocation(location);
|
||||||
|
|
||||||
// Add a small delay for initial load to ensure rendition is ready
|
locationRef.current = location;
|
||||||
if (initial) {
|
extractPageText(bookRef.current, rendition.current);
|
||||||
rendition.current.display(location.toString()).then(() => {
|
|
||||||
setInitialPrevLocLoad(true);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
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
|
// Replace the debounced text extraction with a proper implementation using useMemo
|
||||||
const debouncedExtractText = useMemo(() => {
|
const debouncedExtractText = useMemo(() => {
|
||||||
|
|
@ -86,27 +82,27 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
|
||||||
|
|
||||||
// Load the initial location and setup resize handler
|
// Load the initial location and setup resize handler
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (bookRef.current && rendition.current) {
|
if (!bookRef.current || !rendition.current || isEPUBSetOnce.current) return;
|
||||||
extractPageText(bookRef.current, rendition.current);
|
|
||||||
|
|
||||||
// Add resize observer
|
extractPageText(bookRef.current, rendition.current);
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
|
||||||
if (bookRef.current && rendition.current) {
|
|
||||||
debouncedExtractText(bookRef.current, rendition.current);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Observe the container element
|
// Add resize observer
|
||||||
const container = document.querySelector('.epub-container');
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
if (container) {
|
if (bookRef.current && rendition.current) {
|
||||||
resizeObserver.observe(container);
|
debouncedExtractText(bookRef.current, rendition.current);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
// Observe the container element
|
||||||
resizeObserver.disconnect();
|
const container = document.querySelector('.epub-container');
|
||||||
};
|
if (container) {
|
||||||
|
resizeObserver.observe(container);
|
||||||
}
|
}
|
||||||
}, [extractPageText, debouncedExtractText, initialPrevLocLoad]);
|
|
||||||
|
return () => {
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
};
|
||||||
|
}, [extractPageText, debouncedExtractText]);
|
||||||
|
|
||||||
// Register the location change handler
|
// Register the location change handler
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export function PDFViewer({ zoomLevel }: PDFViewerProps) {
|
||||||
const {
|
const {
|
||||||
currentSentence,
|
currentSentence,
|
||||||
stopAndPlayFromIndex,
|
stopAndPlayFromIndex,
|
||||||
isProcessing
|
isProcessing,
|
||||||
} = useTTS();
|
} = useTTS();
|
||||||
|
|
||||||
// PDF context
|
// PDF context
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,16 @@
|
||||||
|
|
||||||
import { Button } from '@headlessui/react';
|
import { Button } from '@headlessui/react';
|
||||||
|
|
||||||
export const Navigator = ({ currentPage, numPages, skipToPage }: {
|
export const Navigator = ({ currentPage, numPages, skipToLocation }: {
|
||||||
currentPage: number;
|
currentPage: number;
|
||||||
numPages: number | undefined;
|
numPages: number | undefined;
|
||||||
skipToPage: (page: number) => void;
|
skipToLocation: (location: string | number) => void;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center space-x-1">
|
<div className="flex items-center space-x-1">
|
||||||
{/* Page back */}
|
{/* Page back */}
|
||||||
<Button
|
<Button
|
||||||
onClick={() => skipToPage(currentPage - 1)}
|
onClick={() => skipToLocation(currentPage - 1)}
|
||||||
disabled={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"
|
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"
|
aria-label="Previous page"
|
||||||
|
|
@ -30,7 +30,7 @@ export const Navigator = ({ currentPage, numPages, skipToPage }: {
|
||||||
|
|
||||||
{/* Page forward */}
|
{/* Page forward */}
|
||||||
<Button
|
<Button
|
||||||
onClick={() => skipToPage(currentPage + 1)}
|
onClick={() => skipToLocation(currentPage + 1)}
|
||||||
disabled={currentPage >= (numPages || 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"
|
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"
|
aria-label="Next page"
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ export default function TTSPlayer({ currentPage, numPages }: {
|
||||||
setSpeedAndRestart,
|
setSpeedAndRestart,
|
||||||
setVoiceAndRestart,
|
setVoiceAndRestart,
|
||||||
availableVoices,
|
availableVoices,
|
||||||
skipToPage,
|
skipToLocation,
|
||||||
} = useTTS();
|
} = useTTS();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -36,8 +36,13 @@ export default function TTSPlayer({ currentPage, numPages }: {
|
||||||
<SpeedControl setSpeedAndRestart={setSpeedAndRestart} />
|
<SpeedControl setSpeedAndRestart={setSpeedAndRestart} />
|
||||||
|
|
||||||
{/* Page Navigation */}
|
{/* Page Navigation */}
|
||||||
{currentPage && numPages
|
{currentPage && numPages && (
|
||||||
&& <Navigator currentPage={currentPage} numPages={numPages} skipToPage={skipToPage} />}
|
<Navigator
|
||||||
|
currentPage={currentPage}
|
||||||
|
numPages={numPages}
|
||||||
|
skipToLocation={skipToLocation}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Playback Controls */}
|
{/* Playback Controls */}
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
|
|
@ -3,22 +3,10 @@
|
||||||
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
|
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
|
||||||
import { getItem, indexedDBService, setItem } from '@/utils/indexedDB';
|
import { getItem, indexedDBService, setItem } from '@/utils/indexedDB';
|
||||||
|
|
||||||
|
/** Represents the possible view types for document display */
|
||||||
export type ViewType = 'single' | 'dual' | 'scroll';
|
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 = {
|
type ConfigValues = {
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
|
|
@ -26,11 +14,32 @@ type ConfigValues = {
|
||||||
voiceSpeed: number;
|
voiceSpeed: number;
|
||||||
voice: string;
|
voice: string;
|
||||||
skipBlank: boolean;
|
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);
|
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 }) {
|
export function ConfigProvider({ children }: { children: ReactNode }) {
|
||||||
// Config state
|
// Config state
|
||||||
const [apiKey, setApiKey] = useState<string>('');
|
const [apiKey, setApiKey] = useState<string>('');
|
||||||
|
|
@ -108,6 +117,10 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
|
||||||
initializeDB();
|
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 }>) => {
|
const updateConfig = async (newConfig: Partial<{ apiKey: string; baseUrl: string }>) => {
|
||||||
try {
|
try {
|
||||||
if (newConfig.apiKey !== undefined) {
|
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]) => {
|
const updateConfigKey = async <K extends keyof ConfigValues>(key: K, value: ConfigValues[K]) => {
|
||||||
try {
|
try {
|
||||||
await setItem(key, value.toString());
|
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() {
|
export function useConfig() {
|
||||||
const context = useContext(ConfigContext);
|
const context = useContext(ConfigContext);
|
||||||
if (context === undefined) {
|
if (context === undefined) {
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,12 @@ interface EPUBContextType {
|
||||||
|
|
||||||
const EPUBContext = createContext<EPUBContextType | undefined>(undefined);
|
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 }) {
|
export function EPUBProvider({ children }: { children: ReactNode }) {
|
||||||
const { setText: setTTSText, currDocPage, currDocPages, setCurrDocPages, stop } = useTTS();
|
const { setText: setTTSText, currDocPage, currDocPages, setCurrDocPages, stop } = useTTS();
|
||||||
|
|
||||||
|
|
@ -35,7 +41,7 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
|
||||||
const [currDocText, setCurrDocText] = useState<string>();
|
const [currDocText, setCurrDocText] = useState<string>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears the current document state
|
* Clears all current document state and stops any active TTS
|
||||||
*/
|
*/
|
||||||
const clearCurrDoc = useCallback(() => {
|
const clearCurrDoc = useCallback(() => {
|
||||||
setCurrDocData(undefined);
|
setCurrDocData(undefined);
|
||||||
|
|
@ -46,7 +52,9 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
|
||||||
}, [setCurrDocPages, stop]);
|
}, [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> => {
|
const setCurrentDocument = useCallback(async (id: string): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -73,6 +81,9 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts text content from the current EPUB page/location
|
* 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> => {
|
const extractPageText = useCallback(async (book: Book, rendition: Rendition): Promise<string> => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -127,7 +138,8 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom hook to consume the EPUB context
|
* 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() {
|
export function useEPUB() {
|
||||||
const context = useContext(EPUBContext);
|
const context = useContext(EPUBContext);
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,9 @@ const PDFContext = createContext<PDFContextType | undefined>(undefined);
|
||||||
*
|
*
|
||||||
* Main provider component that manages PDF state and functionality.
|
* Main provider component that manages PDF state and functionality.
|
||||||
* Handles document loading, text processing, and integration with TTS.
|
* 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 }) {
|
export function PDFProvider({ children }: { children: ReactNode }) {
|
||||||
const { setText: setTTSText, stop, currDocPageNumber: currDocPage, currDocPages, setCurrDocPages } = useTTS();
|
const { setText: setTTSText, stop, currDocPageNumber: currDocPage, currDocPages, setCurrDocPages } = useTTS();
|
||||||
|
|
@ -82,9 +85,9 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
||||||
const [pdfDocument, setPdfDocument] = useState<PDFDocumentProxy>();
|
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) => {
|
const onDocumentLoadSuccess = useCallback((pdf: PDFDocumentProxy) => {
|
||||||
console.log('Document loaded:', pdf.numPages);
|
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
|
* 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 () => {
|
const loadCurrDocText = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -108,7 +114,8 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
||||||
}, [pdfDocument, currDocPage, setTTSText]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (currDocURL) {
|
if (currDocURL) {
|
||||||
|
|
@ -118,8 +125,10 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the current document based on its ID
|
* 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> => {
|
const setCurrentDocument = useCallback(async (id: string): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -136,6 +145,7 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears the current document state
|
* Clears the current document state
|
||||||
|
* Resets all document-related states and stops any ongoing TTS playback
|
||||||
*/
|
*/
|
||||||
const clearCurrDoc = useCallback(() => {
|
const clearCurrDoc = useCallback(() => {
|
||||||
setCurrDocName(undefined);
|
setCurrDocName(undefined);
|
||||||
|
|
@ -187,7 +197,7 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
||||||
* Ensures the context is used within a provider
|
* Ensures the context is used within a provider
|
||||||
*
|
*
|
||||||
* @throws {Error} If used outside of PDFProvider
|
* @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() {
|
export function usePDF() {
|
||||||
const context = useContext(PDFContext);
|
const context = useContext(PDFContext);
|
||||||
|
|
|
||||||
|
|
@ -71,19 +71,21 @@ interface TTSContextType {
|
||||||
setCurrDocPages: (num: number | undefined) => void;
|
setCurrDocPages: (num: number | undefined) => void;
|
||||||
setSpeedAndRestart: (speed: number) => void;
|
setSpeedAndRestart: (speed: number) => void;
|
||||||
setVoiceAndRestart: (voice: string) => void;
|
setVoiceAndRestart: (voice: string) => void;
|
||||||
skipToPage: (page: number) => void;
|
skipToLocation: (location: string | number) => void;
|
||||||
setEPUBPageInChapter: (page: string | number, total: number, chapter: string | number) => void; // Add this line
|
registerLocationChangeHandler: (handler: (location: string | number) => void) => void; // EPUB-only: Handles chapter navigation
|
||||||
registerLocationChangeHandler: (handler: (location: string | number, initial?: boolean) => void) => void;
|
setIsEPUB: (isEPUB: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the context
|
// Create the context
|
||||||
const TTSContext = createContext<TTSContextType | undefined>(undefined);
|
const TTSContext = createContext<TTSContextType | undefined>(undefined);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TTSProvider Component
|
|
||||||
*
|
|
||||||
* Main provider component that manages the TTS state and functionality.
|
* Main provider component that manages the TTS state and functionality.
|
||||||
* Handles initialization of OpenAI client, audio context, and media session.
|
* 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 }) {
|
export function TTSProvider({ children }: { children: ReactNode }) {
|
||||||
// Configuration context consumption
|
// Configuration context consumption
|
||||||
|
|
@ -106,10 +108,15 @@ export function TTSProvider({ children }: { children: ReactNode }) {
|
||||||
const { availableVoices, fetchVoices } = useVoiceManagement(openApiKey, openApiBaseUrl);
|
const { availableVoices, fetchVoices } = useVoiceManagement(openApiKey, openApiBaseUrl);
|
||||||
|
|
||||||
// Add ref for location change handler
|
// 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;
|
locationChangeHandlerRef.current = handler;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -140,68 +147,97 @@ export function TTSProvider({ children }: { children: ReactNode }) {
|
||||||
* Changes the current page by a specified amount
|
* Changes the current page by a specified amount
|
||||||
*
|
*
|
||||||
* @param {number} [num=1] - The number of pages to increment by
|
* @param {number} [num=1] - The number of pages to increment by
|
||||||
* @returns {void}
|
|
||||||
*/
|
*/
|
||||||
const incrementPage = useCallback((num = 1) => {
|
const incrementPage = useCallback((num = 1) => {
|
||||||
setNextPageLoading(true);
|
setNextPageLoading(true);
|
||||||
setCurrDocPage(currDocPageNumber + num);
|
setCurrDocPage(currDocPageNumber + num);
|
||||||
}, [currDocPageNumber]);
|
}, [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
|
* Sets the current text and splits it into sentences
|
||||||
|
*
|
||||||
|
* @param {string} text - The text to be processed
|
||||||
*/
|
*/
|
||||||
const setText = useCallback((text: string) => {
|
const setText = useCallback((text: string) => {
|
||||||
console.log('Setting page text:', text);
|
console.log('Setting page text:', text);
|
||||||
|
|
||||||
fetch('/api/nlp', {
|
processTextToSentences(text)
|
||||||
method: 'POST',
|
.then(newSentences => {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
if (handleBlankSection(newSentences)) {
|
||||||
body: JSON.stringify({ text }),
|
return;
|
||||||
})
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setSentences(newSentences);
|
setSentences(newSentences);
|
||||||
|
|
@ -217,12 +253,10 @@ export function TTSProvider({ children }: { children: ReactNode }) {
|
||||||
duration: 3000,
|
duration: 3000,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, [isPlaying, skipBlank, currDocPageNumber, currDocPages, incrementPage, isEPUB]);
|
}, [processTextToSentences, handleBlankSection]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops the current audio playback and clears the active Howl instance
|
* Stops the current audio playback and clears the active Howl instance
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
*/
|
||||||
const abortAudio = useCallback(() => {
|
const abortAudio = useCallback(() => {
|
||||||
if (activeHowl) {
|
if (activeHowl) {
|
||||||
|
|
@ -233,8 +267,6 @@ export function TTSProvider({ children }: { children: ReactNode }) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggles the playback state between playing and paused
|
* Toggles the playback state between playing and paused
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
*/
|
||||||
const togglePlay = useCallback(() => {
|
const togglePlay = useCallback(() => {
|
||||||
setIsPlaying((prev) => {
|
setIsPlaying((prev) => {
|
||||||
|
|
@ -248,61 +280,28 @@ export function TTSProvider({ children }: { children: ReactNode }) {
|
||||||
}, [abortAudio]);
|
}, [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
|
* @param {string | number} location - The target location to navigate to
|
||||||
* @returns {void}
|
|
||||||
*/
|
*/
|
||||||
const skipToPage = useCallback((page: number) => {
|
const skipToLocation = useCallback((location: string | number) => {
|
||||||
abortAudio();
|
|
||||||
setIsPlaying(false);
|
|
||||||
setNextPageLoading(true);
|
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
|
// Reset state for new content
|
||||||
abortAudio();
|
abortAudio();
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
setNextPageLoading(true);
|
|
||||||
setCurrentIndex(0);
|
setCurrentIndex(0);
|
||||||
setSentences([]);
|
setSentences([]);
|
||||||
|
|
||||||
// Update current page
|
// Update current page/location
|
||||||
setCurrDocPage(Number(page));
|
setCurrDocPage(location);
|
||||||
|
}, [abortAudio]);
|
||||||
// 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]);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Moves to the next or previous sentence
|
* Moves to the next or previous sentence
|
||||||
*
|
*
|
||||||
* @param {boolean} [backwards=false] - Whether to move backwards
|
* @param {boolean} [backwards=false] - Whether to move backwards
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
*/
|
||||||
const advance = useCallback(async (backwards = false) => {
|
const advance = useCallback(async (backwards = false) => {
|
||||||
const nextIndex = currentIndex + (backwards ? -1 : 1);
|
const nextIndex = currentIndex + (backwards ? -1 : 1);
|
||||||
|
|
@ -345,8 +344,6 @@ export function TTSProvider({ children }: { children: ReactNode }) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Moves forward one sentence in the text
|
* Moves forward one sentence in the text
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
*/
|
||||||
const skipForward = useCallback(() => {
|
const skipForward = useCallback(() => {
|
||||||
setIsProcessing(true);
|
setIsProcessing(true);
|
||||||
|
|
@ -360,8 +357,6 @@ export function TTSProvider({ children }: { children: ReactNode }) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Moves backward one sentence in the text
|
* Moves backward one sentence in the text
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
*/
|
||||||
const skipBackward = useCallback(() => {
|
const skipBackward = useCallback(() => {
|
||||||
setIsProcessing(true);
|
setIsProcessing(true);
|
||||||
|
|
@ -375,8 +370,6 @@ export function TTSProvider({ children }: { children: ReactNode }) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the voice and speed settings from the configuration
|
* Updates the voice and speed settings from the configuration
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
*/
|
||||||
const updateVoiceAndSpeed = useCallback(() => {
|
const updateVoiceAndSpeed = useCallback(() => {
|
||||||
setVoice(configVoice);
|
setVoice(configVoice);
|
||||||
|
|
@ -401,7 +394,8 @@ export function TTSProvider({ children }: { children: ReactNode }) {
|
||||||
/**
|
/**
|
||||||
* Generates and plays audio for the current sentence
|
* 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> => {
|
const getAudio = useCallback(async (sentence: string): Promise<AudioBuffer | undefined> => {
|
||||||
// Check if the audio is already cached
|
// Check if the audio is already cached
|
||||||
|
|
@ -448,7 +442,9 @@ export function TTSProvider({ children }: { children: ReactNode }) {
|
||||||
/**
|
/**
|
||||||
* Processes and plays the current sentence
|
* 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> => {
|
const processSentence = useCallback(async (sentence: string, preload = false): Promise<string> => {
|
||||||
if (isProcessing && !preload) throw new Error('Audio is already being processed');
|
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
|
* Plays the current sentence with Howl
|
||||||
*
|
*
|
||||||
* @returns {Promise<void>}
|
* @param {string} sentence - The sentence to play
|
||||||
*/
|
*/
|
||||||
const playSentenceWithHowl = useCallback(async (sentence: string) => {
|
const playSentenceWithHowl = useCallback(async (sentence: string) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -530,8 +526,6 @@ export function TTSProvider({ children }: { children: ReactNode }) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Preloads the next sentence's audio
|
* Preloads the next sentence's audio
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
*/
|
||||||
const preloadNextAudio = useCallback(() => {
|
const preloadNextAudio = useCallback(() => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -545,8 +539,6 @@ export function TTSProvider({ children }: { children: ReactNode }) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plays the current sentence's audio
|
* Plays the current sentence's audio
|
||||||
*
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
*/
|
||||||
const playAudio = useCallback(async () => {
|
const playAudio = useCallback(async () => {
|
||||||
await playSentenceWithHowl(sentences[currentIndex]);
|
await playSentenceWithHowl(sentences[currentIndex]);
|
||||||
|
|
@ -585,9 +577,7 @@ export function TTSProvider({ children }: { children: ReactNode }) {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops the current audio playback
|
* Stops the current audio playback and resets all state
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
*/
|
||||||
const stop = useCallback(() => {
|
const stop = useCallback(() => {
|
||||||
// Cancel any ongoing request
|
// Cancel any ongoing request
|
||||||
|
|
@ -596,7 +586,6 @@ export function TTSProvider({ children }: { children: ReactNode }) {
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
setCurrentIndex(0);
|
setCurrentIndex(0);
|
||||||
setSentences([]);
|
setSentences([]);
|
||||||
setCurrChapter('');
|
|
||||||
setCurrDocPage(1);
|
setCurrDocPage(1);
|
||||||
setCurrDocPages(undefined);
|
setCurrDocPages(undefined);
|
||||||
setNextPageLoading(false);
|
setNextPageLoading(false);
|
||||||
|
|
@ -608,7 +597,6 @@ export function TTSProvider({ children }: { children: ReactNode }) {
|
||||||
* Stops the current audio playback and starts playing from a specified index
|
* Stops the current audio playback and starts playing from a specified index
|
||||||
*
|
*
|
||||||
* @param {number} index - The index to start playing from
|
* @param {number} index - The index to start playing from
|
||||||
* @returns {void}
|
|
||||||
*/
|
*/
|
||||||
const stopAndPlayFromIndex = useCallback((index: number) => {
|
const stopAndPlayFromIndex = useCallback((index: number) => {
|
||||||
abortAudio();
|
abortAudio();
|
||||||
|
|
@ -621,7 +609,6 @@ export function TTSProvider({ children }: { children: ReactNode }) {
|
||||||
* Sets the speed and restarts the playback
|
* Sets the speed and restarts the playback
|
||||||
*
|
*
|
||||||
* @param {number} newSpeed - The new speed to set
|
* @param {number} newSpeed - The new speed to set
|
||||||
* @returns {void}
|
|
||||||
*/
|
*/
|
||||||
const setSpeedAndRestart = useCallback((newSpeed: number) => {
|
const setSpeedAndRestart = useCallback((newSpeed: number) => {
|
||||||
setSpeed(newSpeed);
|
setSpeed(newSpeed);
|
||||||
|
|
@ -640,7 +627,6 @@ export function TTSProvider({ children }: { children: ReactNode }) {
|
||||||
* Sets the voice and restarts the playback
|
* Sets the voice and restarts the playback
|
||||||
*
|
*
|
||||||
* @param {string} newVoice - The new voice to set
|
* @param {string} newVoice - The new voice to set
|
||||||
* @returns {void}
|
|
||||||
*/
|
*/
|
||||||
const setVoiceAndRestart = useCallback((newVoice: string) => {
|
const setVoiceAndRestart = useCallback((newVoice: string) => {
|
||||||
setVoice(newVoice);
|
setVoice(newVoice);
|
||||||
|
|
@ -675,9 +661,9 @@ export function TTSProvider({ children }: { children: ReactNode }) {
|
||||||
setCurrDocPages,
|
setCurrDocPages,
|
||||||
setSpeedAndRestart,
|
setSpeedAndRestart,
|
||||||
setVoiceAndRestart,
|
setVoiceAndRestart,
|
||||||
skipToPage,
|
skipToLocation,
|
||||||
setEPUBPageInChapter, // Add this line
|
|
||||||
registerLocationChangeHandler,
|
registerLocationChangeHandler,
|
||||||
|
setIsEPUB
|
||||||
}), [
|
}), [
|
||||||
isPlaying,
|
isPlaying,
|
||||||
isProcessing,
|
isProcessing,
|
||||||
|
|
@ -696,9 +682,9 @@ export function TTSProvider({ children }: { children: ReactNode }) {
|
||||||
setCurrDocPages,
|
setCurrDocPages,
|
||||||
setSpeedAndRestart,
|
setSpeedAndRestart,
|
||||||
setVoiceAndRestart,
|
setVoiceAndRestart,
|
||||||
skipToPage,
|
skipToLocation,
|
||||||
setEPUBPageInChapter, // Add this line
|
|
||||||
registerLocationChangeHandler,
|
registerLocationChangeHandler,
|
||||||
|
setIsEPUB
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Use media session hook
|
// Use media session hook
|
||||||
|
|
@ -714,9 +700,8 @@ export function TTSProvider({ children }: { children: ReactNode }) {
|
||||||
getLastDocumentLocation(id as string).then(lastLocation => {
|
getLastDocumentLocation(id as string).then(lastLocation => {
|
||||||
if (lastLocation) {
|
if (lastLocation) {
|
||||||
console.log('Setting last location:', lastLocation);
|
console.log('Setting last location:', lastLocation);
|
||||||
setCurrDocPage(lastLocation);
|
|
||||||
if (locationChangeHandlerRef.current) {
|
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
|
* Custom hook to consume the TTS context
|
||||||
* Ensures the context is used within a provider
|
* Ensures the context is used within a provider
|
||||||
|
*
|
||||||
|
* @throws {Error} If used outside of TTSProvider
|
||||||
|
* @returns {TTSContextType} The TTS context value
|
||||||
*/
|
*/
|
||||||
export function useTTS() {
|
export function useTTS() {
|
||||||
const context = useContext(TTSContext);
|
const context = useContext(TTSContext);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue