This commit is contained in:
Reece Browne 2025-09-18 02:14:31 +01:00
parent 72375d89d1
commit 312fc2d615
11 changed files with 78 additions and 180 deletions

View File

@ -7,7 +7,6 @@ import { useRightRail } from '../../contexts/RightRailContext';
import { useFileState, useFileSelection, useFileManagement } from '../../contexts/FileContext';
import { useNavigationState } from '../../contexts/NavigationContext';
import { useTranslation } from 'react-i18next';
import '../../types/embedPdf';
import LanguageSelector from '../shared/LanguageSelector';
import { useRainbowThemeContext } from '../shared/RainbowThemeProvider';

View File

@ -9,7 +9,6 @@ import { useViewer } from "../../contexts/ViewerContext";
import { LocalEmbedPDF } from './LocalEmbedPDF';
import { PdfViewerToolbar } from './PdfViewerToolbar';
import { ThumbnailSidebar } from './ThumbnailSidebar';
import '../../types/embedPdf';
export interface EmbedPdfViewerProps {
sidebarsVisible: boolean;
@ -200,7 +199,7 @@ const EmbedPdfViewerContent = ({
currentPage={scrollState.currentPage}
totalPages={scrollState.totalPages}
onPageChange={(page) => {
// Placeholder - will implement page navigation later
// Page navigation handled by scrollActions
console.log('Navigate to page:', page);
}}
dualPage={spreadState.isDualPage}

View File

@ -8,7 +8,6 @@ import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
import LastPageIcon from '@mui/icons-material/LastPage';
import DescriptionIcon from '@mui/icons-material/Description';
import ViewWeekIcon from '@mui/icons-material/ViewWeek';
import '../../types/embedPdf';
interface PdfViewerToolbarProps {
// Page navigation props (placeholders for now)

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useEffect } from 'react';
import { useScroll } from '@embedpdf/plugin-scroll/react';
import { useViewer } from '../../contexts/ViewerContext';
@ -10,11 +10,6 @@ export function ScrollAPIBridge() {
const { provides: scroll, state: scrollState } = useScroll();
const { registerBridge, triggerImmediateScrollUpdate } = useViewer();
const [_localState, setLocalState] = useState({
currentPage: 1,
totalPages: 0
});
useEffect(() => {
if (scroll && scrollState) {
const newState = {
@ -22,15 +17,8 @@ export function ScrollAPIBridge() {
totalPages: scrollState.totalPages,
};
setLocalState(prevState => {
// Only update if state actually changed
if (prevState.currentPage !== newState.currentPage || prevState.totalPages !== newState.totalPages) {
// Trigger immediate update for responsive UI
triggerImmediateScrollUpdate(newState.currentPage, newState.totalPages);
return newState;
}
return prevState;
});
registerBridge('scroll', {
state: newState,

View File

@ -13,8 +13,9 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
const { t } = useTranslation();
const viewerContext = React.useContext(ViewerContext);
const searchResults = viewerContext?.getSearchResults();
const searchActiveIndex = viewerContext?.getSearchActiveIndex();
const searchState = viewerContext?.getSearchState();
const searchResults = searchState?.results;
const searchActiveIndex = searchState?.activeIndex;
const searchActions = viewerContext?.searchActions;
const [searchQuery, setSearchQuery] = useState('');
const [jumpToValue, setJumpToValue] = useState('');

View File

@ -10,18 +10,11 @@ export function SelectionAPIBridge() {
const { registerBridge } = useViewer();
const [hasSelection, setHasSelection] = useState(false);
// Store state locally
const [_localState, setLocalState] = useState({
hasSelection: false
});
useEffect(() => {
if (selection) {
// Update local state
const newState = {
hasSelection
};
setLocalState(newState);
// Register this bridge with ViewerContext
registerBridge('selection', {
@ -38,7 +31,6 @@ export function SelectionAPIBridge() {
const hasText = !!sel;
setHasSelection(hasText);
const updatedState = { hasSelection: hasText };
setLocalState(updatedState);
// Re-register with updated state
registerBridge('selection', {
state: updatedState,

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useEffect } from 'react';
import { useSpread, SpreadMode } from '@embedpdf/plugin-spread/react';
import { useViewer } from '../../contexts/ViewerContext';
@ -9,20 +9,12 @@ export function SpreadAPIBridge() {
const { provides: spread, spreadMode } = useSpread();
const { registerBridge } = useViewer();
// Store state locally
const [_localState, setLocalState] = useState({
spreadMode: SpreadMode.None,
isDualPage: false
});
useEffect(() => {
if (spread) {
// Update local state
const newState = {
spreadMode,
isDualPage: spreadMode !== SpreadMode.None
};
setLocalState(newState);
// Register this bridge with ViewerContext
registerBridge('spread', {

View File

@ -1,7 +1,6 @@
import React, { useState, useEffect } from 'react';
import { Box, ScrollArea } from '@mantine/core';
import { useViewer } from '../../contexts/ViewerContext';
import '../../types/embedPdf';
interface ThumbnailSidebarProps {
visible: boolean;
@ -19,39 +18,25 @@ export function ThumbnailSidebar({ visible, onToggle: _onToggle }: ThumbnailSide
// Generate thumbnails when sidebar becomes visible
useEffect(() => {
if (!visible || scrollState.totalPages === 0) return;
console.log('📄 ThumbnailSidebar useEffect triggered:', {
visible,
thumbnailAPI: !!thumbnailAPI,
totalPages: scrollState.totalPages,
existingThumbnails: Object.keys(thumbnails).length
});
if (!thumbnailAPI) return;
const generateThumbnails = async () => {
console.log('📄 Starting thumbnail generation for', scrollState.totalPages, 'pages');
for (let pageIndex = 0; pageIndex < scrollState.totalPages; pageIndex++) {
if (thumbnails[pageIndex]) continue; // Skip if already generated
try {
console.log('📄 Attempting to generate thumbnail for page', pageIndex + 1);
const thumbTask = thumbnailAPI.renderThumb(pageIndex, 1.0);
console.log('📄 Received thumbTask:', thumbTask);
const thumbTask = (thumbnailAPI as any).renderThumb(pageIndex, 1.0);
// Convert Task to Promise and handle properly
thumbTask.toPromise().then((thumbBlob: Blob) => {
console.log('📄 Thumbnail generated successfully for page', pageIndex + 1, 'blob:', thumbBlob);
const thumbUrl = URL.createObjectURL(thumbBlob);
console.log('📄 Created blob URL:', thumbUrl);
setThumbnails(prev => ({
...prev,
[pageIndex]: thumbUrl
}));
}).catch((error: any) => {
console.error('📄 Failed to generate thumbnail for page', pageIndex + 1, error);
console.error('Failed to generate thumbnail for page', pageIndex + 1, error);
setThumbnails(prev => ({
...prev,
[pageIndex]: 'error'

View File

@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react';
import { useEffect, useRef } from 'react';
import { useZoom } from '@embedpdf/plugin-zoom/react';
import { useViewer } from '../../contexts/ViewerContext';
@ -7,21 +7,14 @@ import { useViewer } from '../../contexts/ViewerContext';
*/
export function ZoomAPIBridge() {
const { provides: zoom, state: zoomState } = useZoom();
const { registerBridge } = useViewer();
const { registerBridge, triggerImmediateZoomUpdate } = useViewer();
const hasSetInitialZoom = useRef(false);
// Store state locally
const [_localState, setLocalState] = useState({
currentZoom: 1.4,
zoomPercent: 140
});
// Set initial zoom once when plugin is ready
useEffect(() => {
if (zoom && !hasSetInitialZoom.current) {
hasSetInitialZoom.current = true;
setTimeout(() => {
console.log('Setting initial zoom to 140%');
zoom.requestZoom(1.4);
}, 50);
}
@ -36,9 +29,8 @@ export function ZoomAPIBridge() {
zoomPercent: Math.round(currentZoomLevel * 100),
};
console.log('ZoomAPIBridge - Raw zoom level:', currentZoomLevel, 'Rounded percent:', newState.zoomPercent);
setLocalState(newState);
// Trigger immediate update for responsive UI
triggerImmediateZoomUpdate(newState.zoomPercent);
// Register this bridge with ViewerContext
registerBridge('zoom', {

View File

@ -1,4 +1,5 @@
import React, { createContext, useContext, useState, ReactNode, useRef } from 'react';
import { SpreadMode } from '@embedpdf/plugin-spread/react';
// State interfaces - represent the shape of data from each bridge
interface ScrollState {
@ -20,7 +21,7 @@ interface SelectionState {
}
interface SpreadState {
spreadMode: any;
spreadMode: SpreadMode;
isDualPage: boolean;
}
@ -28,10 +29,15 @@ interface RotationState {
rotation: number;
}
interface SearchState {
results: any[] | null;
activeIndex: number;
}
// Bridge registration interface - bridges register with state and API
interface BridgeRef {
state: any;
api: any;
interface BridgeRef<TState = unknown, TApi = unknown> {
state: TState;
api: TApi;
}
/**
@ -55,9 +61,8 @@ interface ViewerContextType {
getSelectionState: () => SelectionState;
getSpreadState: () => SpreadState;
getRotationState: () => RotationState;
getSearchResults: () => any[] | null;
getSearchActiveIndex: () => number;
getThumbnailAPI: () => any;
getSearchState: () => SearchState;
getThumbnailAPI: () => unknown;
// Immediate update callbacks
registerImmediateZoomUpdate: (callback: (percent: number) => void) => void;
@ -65,6 +70,7 @@ interface ViewerContextType {
// Internal - for bridges to trigger immediate updates
triggerImmediateScrollUpdate: (currentPage: number, totalPages: number) => void;
triggerImmediateZoomUpdate: (zoomPercent: number) => void;
// Action handlers - call EmbedPDF APIs directly
scrollActions: {
@ -91,12 +97,12 @@ interface ViewerContextType {
selectionActions: {
copyToClipboard: () => void;
getSelectedText: () => string;
getFormattedSelection: () => any;
getFormattedSelection: () => unknown;
};
spreadActions: {
setSpreadMode: (mode: any) => void;
getSpreadMode: () => any;
setSpreadMode: (mode: SpreadMode) => void;
getSpreadMode: () => SpreadMode;
toggleSpreadMode: () => void;
};
@ -130,14 +136,14 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
// 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,
scroll: null as BridgeRef<ScrollState> | null,
zoom: null as BridgeRef<ZoomState> | null,
pan: null as BridgeRef<PanState> | null,
selection: null as BridgeRef<SelectionState> | null,
search: null as BridgeRef<SearchState> | null,
spread: null as BridgeRef<SpreadState> | null,
rotation: null as BridgeRef<RotationState> | null,
thumbnail: null as BridgeRef<unknown> | null,
});
// Immediate zoom callback for responsive display updates
@ -147,7 +153,7 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
const immediateScrollUpdateCallback = useRef<((currentPage: number, totalPages: number) => void) | null>(null);
const registerBridge = (type: string, ref: BridgeRef) => {
bridgeRefs.current[type as keyof typeof bridgeRefs.current] = ref;
(bridgeRefs.current as any)[type] = ref;
};
const toggleThumbnailSidebar = () => {
@ -172,19 +178,15 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
};
const getSpreadState = (): SpreadState => {
return bridgeRefs.current.spread?.state || { spreadMode: null, isDualPage: false };
return bridgeRefs.current.spread?.state || { spreadMode: SpreadMode.None, isDualPage: false };
};
const getRotationState = (): RotationState => {
return bridgeRefs.current.rotation?.state || { rotation: 0 };
};
const getSearchResults = () => {
return bridgeRefs.current.search?.state?.results || null;
};
const getSearchActiveIndex = () => {
return bridgeRefs.current.search?.state?.activeIndex || 0;
const getSearchState = (): SearchState => {
return bridgeRefs.current.search?.state || { results: null, activeIndex: 0 };
};
const getThumbnailAPI = () => {
@ -194,32 +196,32 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
// Action handlers - call APIs directly
const scrollActions = {
scrollToPage: (page: number) => {
const api = bridgeRefs.current.scroll?.api;
const api = bridgeRefs.current.scroll?.api as any;
if (api?.scrollToPage) {
api.scrollToPage({ pageNumber: page });
}
},
scrollToFirstPage: () => {
const api = bridgeRefs.current.scroll?.api;
const api = bridgeRefs.current.scroll?.api as any;
if (api?.scrollToPage) {
api.scrollToPage({ pageNumber: 1 });
}
},
scrollToPreviousPage: () => {
const api = bridgeRefs.current.scroll?.api;
const api = bridgeRefs.current.scroll?.api as any;
if (api?.scrollToPreviousPage) {
api.scrollToPreviousPage();
}
},
scrollToNextPage: () => {
const api = bridgeRefs.current.scroll?.api;
const api = bridgeRefs.current.scroll?.api as any;
if (api?.scrollToNextPage) {
api.scrollToNextPage();
}
},
scrollToLastPage: () => {
const scrollState = getScrollState();
const api = bridgeRefs.current.scroll?.api;
const api = bridgeRefs.current.scroll?.api as any;
if (api?.scrollToPage && scrollState.totalPages > 0) {
api.scrollToPage({ pageNumber: scrollState.totalPages });
}
@ -228,7 +230,7 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
const zoomActions = {
zoomIn: () => {
const api = bridgeRefs.current.zoom?.api;
const api = bridgeRefs.current.zoom?.api as any;
if (api?.zoomIn) {
// Update display immediately if callback is registered
if (immediateZoomUpdateCallback.current) {
@ -240,7 +242,7 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
}
},
zoomOut: () => {
const api = bridgeRefs.current.zoom?.api;
const api = bridgeRefs.current.zoom?.api as any;
if (api?.zoomOut) {
// Update display immediately if callback is registered
if (immediateZoomUpdateCallback.current) {
@ -252,13 +254,13 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
}
},
toggleMarqueeZoom: () => {
const api = bridgeRefs.current.zoom?.api;
const api = bridgeRefs.current.zoom?.api as any;
if (api?.toggleMarqueeZoom) {
api.toggleMarqueeZoom();
}
},
requestZoom: (level: number) => {
const api = bridgeRefs.current.zoom?.api;
const api = bridgeRefs.current.zoom?.api as any;
if (api?.requestZoom) {
api.requestZoom(level);
}
@ -267,19 +269,19 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
const panActions = {
enablePan: () => {
const api = bridgeRefs.current.pan?.api;
const api = bridgeRefs.current.pan?.api as any;
if (api?.enable) {
api.enable();
}
},
disablePan: () => {
const api = bridgeRefs.current.pan?.api;
const api = bridgeRefs.current.pan?.api as any;
if (api?.disable) {
api.disable();
}
},
togglePan: () => {
const api = bridgeRefs.current.pan?.api;
const api = bridgeRefs.current.pan?.api as any;
if (api?.toggle) {
api.toggle();
}
@ -288,20 +290,20 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
const selectionActions = {
copyToClipboard: () => {
const api = bridgeRefs.current.selection?.api;
const api = bridgeRefs.current.selection?.api as any;
if (api?.copyToClipboard) {
api.copyToClipboard();
}
},
getSelectedText: () => {
const api = bridgeRefs.current.selection?.api;
const api = bridgeRefs.current.selection?.api as any;
if (api?.getSelectedText) {
return api.getSelectedText();
}
return '';
},
getFormattedSelection: () => {
const api = bridgeRefs.current.selection?.api;
const api = bridgeRefs.current.selection?.api as any;
if (api?.getFormattedSelection) {
return api.getFormattedSelection();
}
@ -310,21 +312,21 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
};
const spreadActions = {
setSpreadMode: (mode: any) => {
const api = bridgeRefs.current.spread?.api;
setSpreadMode: (mode: SpreadMode) => {
const api = bridgeRefs.current.spread?.api as any;
if (api?.setSpreadMode) {
api.setSpreadMode(mode);
}
},
getSpreadMode: () => {
const api = bridgeRefs.current.spread?.api;
const api = bridgeRefs.current.spread?.api as any;
if (api?.getSpreadMode) {
return api.getSpreadMode();
}
return null;
},
toggleSpreadMode: () => {
const api = bridgeRefs.current.spread?.api;
const api = bridgeRefs.current.spread?.api as any;
if (api?.toggleSpreadMode) {
api.toggleSpreadMode();
}
@ -333,25 +335,25 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
const rotationActions = {
rotateForward: () => {
const api = bridgeRefs.current.rotation?.api;
const api = bridgeRefs.current.rotation?.api as any;
if (api?.rotateForward) {
api.rotateForward();
}
},
rotateBackward: () => {
const api = bridgeRefs.current.rotation?.api;
const api = bridgeRefs.current.rotation?.api as any;
if (api?.rotateBackward) {
api.rotateBackward();
}
},
setRotation: (rotation: number) => {
const api = bridgeRefs.current.rotation?.api;
const api = bridgeRefs.current.rotation?.api as any;
if (api?.setRotation) {
api.setRotation(rotation);
}
},
getRotation: () => {
const api = bridgeRefs.current.rotation?.api;
const api = bridgeRefs.current.rotation?.api as any;
if (api?.getRotation) {
return api.getRotation();
}
@ -361,25 +363,25 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
const searchActions = {
search: async (query: string) => {
const api = bridgeRefs.current.search?.api;
const api = bridgeRefs.current.search?.api as any;
if (api?.search) {
return api.search(query);
}
},
next: () => {
const api = bridgeRefs.current.search?.api;
const api = bridgeRefs.current.search?.api as any;
if (api?.next) {
api.next();
}
},
previous: () => {
const api = bridgeRefs.current.search?.api;
const api = bridgeRefs.current.search?.api as any;
if (api?.previous) {
api.previous();
}
},
clear: () => {
const api = bridgeRefs.current.search?.api;
const api = bridgeRefs.current.search?.api as any;
if (api?.clear) {
api.clear();
}
@ -400,6 +402,12 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
}
};
const triggerImmediateZoomUpdate = (zoomPercent: number) => {
if (immediateZoomUpdateCallback.current) {
immediateZoomUpdateCallback.current(zoomPercent);
}
};
const value: ViewerContextType = {
// UI state
isThumbnailSidebarVisible,
@ -412,14 +420,14 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
getSelectionState,
getSpreadState,
getRotationState,
getSearchResults,
getSearchActiveIndex,
getSearchState,
getThumbnailAPI,
// Immediate updates
registerImmediateZoomUpdate,
registerImmediateScrollUpdate,
triggerImmediateScrollUpdate,
triggerImmediateZoomUpdate,
// Actions
scrollActions,

View File

@ -1,57 +0,0 @@
// Types for EmbedPDF global APIs
export interface EmbedPdfZoomAPI {
zoomPercent: number;
zoomIn: () => void;
zoomOut: () => void;
}
export interface EmbedPdfScrollAPI {
currentPage: number;
totalPages: number;
scrollToPage: (page: number) => void;
scrollToFirstPage: () => void;
scrollToPreviousPage: () => void;
scrollToNextPage: () => void;
scrollToLastPage: () => void;
}
export interface EmbedPdfPanAPI {
isPanning: boolean;
togglePan: () => void;
}
export interface EmbedPdfSpreadAPI {
toggleSpreadMode: () => void;
}
export interface EmbedPdfRotateAPI {
rotateForward: () => void;
rotateBackward: () => void;
setRotation: (rotation: number) => void;
getRotation: () => number;
}
export interface EmbedPdfControlsAPI {
pointer: () => void;
}
export interface EmbedPdfThumbnailAPI {
thumbnailAPI: {
renderThumb: (pageIndex: number, scale: number) => {
toPromise: () => Promise<Blob>;
};
};
}
declare global {
interface Window {
embedPdfZoom?: EmbedPdfZoomAPI;
embedPdfScroll?: EmbedPdfScrollAPI;
embedPdfPan?: EmbedPdfPanAPI;
embedPdfSpread?: EmbedPdfSpreadAPI;
embedPdfRotate?: EmbedPdfRotateAPI;
embedPdfControls?: EmbedPdfControlsAPI;
embedPdfThumbnail?: EmbedPdfThumbnailAPI;
toggleThumbnailSidebar?: () => void;
}
}