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

View file

@ -1,7 +1,6 @@
'use client'; 'use client';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { usePDF } from '@/contexts/PDFContext';
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';
@ -20,6 +19,7 @@ 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 { usePdfDocument } from './usePdfDocument';
// Dynamic import for client-side rendering only // Dynamic import for client-side rendering only
const PDFViewer = dynamic( const PDFViewer = dynamic(
@ -34,7 +34,16 @@ export default function PDFViewerPage() {
const canExportAudiobook = useFeatureFlag('enableAudiobookExport'); const canExportAudiobook = useFeatureFlag('enableAudiobookExport');
const { id } = useParams(); const { id } = useParams();
const router = useRouter(); 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 { stop } = useTTS();
const { isAtLimit } = useAuthRateLimit(); const { isAtLimit } = useAuthRateLimit();
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -197,7 +206,7 @@ export default function PDFViewerPage() {
<DocumentSkeleton /> <DocumentSkeleton />
</div> </div>
) : ( ) : (
<PDFViewer zoomLevel={zoomLevel} /> <PDFViewer zoomLevel={zoomLevel} pdfState={pdfState} />
)} )}
</div> </div>
{canExportAudiobook && ( {canExportAudiobook && (

View file

@ -1,23 +1,14 @@
/** /**
* PDF Context Provider * Route-local PDF document hook.
* *
* This module provides a React context for managing PDF document functionality. * This module owns PDF document loading, text extraction, highlighting, and
* It handles document loading, text extraction, highlighting, and integration with TTS. * audiobook integration for the `/pdf/[id]` route.
*
* Key features:
* - PDF document management (add/remove/load)
* - Text extraction and processing
* - Text highlighting and navigation
* - Document state management
*/ */
'use client'; 'use client';
import { import {
createContext,
useContext,
useState, useState,
ReactNode,
useEffect, useEffect,
useCallback, useCallback,
useMemo, useMemo,
@ -50,9 +41,9 @@ import type { AudiobookGenerationSettings } from '@/types/client';
import { clampSegmentPreloadDepth } from '@/types/config'; 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 // Current document state
currDocId: string | undefined; currDocId: string | undefined;
currDocData: ArrayBuffer | undefined; currDocData: ArrayBuffer | undefined;
@ -93,22 +84,13 @@ interface PDFContextType {
isAudioCombining: boolean; isAudioCombining: boolean;
} }
// Create the context
const PDFContext = createContext<PDFContextType | undefined>(undefined);
const EMPTY_TEXT_RETRY_DELAY_MS = 120; const EMPTY_TEXT_RETRY_DELAY_MS = 120;
const EMPTY_TEXT_MAX_RETRIES = 6; const EMPTY_TEXT_MAX_RETRIES = 6;
/** /**
* PDFProvider Component * Main PDF route hook.
*
* 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 }) { export function usePdfDocument(): PdfDocumentState {
const { const {
setText: setTTSText, setText: setTTSText,
stop, stop,
@ -507,8 +489,7 @@ export function PDFProvider({ children }: { children: ReactNode }) {
}; };
}, [registerVisualPageChangeHandler, currDocPages, pdfDocument]); }, [registerVisualPageChangeHandler, currDocPages, pdfDocument]);
// Context value memoization return useMemo(
const contextValue = useMemo(
() => ({ () => ({
onDocumentLoadSuccess, onDocumentLoadSuccess,
setCurrentDocument, setCurrentDocument,
@ -544,25 +525,4 @@ export function PDFProvider({ children }: { children: ReactNode }) {
isAudioCombining, 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 'react-pdf/dist/Page/TextLayer.css';
import { DocumentSkeleton } from '@/components/documents/DocumentSkeleton'; import { DocumentSkeleton } from '@/components/documents/DocumentSkeleton';
import { useTTS } from '@/contexts/TTSContext'; import { useTTS } from '@/contexts/TTSContext';
import { usePDF } from '@/contexts/PDFContext';
import { useConfig } from '@/contexts/ConfigContext'; import { useConfig } from '@/contexts/ConfigContext';
import { usePDFResize } from '@/hooks/pdf/usePDFResize'; import { usePDFResize } from '@/hooks/pdf/usePDFResize';
import type { PdfDocumentState } from '@/app/(app)/pdf/[id]/usePdfDocument';
interface PDFViewerProps { interface PDFViewerProps {
zoomLevel: number; zoomLevel: number;
pdfState: Pick<
PdfDocumentState,
| 'highlightPattern'
| 'clearHighlights'
| 'clearWordHighlights'
| 'highlightWordIndex'
| 'onDocumentLoadSuccess'
| 'currDocId'
| 'currDocData'
| 'currDocPages'
| 'currDocText'
| 'currDocPage'
>;
} }
interface PDFOnLinkClickArgs { interface PDFOnLinkClickArgs {
@ -20,7 +33,7 @@ interface PDFOnLinkClickArgs {
dest?: Dest; dest?: Dest;
} }
export function PDFViewer({ zoomLevel }: PDFViewerProps) { export function PDFViewer({ zoomLevel, pdfState }: PDFViewerProps) {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const [isPageRendering, setIsPageRendering] = useState(false); const [isPageRendering, setIsPageRendering] = useState(false);
const scaleRef = useRef<number>(1); const scaleRef = useRef<number>(1);
@ -43,7 +56,6 @@ export function PDFViewer({ zoomLevel }: PDFViewerProps) {
skipToLocation, skipToLocation,
} = useTTS(); } = useTTS();
// PDF context
const { const {
highlightPattern, highlightPattern,
clearHighlights, clearHighlights,
@ -55,7 +67,7 @@ export function PDFViewer({ zoomLevel }: PDFViewerProps) {
currDocPages, currDocPages,
currDocText, currDocText,
currDocPage, currDocPage,
} = usePDF(); } = pdfState;
// IMPORTANT: // IMPORTANT:
// - pdf.js may transfer/detach ArrayBuffers when sending them to its worker, so we must clone. // - pdf.js may transfer/detach ArrayBuffers when sending them to its worker, so we must clone.