import React, { createContext, useContext, useState, ReactNode, useRef, useCallback, } from 'react'; import { useNavigation } from '@app/contexts/NavigationContext'; import { preferencesService, type PdfRenderMode } from '@app/services/preferencesService'; import { createViewerActions, ScrollActions, ZoomActions, PanActions, SelectionActions, SpreadActions, RotationActions, SearchActions, ExportActions, BookmarkActions, AttachmentActions, PrintActions, } from '@app/contexts/viewer/viewerActions'; import { BridgeRef, BridgeApiMap, BridgeStateMap, BridgeKey, ViewerBridgeRegistry, createBridgeRegistry, registerBridge as setBridgeRef, ScrollState, ZoomState, PanState, SelectionState, SpreadState, RotationState, SearchState, ExportState, ThumbnailAPIWrapper, BookmarkState, AttachmentState, DocumentPermissionsState, PdfPermissionFlag, } from '@app/contexts/viewer/viewerBridges'; import { SpreadMode } from '@embedpdf/plugin-spread/react'; function useImmediateNotifier() { const callbacksRef = useRef(new Set<(...args: Args) => void>()); const register = useCallback((callback: (...args: Args) => void) => { callbacksRef.current.add(callback); return () => { callbacksRef.current.delete(callback); }; }, []); const trigger = useCallback((...args: Args) => { callbacksRef.current.forEach(cb => { try { cb(...args); } catch (error) { console.error('Immediate callback error:', error); } }); }, []); return { register, trigger }; } /** * ViewerContext provides a unified interface to EmbedPDF functionality. * * Architecture: * - Bridges store their own state locally and register with this context * - Context provides read-only access to bridge state via getter functions * - Actions call EmbedPDF APIs directly through bridge references * - No circular dependencies - bridges don't call back into this context */ export interface ViewerContextType { // UI state managed by this context isThumbnailSidebarVisible: boolean; toggleThumbnailSidebar: () => void; isBookmarkSidebarVisible: boolean; toggleBookmarkSidebar: () => void; isAttachmentSidebarVisible: boolean; toggleAttachmentSidebar: () => void; isCommentsSidebarVisible: boolean; setCommentsSidebarVisible: (visible: boolean) => void; toggleCommentsSidebar: () => void; /** Request focus or highlight of a comment card in the sidebar (opens sidebar, then scrolls + flashes or focuses input). */ highlightCommentRequest: { documentId: string; pageIndex: number; annotationId: string; action: 'focus' | 'highlight'; } | null; requestCommentFocus: (documentId: string, pageIndex: number, annotationId: string, hasContent: boolean) => void; clearHighlightCommentRequest: () => void; // Search interface visibility isSearchInterfaceVisible: boolean; searchInterfaceActions: { open: () => void; close: () => void; toggle: () => void; }; // Annotation visibility toggle isAnnotationsVisible: boolean; toggleAnnotationsVisibility: () => void; // Annotation/drawing mode for viewer isAnnotationMode: boolean; setAnnotationMode: (enabled: boolean) => void; // Active file index for multi-file viewing activeFileIndex: number; setActiveFileIndex: (index: number) => void; // State getters - read current state from bridges getScrollState: () => ScrollState; getZoomState: () => ZoomState; getPanState: () => PanState; getSelectionState: () => SelectionState; getSpreadState: () => SpreadState; getRotationState: () => RotationState; getSearchState: () => SearchState; getThumbnailAPI: () => ThumbnailAPIWrapper | null; getExportState: () => ExportState; getBookmarkState: () => BookmarkState; hasBookmarkSupport: () => boolean; getAttachmentState: () => AttachmentState; hasAttachmentSupport: () => boolean; getDocumentPermissions: () => DocumentPermissionsState; hasPermission: (flag: PdfPermissionFlag) => boolean; // Immediate update callbacks registerImmediateZoomUpdate: (callback: (percent: number) => void) => () => void; registerImmediateScrollUpdate: (callback: (currentPage: number, totalPages: number) => void) => () => void; registerImmediateSpreadUpdate: (callback: (mode: SpreadMode, isDualPage: boolean) => void) => () => void; registerImmediatePanUpdate: (callback: (isPanning: boolean) => void) => () => void; // Internal - for bridges to trigger immediate updates triggerImmediateScrollUpdate: (currentPage: number, totalPages: number) => void; triggerImmediateZoomUpdate: (zoomPercent: number) => void; triggerImmediateSpreadUpdate: (mode: SpreadMode, isDualPage?: boolean) => void; triggerImmediatePanUpdate: (isPanning: boolean) => void; // Action handlers - call EmbedPDF APIs directly scrollActions: ScrollActions; zoomActions: ZoomActions; panActions: PanActions; selectionActions: SelectionActions; spreadActions: SpreadActions; rotationActions: RotationActions; searchActions: SearchActions; exportActions: ExportActions; bookmarkActions: BookmarkActions; attachmentActions: AttachmentActions; printActions: PrintActions; // Bridge registration - internal use by bridges registerBridge: ( type: K, ref: BridgeRef | null ) => void; // Save changes function - registered by EmbedPdfViewer applyChanges: (() => Promise) | null; setApplyChanges: (fn: (() => Promise) | null) => void; // PDF page color rendering mode (viewer-only, never modifies the PDF) pdfRenderMode: PdfRenderMode; cyclePdfRenderMode: () => void; } export const ViewerContext = createContext(null); interface ViewerProviderProps { children: ReactNode; } export const ViewerProvider: React.FC = ({ children }) => { // UI state - only state directly managed by this context const [isThumbnailSidebarVisible, setIsThumbnailSidebarVisible] = useState(false); const [isBookmarkSidebarVisible, setIsBookmarkSidebarVisible] = useState(false); const [isAttachmentSidebarVisible, setIsAttachmentSidebarVisible] = useState(false); const [isCommentsSidebarVisible, setIsCommentsSidebarVisible] = useState(false); const [highlightCommentRequest, setHighlightCommentRequest] = useState<{ documentId: string; pageIndex: number; annotationId: string; action: 'focus' | 'highlight'; } | null>(null); const [isSearchInterfaceVisible, setSearchInterfaceVisible] = useState(false); const [isAnnotationsVisible, setIsAnnotationsVisible] = useState(true); const [isAnnotationMode, setIsAnnotationModeState] = useState(false); const [activeFileIndex, setActiveFileIndex] = useState(0); const [pdfRenderMode, setPdfRenderModeState] = useState( () => preferencesService.getPreference('pdfRenderMode') ); // Get current navigation state to check if we're in sign mode useNavigation(); // Bridge registry - bridges register their state and APIs here const bridgeRefs = useRef(createBridgeRegistry()); // Apply changes function - registered by EmbedPdfViewer const applyChangesRef = useRef<(() => Promise) | null>(null); const setApplyChanges = useCallback((fn: (() => Promise) | null) => { applyChangesRef.current = fn; }, []); const applyChanges = useCallback(async () => { if (applyChangesRef.current) { await applyChangesRef.current(); } }, []); const { register: registerImmediateZoomUpdate, trigger: triggerImmediateZoomInternal, } = useImmediateNotifier<[number]>(); const { register: registerImmediateScrollUpdate, trigger: triggerImmediateScrollInternal, } = useImmediateNotifier<[number, number]>(); const { register: registerImmediateSpreadUpdate, trigger: triggerImmediateSpreadInternal, } = useImmediateNotifier<[SpreadMode, boolean]>(); const { register: registerImmediatePanUpdate, trigger: triggerImmediatePanInternal, } = useImmediateNotifier<[boolean]>(); const triggerImmediateZoomUpdate = useCallback( (percent: number) => { triggerImmediateZoomInternal(percent); }, [triggerImmediateZoomInternal] ); const triggerImmediateScrollUpdate = useCallback( (currentPage: number, totalPages: number) => { triggerImmediateScrollInternal(currentPage, totalPages); }, [triggerImmediateScrollInternal] ); const triggerImmediateSpreadUpdate = useCallback( (mode: SpreadMode, isDualPage: boolean = mode !== SpreadMode.None) => { triggerImmediateSpreadInternal(mode, isDualPage); }, [triggerImmediateSpreadInternal] ); const triggerImmediatePanUpdate = useCallback( (isPanning: boolean) => { triggerImmediatePanInternal(isPanning); }, [triggerImmediatePanInternal] ); const registerBridge = useCallback( ( type: K, ref: BridgeRef | null ) => { setBridgeRef(bridgeRefs.current, type, ref); }, [] ); const toggleThumbnailSidebar = () => { setIsThumbnailSidebarVisible(prev => !prev); }; const toggleBookmarkSidebar = () => { setIsBookmarkSidebarVisible(prev => !prev); }; const toggleAttachmentSidebar = () => { setIsAttachmentSidebarVisible(prev => !prev); }; const setCommentsSidebarVisible = (visible: boolean) => { setIsCommentsSidebarVisible(visible); }; const toggleCommentsSidebar = () => { setIsCommentsSidebarVisible(prev => !prev); }; const requestCommentFocus = useCallback( (documentId: string, pageIndex: number, annotationId: string, hasContent: boolean) => { setIsCommentsSidebarVisible(true); setHighlightCommentRequest({ documentId, pageIndex, annotationId, action: hasContent ? 'highlight' : 'focus', }); }, [] ); const clearHighlightCommentRequest = useCallback(() => { setHighlightCommentRequest(null); }, []); const searchInterfaceActions = { open: () => setSearchInterfaceVisible(true), close: () => setSearchInterfaceVisible(false), toggle: () => setSearchInterfaceVisible(prev => !prev), }; const toggleAnnotationsVisibility = () => { setIsAnnotationsVisible(prev => !prev); }; const setAnnotationMode = (enabled: boolean) => { setIsAnnotationModeState(enabled); }; const cyclePdfRenderMode = useCallback(() => { setPdfRenderModeState(prev => { const next: PdfRenderMode = prev === 'normal' ? 'dark' : prev === 'dark' ? 'sepia' : 'normal'; preferencesService.setPreference('pdfRenderMode', next); return next; }); }, []); // State getters - read from bridge refs const getScrollState = (): ScrollState => { return bridgeRefs.current.scroll?.state || { currentPage: 1, totalPages: 0 }; }; const getZoomState = (): ZoomState => { return bridgeRefs.current.zoom?.state || { currentZoom: 1.4, zoomPercent: 140 }; }; const getPanState = (): PanState => { return bridgeRefs.current.pan?.state || { isPanning: false }; }; const getSelectionState = (): SelectionState => { return bridgeRefs.current.selection?.state || { hasSelection: false }; }; const getSpreadState = (): SpreadState => { return bridgeRefs.current.spread?.state || { spreadMode: SpreadMode.None, isDualPage: false }; }; const getRotationState = (): RotationState => { return bridgeRefs.current.rotation?.state || { rotation: 0 }; }; const getSearchState = (): SearchState => { return bridgeRefs.current.search?.state || { results: null, activeIndex: 0 }; }; const getThumbnailAPI = () => { return bridgeRefs.current.thumbnail?.api || null; }; const getExportState = (): ExportState => { return bridgeRefs.current.export?.state || { canExport: false }; }; const getBookmarkState = (): BookmarkState => { return ( bridgeRefs.current.bookmark?.state || { bookmarks: null, isLoading: false, error: null, } ); }; const hasBookmarkSupport = () => Boolean(bridgeRefs.current.bookmark); const getAttachmentState = (): AttachmentState => { return ( bridgeRefs.current.attachment?.state || { attachments: null, isLoading: false, error: null, } ); }; const hasAttachmentSupport = () => Boolean(bridgeRefs.current.attachment); const getDocumentPermissions = (): DocumentPermissionsState => { return bridgeRefs.current.permissions?.state || { isEncrypted: false, isOwnerUnlocked: false, permissions: PdfPermissionFlag.AllowAll, canPrint: true, canModifyContents: true, canCopyContents: true, canModifyAnnotations: true, canFillForms: true, canExtractForAccessibility: true, canAssembleDocument: true, canPrintHighQuality: true, }; }; const hasPermission = (flag: PdfPermissionFlag): boolean => { const api = bridgeRefs.current.permissions?.api; if (api?.hasPermission) { return api.hasPermission(flag); } // Default: allow all permissions - warn in development if (process.env.NODE_ENV === 'development') { console.warn('[ViewerContext] Permissions API not available, defaulting to allow'); } return true; }; // Action handlers - call APIs directly const { scrollActions, zoomActions, panActions, selectionActions, spreadActions, rotationActions, searchActions, exportActions, bookmarkActions, attachmentActions, printActions, } = createViewerActions({ registry: bridgeRefs, getScrollState, getZoomState, triggerImmediateZoomUpdate, }); const value: ViewerContextType = { // UI state isThumbnailSidebarVisible, toggleThumbnailSidebar, isBookmarkSidebarVisible, toggleBookmarkSidebar, isAttachmentSidebarVisible, toggleAttachmentSidebar, isCommentsSidebarVisible, setCommentsSidebarVisible, toggleCommentsSidebar, highlightCommentRequest, requestCommentFocus, clearHighlightCommentRequest, // Search interface isSearchInterfaceVisible, searchInterfaceActions, // Annotation controls isAnnotationsVisible, toggleAnnotationsVisibility, isAnnotationMode, setAnnotationMode, // Active file index activeFileIndex, setActiveFileIndex, // State getters getScrollState, getZoomState, getPanState, getSelectionState, getSpreadState, getRotationState, getSearchState, getThumbnailAPI, getExportState, getBookmarkState, hasBookmarkSupport, getAttachmentState, hasAttachmentSupport, getDocumentPermissions, hasPermission, // Immediate updates registerImmediateZoomUpdate, registerImmediateScrollUpdate, registerImmediateSpreadUpdate, registerImmediatePanUpdate, triggerImmediateScrollUpdate, triggerImmediateZoomUpdate, triggerImmediateSpreadUpdate, triggerImmediatePanUpdate, // Actions scrollActions, zoomActions, panActions, selectionActions, spreadActions, rotationActions, searchActions, exportActions, bookmarkActions, attachmentActions, printActions, // Bridge registration registerBridge, // Apply changes applyChanges, setApplyChanges, // PDF page rendering mode pdfRenderMode, cyclePdfRenderMode, }; return ( {children} ); }; export const useViewer = (): ViewerContextType => { const context = useContext(ViewerContext); if (!context) { throw new Error('useViewer must be used within a ViewerProvider'); } return context; };