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 { TTSProvider } from '@/contexts/TTSContext';
import { EPUBProvider } from '@/contexts/EPUBContext';
export default function EpubReaderLayout({ children }: { children: ReactNode }) {
return (
<ConfigProvider>
<TTSProvider>
<EPUBProvider>{children}</EPUBProvider>
{children}
</TTSProvider>
</ConfigProvider>
);

View file

@ -3,7 +3,6 @@
import { useParams, useRouter } from "next/navigation";
import Link from 'next/link';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useEPUB } from '@/contexts/EPUBContext';
import { DocumentSkeleton } from '@/components/documents/DocumentSkeleton';
import { EPUBViewer } from '@/components/views/EPUBViewer';
import { DocumentSettings } from '@/components/documents/DocumentSettings';
@ -20,12 +19,21 @@ import { resolveDocumentId } from '@/lib/client/dexie';
import { RateLimitBanner } from '@/components/auth/RateLimitBanner';
import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
import { useFeatureFlag } from '@/contexts/RuntimeConfigContext';
import { useEPUB } from './useEpubDocument';
export default function EPUBPage() {
const canExportAudiobook = useFeatureFlag('enableAudiobookExport');
const { id } = useParams();
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 { isAtLimit } = useAuthRateLimit();
const [error, setError] = useState<string | null>(null);
@ -209,7 +217,7 @@ export default function EPUBPage() {
</div>
) : (
<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>

View file

@ -1,10 +1,7 @@
'use client';
import {
createContext,
useContext,
useState,
ReactNode,
useCallback,
useMemo,
useRef,
@ -36,7 +33,7 @@ import {
type EpubCanonicalWordToken,
} from '@/lib/client/epub/epub-word-highlight';
import { useParams } from 'next/navigation';
import { useConfig } from './ConfigContext';
import { useConfig } from '@/contexts/ConfigContext';
import type {
EpubRenderedLocationWalker,
TTSSentenceAlignment,
@ -47,7 +44,7 @@ import type { AudiobookGenerationSettings, TTSSegmentLocator } from '@/types/cli
import { isStableEpubLocator } from '@/types/client';
import type { CanonicalTtsSegment } from '@/lib/shared/tts-segment-plan';
interface EPUBContextType {
export interface EPUBState {
currDocData: ArrayBuffer | undefined;
currDocName: string | undefined;
currDocPages: number | undefined;
@ -90,8 +87,6 @@ interface EPUBContextType {
clearWordHighlights: () => void;
}
const EPUBContext = createContext<EPUBContextType | undefined>(undefined);
const EPUB_CONTINUATION_CHARS = 5000;
const stepToNextNode = (node: Node | null, root: Node): Node | null => {
@ -526,12 +521,9 @@ const resolveVisibleSegmentRange = (
/**
* 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
* Route-local EPUB reader hook.
*/
export function EPUBProvider({ children }: { children: ReactNode }) {
export function useEPUB(): EPUBState {
const { setText: setTTSText, currDocPage, currDocPages, setCurrDocPages, stop, skipToLocation, setIsEPUB } = useTTS();
const { authEnabled } = useAuthConfig();
const { id } = useParams();
@ -1141,8 +1133,7 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
// Context value memoization
const contextValue = useMemo(
return useMemo(
() => ({
setCurrentDocument,
currDocData,
@ -1190,23 +1181,4 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
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 dynamic from 'next/dynamic';
import { useEPUB } from '@/contexts/EPUBContext';
import { useTTS } from '@/contexts/TTSContext';
import { useConfig } from '@/contexts/ConfigContext';
import { DocumentSkeleton } from '@/components/documents/DocumentSkeleton';
import { useEPUBTheme, getThemeStyles } from '@/hooks/epub/useEPUBTheme';
import { useEPUBResize } from '@/hooks/epub/useEPUBResize';
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), {
ssr: false,
@ -17,9 +17,29 @@ const ReactReader = dynamic(() => import('react-reader').then(mod => mod.ReactRe
interface EPUBViewerProps {
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 {
currDocData,
@ -39,7 +59,7 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
clearWordHighlights,
walkUpcomingRenderedLocations,
resolveEpubLocator,
} = useEPUB();
} = epubState;
const {
registerLocationChangeHandler,
registerEpubLocationWalker,