refactor(epub): move reader state to route-local hook

This commit is contained in:
Richard R 2026-05-13 14:22:24 -06:00
parent 2f067c0a90
commit fd6a06e467
4 changed files with 40 additions and 41 deletions

View file

@ -4,13 +4,12 @@ import type { ReactNode } from 'react';
import { ConfigProvider } from '@/contexts/ConfigContext'; import { ConfigProvider } from '@/contexts/ConfigContext';
import { TTSProvider } from '@/contexts/TTSContext'; import { TTSProvider } from '@/contexts/TTSContext';
import { EPUBProvider } from '@/contexts/EPUBContext';
export default function EpubReaderLayout({ children }: { children: ReactNode }) { export default function EpubReaderLayout({ children }: { children: ReactNode }) {
return ( return (
<ConfigProvider> <ConfigProvider>
<TTSProvider> <TTSProvider>
<EPUBProvider>{children}</EPUBProvider> {children}
</TTSProvider> </TTSProvider>
</ConfigProvider> </ConfigProvider>
); );

View file

@ -3,7 +3,6 @@
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
import Link from 'next/link'; import Link from 'next/link';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { useEPUB } from '@/contexts/EPUBContext';
import { DocumentSkeleton } from '@/components/documents/DocumentSkeleton'; import { DocumentSkeleton } from '@/components/documents/DocumentSkeleton';
import { EPUBViewer } from '@/components/views/EPUBViewer'; import { EPUBViewer } from '@/components/views/EPUBViewer';
import { DocumentSettings } from '@/components/documents/DocumentSettings'; import { DocumentSettings } from '@/components/documents/DocumentSettings';
@ -20,12 +19,21 @@ import { resolveDocumentId } from '@/lib/client/dexie';
import { RateLimitBanner } from '@/components/auth/RateLimitBanner'; import { RateLimitBanner } from '@/components/auth/RateLimitBanner';
import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext'; import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
import { useFeatureFlag } from '@/contexts/RuntimeConfigContext'; import { useFeatureFlag } from '@/contexts/RuntimeConfigContext';
import { useEPUB } from './useEpubDocument';
export default function EPUBPage() { export default function EPUBPage() {
const canExportAudiobook = useFeatureFlag('enableAudiobookExport'); const canExportAudiobook = useFeatureFlag('enableAudiobookExport');
const { id } = useParams(); const { id } = useParams();
const router = useRouter(); const router = useRouter();
const { setCurrentDocument, currDocName, clearCurrDoc, createFullAudioBook: createEPUBAudioBook, regenerateChapter: regenerateEPUBChapter, bookRef } = useEPUB(); const epubState = useEPUB();
const {
setCurrentDocument,
currDocName,
clearCurrDoc,
createFullAudioBook: createEPUBAudioBook,
regenerateChapter: regenerateEPUBChapter,
bookRef,
} = epubState;
const { stop } = useTTS(); const { stop } = useTTS();
const { isAtLimit } = useAuthRateLimit(); const { isAtLimit } = useAuthRateLimit();
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -209,7 +217,7 @@ export default function EPUBPage() {
</div> </div>
) : ( ) : (
<div className="h-full w-full" style={{ paddingLeft: `${Math.round(maxPadPx * ((100 - padPct) / 100))}px`, paddingRight: `${Math.round(maxPadPx * ((100 - padPct) / 100))}px` }}> <div className="h-full w-full" style={{ paddingLeft: `${Math.round(maxPadPx * ((100 - padPct) / 100))}px`, paddingRight: `${Math.round(maxPadPx * ((100 - padPct) / 100))}px` }}>
<EPUBViewer className="h-full" /> <EPUBViewer className="h-full" epubState={epubState} />
</div> </div>
)} )}
</div> </div>

View file

@ -1,10 +1,7 @@
'use client'; 'use client';
import { import {
createContext,
useContext,
useState, useState,
ReactNode,
useCallback, useCallback,
useMemo, useMemo,
useRef, useRef,
@ -36,7 +33,7 @@ import {
type EpubCanonicalWordToken, type EpubCanonicalWordToken,
} from '@/lib/client/epub/epub-word-highlight'; } from '@/lib/client/epub/epub-word-highlight';
import { useParams } from 'next/navigation'; import { useParams } from 'next/navigation';
import { useConfig } from './ConfigContext'; import { useConfig } from '@/contexts/ConfigContext';
import type { import type {
EpubRenderedLocationWalker, EpubRenderedLocationWalker,
TTSSentenceAlignment, TTSSentenceAlignment,
@ -47,7 +44,7 @@ import type { AudiobookGenerationSettings, TTSSegmentLocator } from '@/types/cli
import { isStableEpubLocator } from '@/types/client'; import { isStableEpubLocator } from '@/types/client';
import type { CanonicalTtsSegment } from '@/lib/shared/tts-segment-plan'; import type { CanonicalTtsSegment } from '@/lib/shared/tts-segment-plan';
interface EPUBContextType { export interface EPUBState {
currDocData: ArrayBuffer | undefined; currDocData: ArrayBuffer | undefined;
currDocName: string | undefined; currDocName: string | undefined;
currDocPages: number | undefined; currDocPages: number | undefined;
@ -90,8 +87,6 @@ interface EPUBContextType {
clearWordHighlights: () => void; clearWordHighlights: () => void;
} }
const EPUBContext = createContext<EPUBContextType | undefined>(undefined);
const EPUB_CONTINUATION_CHARS = 5000; const EPUB_CONTINUATION_CHARS = 5000;
const stepToNextNode = (node: Node | null, root: Node): Node | null => { const stepToNextNode = (node: Node | null, root: Node): Node | null => {
@ -526,12 +521,9 @@ const resolveVisibleSegmentRange = (
/** /**
* Provider component for EPUB functionality * Route-local EPUB reader hook.
* 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 useEPUB(): EPUBState {
const { setText: setTTSText, currDocPage, currDocPages, setCurrDocPages, stop, skipToLocation, setIsEPUB } = useTTS(); const { setText: setTTSText, currDocPage, currDocPages, setCurrDocPages, stop, skipToLocation, setIsEPUB } = useTTS();
const { authEnabled } = useAuthConfig(); const { authEnabled } = useAuthConfig();
const { id } = useParams(); const { id } = useParams();
@ -1141,8 +1133,7 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
// Context value memoization return useMemo(
const contextValue = useMemo(
() => ({ () => ({
setCurrentDocument, setCurrentDocument,
currDocData, currDocData,
@ -1190,23 +1181,4 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
clearWordHighlights, clearWordHighlights,
] ]
); );
return (
<EPUBContext.Provider value={contextValue}>
{children}
</EPUBContext.Provider>
);
}
/**
* Custom hook to consume the EPUB context
* @returns {EPUBContextType} The EPUB context value
* @throws {Error} When used outside of EPUBProvider
*/
export function useEPUB() {
const context = useContext(EPUBContext);
if (context === undefined) {
throw new Error('useEPUB must be used within an EPUBProvider');
}
return context;
} }

View file

@ -2,13 +2,13 @@
import { useEffect, useRef, useCallback, useState } from 'react'; import { useEffect, useRef, useCallback, useState } from 'react';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useEPUB } from '@/contexts/EPUBContext';
import { useTTS } from '@/contexts/TTSContext'; import { useTTS } from '@/contexts/TTSContext';
import { useConfig } from '@/contexts/ConfigContext'; import { useConfig } from '@/contexts/ConfigContext';
import { DocumentSkeleton } from '@/components/documents/DocumentSkeleton'; import { DocumentSkeleton } from '@/components/documents/DocumentSkeleton';
import { useEPUBTheme, getThemeStyles } from '@/hooks/epub/useEPUBTheme'; import { useEPUBTheme, getThemeStyles } from '@/hooks/epub/useEPUBTheme';
import { useEPUBResize } from '@/hooks/epub/useEPUBResize'; import { useEPUBResize } from '@/hooks/epub/useEPUBResize';
import { DotsVerticalIcon, ChevronLeftIcon, ChevronRightIcon } from '@/components/icons/Icons'; import { DotsVerticalIcon, ChevronLeftIcon, ChevronRightIcon } from '@/components/icons/Icons';
import type { EPUBState } from '@/app/(app)/epub/[id]/useEpubDocument';
const ReactReader = dynamic(() => import('react-reader').then(mod => mod.ReactReader), { const ReactReader = dynamic(() => import('react-reader').then(mod => mod.ReactReader), {
ssr: false, ssr: false,
@ -17,9 +17,29 @@ const ReactReader = dynamic(() => import('react-reader').then(mod => mod.ReactRe
interface EPUBViewerProps { interface EPUBViewerProps {
className?: string; className?: string;
epubState: Pick<
EPUBState,
| 'currDocData'
| 'currDocName'
| 'currDocPage'
| 'currDocPages'
| 'locationRef'
| 'handleLocationChanged'
| 'bookRef'
| 'renditionRef'
| 'tocRef'
| 'setRendition'
| 'extractPageText'
| 'highlightSegment'
| 'clearHighlights'
| 'highlightWordIndex'
| 'clearWordHighlights'
| 'walkUpcomingRenderedLocations'
| 'resolveEpubLocator'
>;
} }
export function EPUBViewer({ className = '' }: EPUBViewerProps) { export function EPUBViewer({ className = '', epubState }: EPUBViewerProps) {
const [isTocOpen, setIsTocOpen] = useState(false); const [isTocOpen, setIsTocOpen] = useState(false);
const { const {
currDocData, currDocData,
@ -39,7 +59,7 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
clearWordHighlights, clearWordHighlights,
walkUpcomingRenderedLocations, walkUpcomingRenderedLocations,
resolveEpubLocator, resolveEpubLocator,
} = useEPUB(); } = epubState;
const { const {
registerLocationChangeHandler, registerLocationChangeHandler,
registerEpubLocationWalker, registerEpubLocationWalker,