diff --git a/frontend/src/core/components/viewer/ZoomAPIBridge.tsx b/frontend/src/core/components/viewer/ZoomAPIBridge.tsx index 9cec9e9da..ddc362e2b 100644 --- a/frontend/src/core/components/viewer/ZoomAPIBridge.tsx +++ b/frontend/src/core/components/viewer/ZoomAPIBridge.tsx @@ -209,7 +209,7 @@ export function ZoomAPIBridge() { ]); useEffect(() => { - if (!zoom || typeof zoom.onZoomChange !== 'function') { + if (!zoom) { return; } @@ -222,9 +222,7 @@ export function ZoomAPIBridge() { }); return () => { - if (typeof unsubscribe === 'function') { - unsubscribe(); - } + unsubscribe(); }; }, [zoom, triggerImmediateZoomUpdate]); diff --git a/frontend/src/core/contexts/ViewerContext.tsx b/frontend/src/core/contexts/ViewerContext.tsx index 076cf86f7..44c3b741e 100644 --- a/frontend/src/core/contexts/ViewerContext.tsx +++ b/frontend/src/core/contexts/ViewerContext.tsx @@ -6,115 +6,35 @@ import React, { useRef, useCallback, } from 'react'; -import { SpreadMode } from '@embedpdf/plugin-spread/react'; import { useNavigation } from '@app/contexts/NavigationContext'; - -// Bridge API interfaces - these match what the bridges provide -interface ScrollAPIWrapper { - scrollToPage: (params: { pageNumber: number }) => void; - scrollToPreviousPage: () => void; - scrollToNextPage: () => void; -} - -interface ZoomAPIWrapper { - zoomIn: () => void; - zoomOut: () => void; - toggleMarqueeZoom: () => void; - requestZoom: (level: number) => void; -} - -interface PanAPIWrapper { - enable: () => void; - disable: () => void; - toggle: () => void; -} - -interface SelectionAPIWrapper { - copyToClipboard: () => void; - getSelectedText: () => string | any; - getFormattedSelection: () => any; -} - -interface SpreadAPIWrapper { - setSpreadMode: (mode: SpreadMode) => void; - getSpreadMode: () => SpreadMode | null; - toggleSpreadMode: () => void; -} - -interface RotationAPIWrapper { - rotateForward: () => void; - rotateBackward: () => void; - setRotation: (rotation: number) => void; - getRotation: () => number; -} - -interface SearchAPIWrapper { - search: (query: string) => Promise; - clear: () => void; - next: () => void; - previous: () => void; -} - -interface ThumbnailAPIWrapper { - renderThumb: (pageIndex: number, scale: number) => { toPromise: () => Promise }; -} - -interface ExportAPIWrapper { - download: () => void; - saveAsCopy: () => { toPromise: () => Promise }; -} - - -// State interfaces - represent the shape of data from each bridge -interface ScrollState { - currentPage: number; - totalPages: number; -} - -interface ZoomState { - currentZoom: number; - zoomPercent: number; -} - -interface PanState { - isPanning: boolean; -} - -interface SelectionState { - hasSelection: boolean; -} - -interface SpreadState { - spreadMode: SpreadMode; - isDualPage: boolean; -} - -interface RotationState { - rotation: number; -} - -interface SearchResult { - pageIndex: number; - rects: Array<{ - origin: { x: number; y: number }; - size: { width: number; height: number }; - }>; -} - -interface SearchState { - results: SearchResult[] | null; - activeIndex: number; -} - -interface ExportState { - canExport: boolean; -} - -// Bridge registration interface - bridges register with state and API -interface BridgeRef { - state: TState; - api: TApi; -} +import { + BridgeRef, + BridgeApiMap, + BridgeStateMap, + BridgeKey, + ViewerBridgeRegistry, + createBridgeRegistry, + registerBridge as setBridgeRef, + ScrollAPIWrapper, + ScrollState, + ZoomAPIWrapper, + ZoomState, + PanAPIWrapper, + PanState, + SelectionAPIWrapper, + SelectionState, + SpreadAPIWrapper, + SpreadState, + RotationAPIWrapper, + RotationState, + SearchAPIWrapper, + SearchState, + SearchResult, + ThumbnailAPIWrapper, + ExportAPIWrapper, + ExportState, +} from '@core/contexts/viewer/viewerBridges'; +import { SpreadMode } from '@embedpdf/plugin-spread/react'; function useImmediateNotifier() { const callbackRef = useRef<((...args: Args) => void) | null>(null); @@ -232,7 +152,7 @@ interface ViewerContextType { }; // Bridge registration - internal use by bridges - registerBridge: (type: string, ref: BridgeRef) => void; + registerBridge: (type: BridgeKey, ref: BridgeRef) => void; } export const ViewerContext = createContext(null); @@ -252,17 +172,7 @@ export const ViewerProvider: React.FC = ({ children }) => { useNavigation(); // Bridge registry - bridges register their state and APIs here - const bridgeRefs = useRef({ - scroll: null as BridgeRef | null, - zoom: null as BridgeRef | null, - pan: null as BridgeRef | null, - selection: null as BridgeRef | null, - search: null as BridgeRef | null, - spread: null as BridgeRef | null, - rotation: null as BridgeRef | null, - thumbnail: null as BridgeRef | null, - export: null as BridgeRef | null, - }); + const bridgeRefs = useRef(createBridgeRegistry()); const { register: registerImmediateZoomUpdate, @@ -298,38 +208,15 @@ export const ViewerProvider: React.FC = ({ children }) => { [triggerImmediateSpreadInternal] ); - const registerBridge = (type: string, ref: BridgeRef) => { - // Type-safe assignment - we know the bridges will provide correct types - switch (type) { - case 'scroll': - bridgeRefs.current.scroll = ref as BridgeRef; - break; - case 'zoom': - bridgeRefs.current.zoom = ref as BridgeRef; - break; - case 'pan': - bridgeRefs.current.pan = ref as BridgeRef; - break; - case 'selection': - bridgeRefs.current.selection = ref as BridgeRef; - break; - case 'search': - bridgeRefs.current.search = ref as BridgeRef; - break; - case 'spread': - bridgeRefs.current.spread = ref as BridgeRef; - break; - case 'rotation': - bridgeRefs.current.rotation = ref as BridgeRef; - break; - case 'thumbnail': - bridgeRefs.current.thumbnail = ref as BridgeRef; - break; - case 'export': - bridgeRefs.current.export = ref as BridgeRef; - break; - } - }; + const registerBridge = useCallback( + ( + type: K, + ref: BridgeRef + ) => { + setBridgeRef(bridgeRefs.current, type, ref); + }, + [] + ); const toggleThumbnailSidebar = () => { setIsThumbnailSidebarVisible(prev => !prev); diff --git a/frontend/src/core/contexts/viewer/viewerBridges.ts b/frontend/src/core/contexts/viewer/viewerBridges.ts new file mode 100644 index 000000000..25b7fa1e4 --- /dev/null +++ b/frontend/src/core/contexts/viewer/viewerBridges.ts @@ -0,0 +1,171 @@ +import { SpreadMode } from '@embedpdf/plugin-spread/react'; + +export interface ScrollAPIWrapper { + scrollToPage: (params: { pageNumber: number }) => void; + scrollToPreviousPage: () => void; + scrollToNextPage: () => void; +} + +export interface ZoomAPIWrapper { + zoomIn: () => void; + zoomOut: () => void; + toggleMarqueeZoom: () => void; + requestZoom: (level: number) => void; +} + +export interface PanAPIWrapper { + enable: () => void; + disable: () => void; + toggle: () => void; +} + +export interface SelectionAPIWrapper { + copyToClipboard: () => void; + getSelectedText: () => string | any; + getFormattedSelection: () => any; +} + +export interface SpreadAPIWrapper { + setSpreadMode: (mode: SpreadMode) => void; + getSpreadMode: () => SpreadMode | null; + toggleSpreadMode: () => void; +} + +export interface RotationAPIWrapper { + rotateForward: () => void; + rotateBackward: () => void; + setRotation: (rotation: number) => void; + getRotation: () => number; +} + +export interface SearchAPIWrapper { + search: (query: string) => Promise; + clear: () => void; + next: () => void; + previous: () => void; +} + +export interface ThumbnailAPIWrapper { + renderThumb: (pageIndex: number, scale: number) => { + toPromise: () => Promise; + }; +} + +export interface ExportAPIWrapper { + download: () => void; + saveAsCopy: () => { toPromise: () => Promise }; +} + +export interface ScrollState { + currentPage: number; + totalPages: number; +} + +export interface ZoomState { + currentZoom: number; + zoomPercent: number; +} + +export interface PanState { + isPanning: boolean; +} + +export interface SelectionState { + hasSelection: boolean; +} + +export interface SpreadState { + spreadMode: SpreadMode; + isDualPage: boolean; +} + +export interface RotationState { + rotation: number; +} + +export interface SearchResult { + pageIndex: number; + rects: Array<{ + origin: { x: number; y: number }; + size: { width: number; height: number }; + }>; +} + +export interface SearchState { + results: SearchResult[] | null; + activeIndex: number; +} + +export interface ExportState { + canExport: boolean; +} + +export interface BridgeRef { + state: TState; + api: TApi; +} + +export interface BridgeStateMap { + scroll: ScrollState; + zoom: ZoomState; + pan: PanState; + selection: SelectionState; + spread: SpreadState; + rotation: RotationState; + search: SearchState; + thumbnail: unknown; + export: ExportState; +} + +export interface BridgeApiMap { + scroll: ScrollAPIWrapper; + zoom: ZoomAPIWrapper; + pan: PanAPIWrapper; + selection: SelectionAPIWrapper; + spread: SpreadAPIWrapper; + rotation: RotationAPIWrapper; + search: SearchAPIWrapper; + thumbnail: ThumbnailAPIWrapper; + export: ExportAPIWrapper; +} + +export type BridgeKey = keyof BridgeStateMap; + +export type ViewerBridgeRegistry = { + [K in BridgeKey]: BridgeRef | null; +}; + +export const createBridgeRegistry = (): ViewerBridgeRegistry => ({ + scroll: null, + zoom: null, + pan: null, + selection: null, + spread: null, + rotation: null, + search: null, + thumbnail: null, + export: null, +}); + +export function registerBridge( + registry: ViewerBridgeRegistry, + type: K, + ref: BridgeRef +): void { + registry[type] = ref; +} + +export function getBridgeState( + registry: ViewerBridgeRegistry, + type: K, + fallback: BridgeStateMap[K] +): BridgeStateMap[K] { + return registry[type]?.state ?? fallback; +} + +export function getBridgeApi( + registry: ViewerBridgeRegistry, + type: K +): BridgeApiMap[K] | null { + return registry[type]?.api ?? null; +} diff --git a/frontend/src/core/utils/viewerZoom.ts b/frontend/src/core/utils/viewerZoom.ts index d185398c0..1fa1fb492 100644 --- a/frontend/src/core/utils/viewerZoom.ts +++ b/frontend/src/core/utils/viewerZoom.ts @@ -167,7 +167,7 @@ export function useFitWidthResize({ return; } - if (typeof timeoutId === 'number') { + if (timeoutId !== undefined) { window.clearTimeout(timeoutId); } @@ -179,7 +179,7 @@ export function useFitWidthResize({ window.addEventListener('resize', handleResize); return () => { - if (typeof timeoutId === 'number') { + if (timeoutId !== undefined) { window.clearTimeout(timeoutId); } window.removeEventListener('resize', handleResize);