Stirling-PDF/frontend/src/core/contexts/ViewerContext.tsx
Reece Browne f29d85565a
Chore/v2/ctrlf (#5217)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-11 12:42:16 +00:00

378 lines
10 KiB
TypeScript

import React, {
createContext,
useContext,
useState,
ReactNode,
useRef,
useCallback,
} from 'react';
import { useNavigation } from '@app/contexts/NavigationContext';
import {
createViewerActions,
ScrollActions,
ZoomActions,
PanActions,
SelectionActions,
SpreadActions,
RotationActions,
SearchActions,
ExportActions,
BookmarkActions,
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,
} from '@app/contexts/viewer/viewerBridges';
import { SpreadMode } from '@embedpdf/plugin-spread/react';
function useImmediateNotifier<Args extends unknown[]>() {
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
*/
interface ViewerContextType {
// UI state managed by this context
isThumbnailSidebarVisible: boolean;
toggleThumbnailSidebar: () => void;
isBookmarkSidebarVisible: boolean;
toggleBookmarkSidebar: () => 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;
toggleAnnotationMode: () => 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;
// 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;
// Internal - for bridges to trigger immediate updates
triggerImmediateScrollUpdate: (currentPage: number, totalPages: number) => void;
triggerImmediateZoomUpdate: (zoomPercent: number) => void;
triggerImmediateSpreadUpdate: (mode: SpreadMode, isDualPage?: 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;
printActions: PrintActions;
// Bridge registration - internal use by bridges
registerBridge: <K extends BridgeKey>(
type: K,
ref: BridgeRef<BridgeStateMap[K], BridgeApiMap[K]>
) => void;
}
export const ViewerContext = createContext<ViewerContextType | null>(null);
interface ViewerProviderProps {
children: ReactNode;
}
export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
// UI state - only state directly managed by this context
const [isThumbnailSidebarVisible, setIsThumbnailSidebarVisible] = useState(false);
const [isBookmarkSidebarVisible, setIsBookmarkSidebarVisible] = useState(false);
const [isSearchInterfaceVisible, setSearchInterfaceVisible] = useState(false);
const [isAnnotationsVisible, setIsAnnotationsVisible] = useState(true);
const [isAnnotationMode, setIsAnnotationModeState] = useState(false);
const [activeFileIndex, setActiveFileIndex] = useState(0);
// 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<ViewerBridgeRegistry>(createBridgeRegistry());
const {
register: registerImmediateZoomUpdate,
trigger: triggerImmediateZoomInternal,
} = useImmediateNotifier<[number]>();
const {
register: registerImmediateScrollUpdate,
trigger: triggerImmediateScrollInternal,
} = useImmediateNotifier<[number, number]>();
const {
register: registerImmediateSpreadUpdate,
trigger: triggerImmediateSpreadInternal,
} = useImmediateNotifier<[SpreadMode, 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 registerBridge = useCallback(
<K extends BridgeKey>(
type: K,
ref: BridgeRef<BridgeStateMap[K], BridgeApiMap[K]>
) => {
setBridgeRef(bridgeRefs.current, type, ref);
},
[]
);
const toggleThumbnailSidebar = () => {
setIsThumbnailSidebarVisible(prev => !prev);
};
const toggleBookmarkSidebar = () => {
setIsBookmarkSidebarVisible(prev => !prev);
};
const searchInterfaceActions = {
open: () => setSearchInterfaceVisible(true),
close: () => setSearchInterfaceVisible(false),
toggle: () => setSearchInterfaceVisible(prev => !prev),
};
const toggleAnnotationsVisibility = () => {
setIsAnnotationsVisible(prev => !prev);
};
const setAnnotationMode = (enabled: boolean) => {
setIsAnnotationModeState(enabled);
};
const toggleAnnotationMode = () => {
setIsAnnotationModeState(prev => !prev);
};
// 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);
// Action handlers - call APIs directly
const {
scrollActions,
zoomActions,
panActions,
selectionActions,
spreadActions,
rotationActions,
searchActions,
exportActions,
bookmarkActions,
printActions,
} = createViewerActions({
registry: bridgeRefs,
getScrollState,
getZoomState,
triggerImmediateZoomUpdate,
});
const value: ViewerContextType = {
// UI state
isThumbnailSidebarVisible,
toggleThumbnailSidebar,
isBookmarkSidebarVisible,
toggleBookmarkSidebar,
// Search interface
isSearchInterfaceVisible,
searchInterfaceActions,
// Annotation controls
isAnnotationsVisible,
toggleAnnotationsVisibility,
isAnnotationMode,
setAnnotationMode,
toggleAnnotationMode,
// Active file index
activeFileIndex,
setActiveFileIndex,
// State getters
getScrollState,
getZoomState,
getPanState,
getSelectionState,
getSpreadState,
getRotationState,
getSearchState,
getThumbnailAPI,
getExportState,
getBookmarkState,
hasBookmarkSupport,
// Immediate updates
registerImmediateZoomUpdate,
registerImmediateScrollUpdate,
registerImmediateSpreadUpdate,
triggerImmediateScrollUpdate,
triggerImmediateZoomUpdate,
triggerImmediateSpreadUpdate,
// Actions
scrollActions,
zoomActions,
panActions,
selectionActions,
spreadActions,
rotationActions,
searchActions,
exportActions,
bookmarkActions,
printActions,
// Bridge registration
registerBridge,
};
return (
<ViewerContext.Provider value={value}>
{children}
</ViewerContext.Provider>
);
};
export const useViewer = (): ViewerContextType => {
const context = useContext(ViewerContext);
if (!context) {
throw new Error('useViewer must be used within a ViewerProvider');
}
return context;
};