Viewer bridge handler. Reduce overengineering as flagged by copilot

This commit is contained in:
Reece 2025-10-31 20:06:11 +00:00
parent 1bc8e7613f
commit fe23952828
4 changed files with 214 additions and 158 deletions

View File

@ -209,7 +209,7 @@ export function ZoomAPIBridge() {
]); ]);
useEffect(() => { useEffect(() => {
if (!zoom || typeof zoom.onZoomChange !== 'function') { if (!zoom) {
return; return;
} }
@ -222,9 +222,7 @@ export function ZoomAPIBridge() {
}); });
return () => { return () => {
if (typeof unsubscribe === 'function') { unsubscribe();
unsubscribe();
}
}; };
}, [zoom, triggerImmediateZoomUpdate]); }, [zoom, triggerImmediateZoomUpdate]);

View File

@ -6,115 +6,35 @@ import React, {
useRef, useRef,
useCallback, useCallback,
} from 'react'; } from 'react';
import { SpreadMode } from '@embedpdf/plugin-spread/react';
import { useNavigation } from '@app/contexts/NavigationContext'; import { useNavigation } from '@app/contexts/NavigationContext';
import {
// Bridge API interfaces - these match what the bridges provide BridgeRef,
interface ScrollAPIWrapper { BridgeApiMap,
scrollToPage: (params: { pageNumber: number }) => void; BridgeStateMap,
scrollToPreviousPage: () => void; BridgeKey,
scrollToNextPage: () => void; ViewerBridgeRegistry,
} createBridgeRegistry,
registerBridge as setBridgeRef,
interface ZoomAPIWrapper { ScrollAPIWrapper,
zoomIn: () => void; ScrollState,
zoomOut: () => void; ZoomAPIWrapper,
toggleMarqueeZoom: () => void; ZoomState,
requestZoom: (level: number) => void; PanAPIWrapper,
} PanState,
SelectionAPIWrapper,
interface PanAPIWrapper { SelectionState,
enable: () => void; SpreadAPIWrapper,
disable: () => void; SpreadState,
toggle: () => void; RotationAPIWrapper,
} RotationState,
SearchAPIWrapper,
interface SelectionAPIWrapper { SearchState,
copyToClipboard: () => void; SearchResult,
getSelectedText: () => string | any; ThumbnailAPIWrapper,
getFormattedSelection: () => any; ExportAPIWrapper,
} ExportState,
} from '@core/contexts/viewer/viewerBridges';
interface SpreadAPIWrapper { import { SpreadMode } from '@embedpdf/plugin-spread/react';
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<any>;
clear: () => void;
next: () => void;
previous: () => void;
}
interface ThumbnailAPIWrapper {
renderThumb: (pageIndex: number, scale: number) => { toPromise: () => Promise<Blob> };
}
interface ExportAPIWrapper {
download: () => void;
saveAsCopy: () => { toPromise: () => Promise<ArrayBuffer> };
}
// 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<TState = unknown, TApi = unknown> {
state: TState;
api: TApi;
}
function useImmediateNotifier<Args extends unknown[]>() { function useImmediateNotifier<Args extends unknown[]>() {
const callbackRef = useRef<((...args: Args) => void) | null>(null); const callbackRef = useRef<((...args: Args) => void) | null>(null);
@ -232,7 +152,7 @@ interface ViewerContextType {
}; };
// Bridge registration - internal use by bridges // Bridge registration - internal use by bridges
registerBridge: (type: string, ref: BridgeRef) => void; registerBridge: (type: BridgeKey, ref: BridgeRef) => void;
} }
export const ViewerContext = createContext<ViewerContextType | null>(null); export const ViewerContext = createContext<ViewerContextType | null>(null);
@ -252,17 +172,7 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
useNavigation(); useNavigation();
// Bridge registry - bridges register their state and APIs here // Bridge registry - bridges register their state and APIs here
const bridgeRefs = useRef({ const bridgeRefs = useRef<ViewerBridgeRegistry>(createBridgeRegistry());
scroll: null as BridgeRef<ScrollState, ScrollAPIWrapper> | null,
zoom: null as BridgeRef<ZoomState, ZoomAPIWrapper> | null,
pan: null as BridgeRef<PanState, PanAPIWrapper> | null,
selection: null as BridgeRef<SelectionState, SelectionAPIWrapper> | null,
search: null as BridgeRef<SearchState, SearchAPIWrapper> | null,
spread: null as BridgeRef<SpreadState, SpreadAPIWrapper> | null,
rotation: null as BridgeRef<RotationState, RotationAPIWrapper> | null,
thumbnail: null as BridgeRef<unknown, ThumbnailAPIWrapper> | null,
export: null as BridgeRef<ExportState, ExportAPIWrapper> | null,
});
const { const {
register: registerImmediateZoomUpdate, register: registerImmediateZoomUpdate,
@ -298,38 +208,15 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
[triggerImmediateSpreadInternal] [triggerImmediateSpreadInternal]
); );
const registerBridge = (type: string, ref: BridgeRef) => { const registerBridge = useCallback(
// Type-safe assignment - we know the bridges will provide correct types <K extends BridgeKey>(
switch (type) { type: K,
case 'scroll': ref: BridgeRef<BridgeStateMap[K], BridgeApiMap[K]>
bridgeRefs.current.scroll = ref as BridgeRef<ScrollState, ScrollAPIWrapper>; ) => {
break; setBridgeRef(bridgeRefs.current, type, ref);
case 'zoom': },
bridgeRefs.current.zoom = ref as BridgeRef<ZoomState, ZoomAPIWrapper>; []
break; );
case 'pan':
bridgeRefs.current.pan = ref as BridgeRef<PanState, PanAPIWrapper>;
break;
case 'selection':
bridgeRefs.current.selection = ref as BridgeRef<SelectionState, SelectionAPIWrapper>;
break;
case 'search':
bridgeRefs.current.search = ref as BridgeRef<SearchState, SearchAPIWrapper>;
break;
case 'spread':
bridgeRefs.current.spread = ref as BridgeRef<SpreadState, SpreadAPIWrapper>;
break;
case 'rotation':
bridgeRefs.current.rotation = ref as BridgeRef<RotationState, RotationAPIWrapper>;
break;
case 'thumbnail':
bridgeRefs.current.thumbnail = ref as BridgeRef<unknown, ThumbnailAPIWrapper>;
break;
case 'export':
bridgeRefs.current.export = ref as BridgeRef<ExportState, ExportAPIWrapper>;
break;
}
};
const toggleThumbnailSidebar = () => { const toggleThumbnailSidebar = () => {
setIsThumbnailSidebarVisible(prev => !prev); setIsThumbnailSidebarVisible(prev => !prev);

View File

@ -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<any>;
clear: () => void;
next: () => void;
previous: () => void;
}
export interface ThumbnailAPIWrapper {
renderThumb: (pageIndex: number, scale: number) => {
toPromise: () => Promise<Blob>;
};
}
export interface ExportAPIWrapper {
download: () => void;
saveAsCopy: () => { toPromise: () => Promise<ArrayBuffer> };
}
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<TState = unknown, TApi = unknown> {
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<BridgeStateMap[K], BridgeApiMap[K]> | 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<K extends BridgeKey>(
registry: ViewerBridgeRegistry,
type: K,
ref: BridgeRef<BridgeStateMap[K], BridgeApiMap[K]>
): void {
registry[type] = ref;
}
export function getBridgeState<K extends BridgeKey>(
registry: ViewerBridgeRegistry,
type: K,
fallback: BridgeStateMap[K]
): BridgeStateMap[K] {
return registry[type]?.state ?? fallback;
}
export function getBridgeApi<K extends BridgeKey>(
registry: ViewerBridgeRegistry,
type: K
): BridgeApiMap[K] | null {
return registry[type]?.api ?? null;
}

View File

@ -167,7 +167,7 @@ export function useFitWidthResize({
return; return;
} }
if (typeof timeoutId === 'number') { if (timeoutId !== undefined) {
window.clearTimeout(timeoutId); window.clearTimeout(timeoutId);
} }
@ -179,7 +179,7 @@ export function useFitWidthResize({
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);
return () => { return () => {
if (typeof timeoutId === 'number') { if (timeoutId !== undefined) {
window.clearTimeout(timeoutId); window.clearTimeout(timeoutId);
} }
window.removeEventListener('resize', handleResize); window.removeEventListener('resize', handleResize);