From 2f067c0a90b5e9ef328d0c891abe33618848c68c Mon Sep 17 00:00:00 2001 From: Richard R Date: Wed, 13 May 2026 14:08:47 -0600 Subject: [PATCH] refactor(pdf): move reader state to route-local hook --- src/app/(app)/pdf/[id]/layout.tsx | 5 +- src/app/(app)/pdf/[id]/page.tsx | 15 ++++- .../(app)/pdf/[id]/usePdfDocument.ts} | 58 +++---------------- src/components/views/PDFViewer.tsx | 20 +++++-- 4 files changed, 38 insertions(+), 60 deletions(-) rename src/{contexts/PDFContext.tsx => app/(app)/pdf/[id]/usePdfDocument.ts} (91%) diff --git a/src/app/(app)/pdf/[id]/layout.tsx b/src/app/(app)/pdf/[id]/layout.tsx index f5ac472..01be8bf 100644 --- a/src/app/(app)/pdf/[id]/layout.tsx +++ b/src/app/(app)/pdf/[id]/layout.tsx @@ -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 ( - - {children} - + {children} ); } diff --git a/src/app/(app)/pdf/[id]/page.tsx b/src/app/(app)/pdf/[id]/page.tsx index 8b4d154..1ecd6aa 100644 --- a/src/app/(app)/pdf/[id]/page.tsx +++ b/src/app/(app)/pdf/[id]/page.tsx @@ -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(null); @@ -197,7 +206,7 @@ export default function PDFViewerPage() { ) : ( - + )} {canExportAudiobook && ( diff --git a/src/contexts/PDFContext.tsx b/src/app/(app)/pdf/[id]/usePdfDocument.ts similarity index 91% rename from src/contexts/PDFContext.tsx rename to src/app/(app)/pdf/[id]/usePdfDocument.ts index 7496ebe..63418d1 100644 --- a/src/contexts/PDFContext.tsx +++ b/src/app/(app)/pdf/[id]/usePdfDocument.ts @@ -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(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 ( - - {children} - - ); -} - -/** - * 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; } diff --git a/src/components/views/PDFViewer.tsx b/src/components/views/PDFViewer.tsx index 805bb1c..01c980c 100644 --- a/src/components/views/PDFViewer.tsx +++ b/src/components/views/PDFViewer.tsx @@ -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(null); const [isPageRendering, setIsPageRendering] = useState(false); const scaleRef = useRef(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.