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

This commit is contained in:
Richard R 2026-05-13 14:08:47 -06:00
parent 41036e1341
commit 2f067c0a90
4 changed files with 38 additions and 60 deletions

View file

@ -4,14 +4,11 @@ import type { ReactNode } from 'react';
import { ConfigProvider } from '@/contexts/ConfigContext';
import { TTSProvider } from '@/contexts/TTSContext';
import { PDFProvider } from '@/contexts/PDFContext';
export default function PdfReaderLayout({ children }: { children: ReactNode }) {
return (
<ConfigProvider>
<TTSProvider>
<PDFProvider>{children}</PDFProvider>
</TTSProvider>
<TTSProvider>{children}</TTSProvider>
</ConfigProvider>
);
}

View file

@ -1,7 +1,6 @@
'use client';
import dynamic from 'next/dynamic';
import { usePDF } from '@/contexts/PDFContext';
import { useParams, useRouter } from 'next/navigation';
import Link from 'next/link';
import { useCallback, useEffect, useRef, useState } from 'react';
@ -20,6 +19,7 @@ import { resolveDocumentId } from '@/lib/client/dexie';
import { RateLimitBanner } from '@/components/auth/RateLimitBanner';
import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
import { useFeatureFlag } from '@/contexts/RuntimeConfigContext';
import { usePdfDocument } from './usePdfDocument';
// Dynamic import for client-side rendering only
const PDFViewer = dynamic(
@ -34,7 +34,16 @@ export default function PDFViewerPage() {
const canExportAudiobook = useFeatureFlag('enableAudiobookExport');
const { id } = useParams();
const router = useRouter();
const { setCurrentDocument, currDocName, clearCurrDoc, currDocPage, currDocPages, createFullAudioBook: createPDFAudioBook, regenerateChapter: regeneratePDFChapter } = usePDF();
const pdfState = usePdfDocument();
const {
setCurrentDocument,
currDocName,
clearCurrDoc,
currDocPage,
currDocPages,
createFullAudioBook: createPDFAudioBook,
regenerateChapter: regeneratePDFChapter,
} = pdfState;
const { stop } = useTTS();
const { isAtLimit } = useAuthRateLimit();
const [error, setError] = useState<string | null>(null);
@ -197,7 +206,7 @@ export default function PDFViewerPage() {
<DocumentSkeleton />
</div>
) : (
<PDFViewer zoomLevel={zoomLevel} />
<PDFViewer zoomLevel={zoomLevel} pdfState={pdfState} />
)}
</div>
{canExportAudiobook && (

View file

@ -1,23 +1,14 @@
/**
* PDF Context Provider
*
* This module provides a React context for managing PDF document functionality.
* It handles document loading, text extraction, highlighting, and integration with TTS.
*
* Key features:
* - PDF document management (add/remove/load)
* - Text extraction and processing
* - Text highlighting and navigation
* - Document state management
* Route-local PDF document hook.
*
* This module owns PDF document loading, text extraction, highlighting, and
* audiobook integration for the `/pdf/[id]` route.
*/
'use client';
import {
createContext,
useContext,
useState,
ReactNode,
useEffect,
useCallback,
useMemo,
@ -50,9 +41,9 @@ import type { AudiobookGenerationSettings } from '@/types/client';
import { clampSegmentPreloadDepth } from '@/types/config';
/**
* Interface defining all available methods and properties in the PDF context
* Interface defining all available methods and properties for the PDF route.
*/
interface PDFContextType {
export interface PdfDocumentState {
// Current document state
currDocId: string | undefined;
currDocData: ArrayBuffer | undefined;
@ -93,22 +84,13 @@ interface PDFContextType {
isAudioCombining: boolean;
}
// Create the context
const PDFContext = createContext<PDFContextType | undefined>(undefined);
const EMPTY_TEXT_RETRY_DELAY_MS = 120;
const EMPTY_TEXT_MAX_RETRIES = 6;
/**
* PDFProvider Component
*
* 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
* Main PDF route hook.
*/
export function PDFProvider({ children }: { children: ReactNode }) {
export function usePdfDocument(): PdfDocumentState {
const {
setText: setTTSText,
stop,
@ -507,8 +489,7 @@ export function PDFProvider({ children }: { children: ReactNode }) {
};
}, [registerVisualPageChangeHandler, currDocPages, pdfDocument]);
// Context value memoization
const contextValue = useMemo(
return useMemo(
() => ({
onDocumentLoadSuccess,
setCurrentDocument,
@ -544,25 +525,4 @@ export function PDFProvider({ children }: { children: ReactNode }) {
isAudioCombining,
]
);
return (
<PDFContext.Provider value={contextValue}>
{children}
</PDFContext.Provider>
);
}
/**
* Custom hook to consume the PDF context
* Ensures the context is used within a provider
*
* @throws {Error} If used outside of PDFProvider
* @returns {PDFContextType} The PDF context value containing all PDF-related functionality
*/
export function usePDF() {
const context = useContext(PDFContext);
if (context === undefined) {
throw new Error('usePDF must be used within a PDFProvider');
}
return context;
}

View file

@ -7,12 +7,25 @@ import 'react-pdf/dist/Page/AnnotationLayer.css';
import 'react-pdf/dist/Page/TextLayer.css';
import { DocumentSkeleton } from '@/components/documents/DocumentSkeleton';
import { useTTS } from '@/contexts/TTSContext';
import { usePDF } from '@/contexts/PDFContext';
import { useConfig } from '@/contexts/ConfigContext';
import { usePDFResize } from '@/hooks/pdf/usePDFResize';
import type { PdfDocumentState } from '@/app/(app)/pdf/[id]/usePdfDocument';
interface PDFViewerProps {
zoomLevel: number;
pdfState: Pick<
PdfDocumentState,
| 'highlightPattern'
| 'clearHighlights'
| 'clearWordHighlights'
| 'highlightWordIndex'
| 'onDocumentLoadSuccess'
| 'currDocId'
| 'currDocData'
| 'currDocPages'
| 'currDocText'
| 'currDocPage'
>;
}
interface PDFOnLinkClickArgs {
@ -20,7 +33,7 @@ interface PDFOnLinkClickArgs {
dest?: Dest;
}
export function PDFViewer({ zoomLevel }: PDFViewerProps) {
export function PDFViewer({ zoomLevel, pdfState }: PDFViewerProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [isPageRendering, setIsPageRendering] = useState(false);
const scaleRef = useRef<number>(1);
@ -43,7 +56,6 @@ export function PDFViewer({ zoomLevel }: PDFViewerProps) {
skipToLocation,
} = useTTS();
// PDF context
const {
highlightPattern,
clearHighlights,
@ -55,7 +67,7 @@ export function PDFViewer({ zoomLevel }: PDFViewerProps) {
currDocPages,
currDocText,
currDocPage,
} = usePDF();
} = pdfState;
// IMPORTANT:
// - pdf.js may transfer/detach ArrayBuffers when sending them to its worker, so we must clone.