mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-18 01:19:24 +00:00
Improved Structure with context at root
This commit is contained in:
parent
3755bfde34
commit
9b5c50db07
@ -2286,7 +2286,12 @@
|
||||
"downloadSelected": "Download Selected Files",
|
||||
"downloadAll": "Download All",
|
||||
"toggleTheme": "Toggle Theme",
|
||||
"language": "Language"
|
||||
"language": "Language",
|
||||
"search": "Search PDF",
|
||||
"panMode": "Pan Mode",
|
||||
"rotateLeft": "Rotate Left",
|
||||
"rotateRight": "Rotate Right",
|
||||
"toggleSidebar": "Toggle Sidebar"
|
||||
},
|
||||
"toolPicker": {
|
||||
"searchPlaceholder": "Search tools...",
|
||||
|
@ -13,6 +13,7 @@ import "./styles/tailwind.css";
|
||||
import "./styles/cookieconsent.css";
|
||||
import "./index.css";
|
||||
import { RightRailProvider } from "./contexts/RightRailContext";
|
||||
import { ViewerProvider } from "./contexts/ViewerContext";
|
||||
|
||||
// Import file ID debugging helpers (development only)
|
||||
import "./utils/fileIdSafety";
|
||||
@ -43,9 +44,11 @@ export default function App() {
|
||||
<FilesModalProvider>
|
||||
<ToolWorkflowProvider>
|
||||
<SidebarProvider>
|
||||
<RightRailProvider>
|
||||
<HomePage />
|
||||
</RightRailProvider>
|
||||
<ViewerProvider>
|
||||
<RightRailProvider>
|
||||
<HomePage />
|
||||
</RightRailProvider>
|
||||
</ViewerProvider>
|
||||
</SidebarProvider>
|
||||
</ToolWorkflowProvider>
|
||||
</FilesModalProvider>
|
||||
|
@ -14,11 +14,15 @@ import { useRainbowThemeContext } from '../shared/RainbowThemeProvider';
|
||||
import { Tooltip } from '../shared/Tooltip';
|
||||
import BulkSelectionPanel from '../pageEditor/BulkSelectionPanel';
|
||||
import { SearchInterface } from '../viewer/SearchInterface';
|
||||
import { ViewerContext } from '../../contexts/ViewerContext';
|
||||
|
||||
export default function RightRail() {
|
||||
const { t } = useTranslation();
|
||||
const [isPanning, setIsPanning] = useState(false);
|
||||
const [currentRotation, setCurrentRotation] = useState(0);
|
||||
const [_currentRotation, setCurrentRotation] = useState(0);
|
||||
|
||||
// Viewer context for PDF controls - safely handle when not available
|
||||
const viewerContext = React.useContext(ViewerContext);
|
||||
const { toggleTheme } = useRainbowThemeContext();
|
||||
const { buttons, actions } = useRightRail();
|
||||
const topButtons = useMemo(() => buttons.filter(b => (b.section || 'top') === 'top' && (b.visible ?? true)), [buttons]);
|
||||
@ -32,23 +36,13 @@ export default function RightRail() {
|
||||
// Navigation view
|
||||
const { workbench: currentView } = useNavigationState();
|
||||
|
||||
// Sync rotation state with EmbedPDF API
|
||||
// Update rotation display when switching to viewer mode
|
||||
useEffect(() => {
|
||||
if (currentView === 'viewer' && window.embedPdfRotate) {
|
||||
const updateRotation = () => {
|
||||
const rotation = window.embedPdfRotate?.getRotation() || 0;
|
||||
setCurrentRotation(rotation * 90); // Convert enum to degrees
|
||||
};
|
||||
|
||||
// Update rotation immediately
|
||||
updateRotation();
|
||||
|
||||
// Set up periodic updates to keep state in sync
|
||||
const interval = setInterval(updateRotation, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
if (currentView === 'viewer' && viewerContext) {
|
||||
const rotationState = viewerContext.getRotationState();
|
||||
setCurrentRotation((rotationState?.rotation ?? 0) * 90);
|
||||
}
|
||||
}, [currentView]);
|
||||
}, [currentView, viewerContext]);
|
||||
|
||||
// File state and selection
|
||||
const { state, selectors } = useFileState();
|
||||
@ -269,7 +263,7 @@ export default function RightRail() {
|
||||
radius="md"
|
||||
className="right-rail-icon"
|
||||
onClick={() => {
|
||||
window.embedPdfPan?.togglePan();
|
||||
viewerContext?.panActions.togglePan();
|
||||
setIsPanning(!isPanning);
|
||||
}}
|
||||
disabled={currentView !== 'viewer'}
|
||||
@ -285,7 +279,7 @@ export default function RightRail() {
|
||||
radius="md"
|
||||
className="right-rail-icon"
|
||||
onClick={() => {
|
||||
window.embedPdfRotate?.rotateBackward();
|
||||
viewerContext?.rotationActions.rotateBackward();
|
||||
}}
|
||||
disabled={currentView !== 'viewer'}
|
||||
>
|
||||
@ -300,7 +294,7 @@ export default function RightRail() {
|
||||
radius="md"
|
||||
className="right-rail-icon"
|
||||
onClick={() => {
|
||||
window.embedPdfRotate?.rotateForward();
|
||||
viewerContext?.rotationActions.rotateForward();
|
||||
}}
|
||||
disabled={currentView !== 'viewer'}
|
||||
>
|
||||
@ -314,7 +308,9 @@ export default function RightRail() {
|
||||
variant="subtle"
|
||||
radius="md"
|
||||
className="right-rail-icon"
|
||||
onClick={() => window.toggleThumbnailSidebar?.()}
|
||||
onClick={() => {
|
||||
viewerContext?.toggleThumbnailSidebar();
|
||||
}}
|
||||
disabled={currentView !== 'viewer'}
|
||||
>
|
||||
<LocalIcon icon="view-list" width="1.5rem" height="1.5rem" />
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { useSearch } from '@embedpdf/plugin-search/react';
|
||||
import { useViewer } from '../../contexts/ViewerContext';
|
||||
|
||||
interface SearchLayerProps {
|
||||
pageIndex: number;
|
||||
@ -32,6 +33,7 @@ export function CustomSearchLayer({
|
||||
borderRadius = 4
|
||||
}: SearchLayerProps) {
|
||||
const { provides: searchProvides } = useSearch();
|
||||
const { scrollActions } = useViewer();
|
||||
const [searchResultState, setSearchResultState] = useState<SearchResultState | null>(null);
|
||||
|
||||
// Subscribe to search result state changes
|
||||
@ -41,27 +43,13 @@ export function CustomSearchLayer({
|
||||
}
|
||||
|
||||
const unsubscribe = searchProvides.onSearchResultStateChange?.((state: SearchResultState) => {
|
||||
// Expose search results globally for SearchInterface
|
||||
if (state?.results) {
|
||||
(window as any).currentSearchResults = state.results;
|
||||
(window as any).currentActiveIndex = (state.activeResultIndex || 0) + 1; // Convert to 1-based index
|
||||
|
||||
// Auto-scroll to active result if we have one
|
||||
if (state.activeResultIndex !== undefined && state.activeResultIndex >= 0) {
|
||||
const activeResult = state.results[state.activeResultIndex];
|
||||
if (activeResult) {
|
||||
// Use the scroll API to navigate to the page containing the active result
|
||||
const scrollAPI = (window as any).embedPdfScroll;
|
||||
if (scrollAPI && scrollAPI.scrollToPage) {
|
||||
// Convert 0-based page index to 1-based page number
|
||||
const pageNumber = activeResult.pageIndex + 1;
|
||||
scrollAPI.scrollToPage(pageNumber);
|
||||
}
|
||||
}
|
||||
// Auto-scroll to active search result
|
||||
if (state?.results && state.activeResultIndex !== undefined && state.activeResultIndex >= 0) {
|
||||
const activeResult = state.results[state.activeResultIndex];
|
||||
if (activeResult) {
|
||||
const pageNumber = activeResult.pageIndex + 1; // Convert to 1-based page number
|
||||
scrollActions.scrollToPage(pageNumber);
|
||||
}
|
||||
} else {
|
||||
(window as any).currentSearchResults = null;
|
||||
(window as any).currentActiveIndex = 0;
|
||||
}
|
||||
|
||||
setSearchResultState(state);
|
||||
|
@ -5,7 +5,7 @@ import CloseIcon from '@mui/icons-material/Close';
|
||||
|
||||
import { useFileState } from "../../contexts/FileContext";
|
||||
import { useFileWithUrl } from "../../hooks/useFileWithUrl";
|
||||
import { ViewerProvider, useViewer } from "../../contexts/ViewerContext";
|
||||
import { useViewer } from "../../contexts/ViewerContext";
|
||||
import { LocalEmbedPDF } from './LocalEmbedPDF';
|
||||
import { PdfViewerToolbar } from './PdfViewerToolbar';
|
||||
import { ThumbnailSidebar } from './ThumbnailSidebar';
|
||||
@ -28,7 +28,8 @@ const EmbedPdfViewerContent = ({
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const viewerRef = React.useRef<HTMLDivElement>(null);
|
||||
const [isViewerHovered, setIsViewerHovered] = React.useState(false);
|
||||
const { isThumbnailSidebarVisible, toggleThumbnailSidebar } = useViewer();
|
||||
const { isThumbnailSidebarVisible, toggleThumbnailSidebar, zoomActions, spreadActions, panActions: _panActions, rotationActions: _rotationActions } = useViewer();
|
||||
|
||||
|
||||
// Get current file from FileContext
|
||||
const { selectors } = useFileState();
|
||||
@ -68,15 +69,12 @@ const EmbedPdfViewerContent = ({
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const zoomAPI = window.embedPdfZoom;
|
||||
if (zoomAPI) {
|
||||
if (event.deltaY < 0) {
|
||||
// Scroll up - zoom in
|
||||
zoomAPI.zoomIn();
|
||||
} else {
|
||||
// Scroll down - zoom out
|
||||
zoomAPI.zoomOut();
|
||||
}
|
||||
if (event.deltaY < 0) {
|
||||
// Scroll up - zoom in
|
||||
zoomActions.zoomIn();
|
||||
} else {
|
||||
// Scroll down - zoom out
|
||||
zoomActions.zoomOut();
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -97,17 +95,14 @@ const EmbedPdfViewerContent = ({
|
||||
|
||||
// Check if Ctrl (Windows/Linux) or Cmd (Mac) is pressed
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
const zoomAPI = window.embedPdfZoom;
|
||||
if (zoomAPI) {
|
||||
if (event.key === '=' || event.key === '+') {
|
||||
// Ctrl+= or Ctrl++ for zoom in
|
||||
event.preventDefault();
|
||||
zoomAPI.zoomIn();
|
||||
} else if (event.key === '-' || event.key === '_') {
|
||||
// Ctrl+- for zoom out
|
||||
event.preventDefault();
|
||||
zoomAPI.zoomOut();
|
||||
}
|
||||
if (event.key === '=' || event.key === '+') {
|
||||
// Ctrl+= or Ctrl++ for zoom in
|
||||
event.preventDefault();
|
||||
zoomActions.zoomIn();
|
||||
} else if (event.key === '-' || event.key === '_') {
|
||||
// Ctrl+- for zoom out
|
||||
event.preventDefault();
|
||||
zoomActions.zoomOut();
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -118,14 +113,6 @@ const EmbedPdfViewerContent = ({
|
||||
};
|
||||
}, [isViewerHovered]);
|
||||
|
||||
// Expose toggle functions globally for right rail buttons
|
||||
React.useEffect(() => {
|
||||
window.toggleThumbnailSidebar = toggleThumbnailSidebar;
|
||||
|
||||
return () => {
|
||||
delete window.toggleThumbnailSidebar;
|
||||
};
|
||||
}, [toggleThumbnailSidebar]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@ -210,7 +197,7 @@ const EmbedPdfViewerContent = ({
|
||||
}}
|
||||
dualPage={false}
|
||||
onDualPageToggle={() => {
|
||||
window.embedPdfSpread?.toggleSpreadMode();
|
||||
spreadActions.toggleSpreadMode();
|
||||
}}
|
||||
currentZoom={100}
|
||||
/>
|
||||
@ -230,11 +217,7 @@ const EmbedPdfViewerContent = ({
|
||||
};
|
||||
|
||||
const EmbedPdfViewer = (props: EmbedPdfViewerProps) => {
|
||||
return (
|
||||
<ViewerProvider>
|
||||
<EmbedPdfViewerContent {...props} />
|
||||
</ViewerProvider>
|
||||
);
|
||||
return <EmbedPdfViewerContent {...props} />;
|
||||
};
|
||||
|
||||
export default EmbedPdfViewer;
|
@ -1,47 +1,45 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { usePan } from '@embedpdf/plugin-pan/react';
|
||||
import { useViewer } from '../../contexts/ViewerContext';
|
||||
|
||||
/**
|
||||
* Component that runs inside EmbedPDF context and bridges pan controls to global window
|
||||
* Component that runs inside EmbedPDF context and updates pan state in ViewerContext
|
||||
*/
|
||||
export function PanAPIBridge() {
|
||||
const { provides: pan, isPanning } = usePan();
|
||||
const [panStateListeners, setPanStateListeners] = useState<Array<(isPanning: boolean) => void>>([]);
|
||||
const { registerBridge } = useViewer();
|
||||
|
||||
// Store state locally
|
||||
const [_localState, setLocalState] = useState({
|
||||
isPanning: false
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (pan) {
|
||||
// Export pan controls to global window for right rail access
|
||||
(window as any).embedPdfPan = {
|
||||
enablePan: () => {
|
||||
console.log('EmbedPDF: Enabling pan mode');
|
||||
pan.enablePan();
|
||||
},
|
||||
disablePan: () => {
|
||||
console.log('EmbedPDF: Disabling pan mode');
|
||||
pan.disablePan();
|
||||
},
|
||||
togglePan: () => {
|
||||
pan.togglePan();
|
||||
},
|
||||
makePanDefault: () => pan.makePanDefault(),
|
||||
get isPanning() { return isPanning; }, // Use getter to always return current value
|
||||
// Subscribe to pan state changes for reactive UI
|
||||
onPanStateChange: (callback: (isPanning: boolean) => void) => {
|
||||
setPanStateListeners(prev => [...prev, callback]);
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
setPanStateListeners(prev => prev.filter(cb => cb !== callback));
|
||||
};
|
||||
},
|
||||
// Update local state
|
||||
const newState = {
|
||||
isPanning
|
||||
};
|
||||
setLocalState(newState);
|
||||
|
||||
// Register this bridge with ViewerContext
|
||||
registerBridge('pan', {
|
||||
state: newState,
|
||||
api: {
|
||||
enable: () => {
|
||||
pan.enablePan();
|
||||
},
|
||||
disable: () => {
|
||||
pan.disablePan();
|
||||
},
|
||||
toggle: () => {
|
||||
pan.togglePan();
|
||||
},
|
||||
makePanDefault: () => pan.makePanDefault(),
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [pan, isPanning]);
|
||||
|
||||
// Notify all listeners when isPanning state changes
|
||||
useEffect(() => {
|
||||
panStateListeners.forEach(callback => callback(isPanning));
|
||||
}, [isPanning, panStateListeners]);
|
||||
}, [pan, isPanning, registerBridge]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button, Paper, Group, NumberInput } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useViewer } from '../../contexts/ViewerContext';
|
||||
import FirstPageIcon from '@mui/icons-material/FirstPage';
|
||||
import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';
|
||||
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
|
||||
@ -19,104 +20,60 @@ interface PdfViewerToolbarProps {
|
||||
dualPage?: boolean;
|
||||
onDualPageToggle?: () => void;
|
||||
|
||||
// Zoom controls (will connect to window.embedPdfZoom)
|
||||
// Zoom controls (connected via ViewerContext)
|
||||
currentZoom?: number;
|
||||
}
|
||||
|
||||
export function PdfViewerToolbar({
|
||||
currentPage = 1,
|
||||
totalPages = 1,
|
||||
totalPages: _totalPages = 1,
|
||||
onPageChange,
|
||||
dualPage = false,
|
||||
onDualPageToggle,
|
||||
currentZoom = 100,
|
||||
currentZoom: _currentZoom = 100,
|
||||
}: PdfViewerToolbarProps) {
|
||||
const { t } = useTranslation();
|
||||
const [pageInput, setPageInput] = useState(currentPage);
|
||||
const [dynamicZoom, setDynamicZoom] = useState(currentZoom);
|
||||
const [dynamicPage, setDynamicPage] = useState(currentPage);
|
||||
const [dynamicTotalPages, setDynamicTotalPages] = useState(totalPages);
|
||||
const { getScrollState, getZoomState, scrollActions, zoomActions } = useViewer();
|
||||
|
||||
const scrollState = getScrollState();
|
||||
const zoomState = getZoomState();
|
||||
const [pageInput, setPageInput] = useState(scrollState.currentPage || currentPage);
|
||||
|
||||
// Update zoom and scroll state from EmbedPDF APIs
|
||||
// Update page input when scroll state changes
|
||||
useEffect(() => {
|
||||
const updateState = () => {
|
||||
// Update zoom
|
||||
if (window.embedPdfZoom) {
|
||||
const zoomPercent = window.embedPdfZoom.zoomPercent || currentZoom;
|
||||
setDynamicZoom(zoomPercent);
|
||||
}
|
||||
|
||||
// Update scroll/page state
|
||||
if (window.embedPdfScroll) {
|
||||
const currentPageNum = window.embedPdfScroll.currentPage || currentPage;
|
||||
const totalPagesNum = window.embedPdfScroll.totalPages || totalPages;
|
||||
setDynamicPage(currentPageNum);
|
||||
setDynamicTotalPages(totalPagesNum);
|
||||
setPageInput(currentPageNum);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// Update state immediately
|
||||
updateState();
|
||||
|
||||
// Set up periodic updates to keep state in sync
|
||||
const interval = setInterval(updateState, 200);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [currentZoom, currentPage, totalPages]);
|
||||
setPageInput(scrollState.currentPage);
|
||||
}, [scrollState.currentPage]);
|
||||
|
||||
const handleZoomOut = () => {
|
||||
if (window.embedPdfZoom) {
|
||||
window.embedPdfZoom.zoomOut();
|
||||
}
|
||||
zoomActions.zoomOut();
|
||||
};
|
||||
|
||||
const handleZoomIn = () => {
|
||||
if (window.embedPdfZoom) {
|
||||
window.embedPdfZoom.zoomIn();
|
||||
}
|
||||
zoomActions.zoomIn();
|
||||
};
|
||||
|
||||
const handlePageNavigation = (page: number) => {
|
||||
if (window.embedPdfScroll) {
|
||||
window.embedPdfScroll.scrollToPage(page);
|
||||
} else if (onPageChange) {
|
||||
scrollActions.scrollToPage(page);
|
||||
if (onPageChange) {
|
||||
onPageChange(page);
|
||||
}
|
||||
setPageInput(page);
|
||||
};
|
||||
|
||||
const handleFirstPage = () => {
|
||||
if (window.embedPdfScroll) {
|
||||
window.embedPdfScroll.scrollToFirstPage();
|
||||
} else {
|
||||
handlePageNavigation(1);
|
||||
}
|
||||
scrollActions.scrollToFirstPage();
|
||||
};
|
||||
|
||||
const handlePreviousPage = () => {
|
||||
if (window.embedPdfScroll) {
|
||||
window.embedPdfScroll.scrollToPreviousPage();
|
||||
} else {
|
||||
handlePageNavigation(Math.max(1, dynamicPage - 1));
|
||||
}
|
||||
scrollActions.scrollToPreviousPage();
|
||||
};
|
||||
|
||||
const handleNextPage = () => {
|
||||
if (window.embedPdfScroll) {
|
||||
window.embedPdfScroll.scrollToNextPage();
|
||||
} else {
|
||||
handlePageNavigation(Math.min(dynamicTotalPages, dynamicPage + 1));
|
||||
}
|
||||
scrollActions.scrollToNextPage();
|
||||
};
|
||||
|
||||
const handleLastPage = () => {
|
||||
if (window.embedPdfScroll) {
|
||||
window.embedPdfScroll.scrollToLastPage();
|
||||
} else {
|
||||
handlePageNavigation(dynamicTotalPages);
|
||||
}
|
||||
scrollActions.scrollToLastPage();
|
||||
};
|
||||
|
||||
return (
|
||||
@ -146,7 +103,7 @@ export function PdfViewerToolbar({
|
||||
px={8}
|
||||
radius="xl"
|
||||
onClick={handleFirstPage}
|
||||
disabled={dynamicPage === 1}
|
||||
disabled={scrollState.currentPage === 1}
|
||||
style={{ minWidth: '2.5rem' }}
|
||||
title={t("viewer.firstPage", "First Page")}
|
||||
>
|
||||
@ -161,7 +118,7 @@ export function PdfViewerToolbar({
|
||||
px={8}
|
||||
radius="xl"
|
||||
onClick={handlePreviousPage}
|
||||
disabled={dynamicPage === 1}
|
||||
disabled={scrollState.currentPage === 1}
|
||||
style={{ minWidth: '2.5rem' }}
|
||||
title={t("viewer.previousPage", "Previous Page")}
|
||||
>
|
||||
@ -174,12 +131,12 @@ export function PdfViewerToolbar({
|
||||
onChange={(value) => {
|
||||
const page = Number(value);
|
||||
setPageInput(page);
|
||||
if (!isNaN(page) && page >= 1 && page <= dynamicTotalPages) {
|
||||
if (!isNaN(page) && page >= 1 && page <= scrollState.totalPages) {
|
||||
handlePageNavigation(page);
|
||||
}
|
||||
}}
|
||||
min={1}
|
||||
max={dynamicTotalPages}
|
||||
max={scrollState.totalPages}
|
||||
hideControls
|
||||
styles={{
|
||||
input: { width: 48, textAlign: "center", fontWeight: 500, fontSize: 16 },
|
||||
@ -187,7 +144,7 @@ export function PdfViewerToolbar({
|
||||
/>
|
||||
|
||||
<span style={{ fontWeight: 500, fontSize: 16 }}>
|
||||
/ {dynamicTotalPages}
|
||||
/ {scrollState.totalPages}
|
||||
</span>
|
||||
|
||||
{/* Next Page Button */}
|
||||
@ -198,7 +155,7 @@ export function PdfViewerToolbar({
|
||||
px={8}
|
||||
radius="xl"
|
||||
onClick={handleNextPage}
|
||||
disabled={dynamicPage === dynamicTotalPages}
|
||||
disabled={scrollState.currentPage === scrollState.totalPages}
|
||||
style={{ minWidth: '2.5rem' }}
|
||||
title={t("viewer.nextPage", "Next Page")}
|
||||
>
|
||||
@ -213,7 +170,7 @@ export function PdfViewerToolbar({
|
||||
px={8}
|
||||
radius="xl"
|
||||
onClick={handleLastPage}
|
||||
disabled={dynamicPage === dynamicTotalPages}
|
||||
disabled={scrollState.currentPage === scrollState.totalPages}
|
||||
style={{ minWidth: '2.5rem' }}
|
||||
title={t("viewer.lastPage", "Last Page")}
|
||||
>
|
||||
@ -247,7 +204,7 @@ export function PdfViewerToolbar({
|
||||
−
|
||||
</Button>
|
||||
<span style={{ minWidth: '2.5rem', textAlign: "center" }}>
|
||||
{dynamicZoom}%
|
||||
{zoomState.zoomPercent}%
|
||||
</span>
|
||||
<Button
|
||||
variant="subtle"
|
||||
|
@ -1,23 +1,39 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRotate } from '@embedpdf/plugin-rotate/react';
|
||||
import { useViewer } from '../../contexts/ViewerContext';
|
||||
|
||||
/**
|
||||
* Component that runs inside EmbedPDF context and exports rotate controls globally
|
||||
* Component that runs inside EmbedPDF context and updates rotation state in ViewerContext
|
||||
*/
|
||||
export function RotateAPIBridge() {
|
||||
const { provides: rotate, rotation } = useRotate();
|
||||
const { registerBridge } = useViewer();
|
||||
|
||||
// Store state locally
|
||||
const [_localState, setLocalState] = useState({
|
||||
rotation: 0
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (rotate) {
|
||||
// Export rotate controls to global window for right rail access
|
||||
window.embedPdfRotate = {
|
||||
rotateForward: () => rotate.rotateForward(),
|
||||
rotateBackward: () => rotate.rotateBackward(),
|
||||
setRotation: (rotationValue: number) => rotate.setRotation(rotationValue),
|
||||
getRotation: () => rotation,
|
||||
// Update local state
|
||||
const newState = {
|
||||
rotation
|
||||
};
|
||||
setLocalState(newState);
|
||||
|
||||
// Register this bridge with ViewerContext
|
||||
registerBridge('rotation', {
|
||||
state: newState,
|
||||
api: {
|
||||
rotateForward: () => rotate.rotateForward(),
|
||||
rotateBackward: () => rotate.rotateBackward(),
|
||||
setRotation: (rotationValue: number) => rotate.setRotation(rotationValue),
|
||||
getRotation: () => rotation,
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [rotate, rotation]);
|
||||
}, [rotate, rotation, registerBridge]);
|
||||
|
||||
return null;
|
||||
}
|
@ -1,27 +1,34 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useScroll } from '@embedpdf/plugin-scroll/react';
|
||||
import { useViewer } from '../../contexts/ViewerContext';
|
||||
|
||||
/**
|
||||
* Component that runs inside EmbedPDF context and exports scroll controls globally
|
||||
* ScrollAPIBridge manages scroll state and exposes scroll actions.
|
||||
* Registers with ViewerContext to provide scroll functionality to UI components.
|
||||
*/
|
||||
export function ScrollAPIBridge() {
|
||||
const { provides: scroll, state: scrollState } = useScroll();
|
||||
const { registerBridge } = useViewer();
|
||||
|
||||
const [_localState, setLocalState] = useState({
|
||||
currentPage: 1,
|
||||
totalPages: 0
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (scroll && scrollState) {
|
||||
// Export scroll controls to global window for toolbar access
|
||||
(window as any).embedPdfScroll = {
|
||||
scrollToPage: (page: number) => scroll.scrollToPage({ pageNumber: page }),
|
||||
scrollToNextPage: () => scroll.scrollToNextPage(),
|
||||
scrollToPreviousPage: () => scroll.scrollToPreviousPage(),
|
||||
scrollToFirstPage: () => scroll.scrollToPage({ pageNumber: 1 }),
|
||||
scrollToLastPage: () => scroll.scrollToPage({ pageNumber: scrollState.totalPages }),
|
||||
const newState = {
|
||||
currentPage: scrollState.currentPage,
|
||||
totalPages: scrollState.totalPages,
|
||||
};
|
||||
setLocalState(newState);
|
||||
|
||||
registerBridge('scroll', {
|
||||
state: newState,
|
||||
api: scroll
|
||||
});
|
||||
}
|
||||
}, [scroll, scrollState]);
|
||||
}, [scroll, scrollState, registerBridge]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
@ -1,52 +1,55 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSearch } from '@embedpdf/plugin-search/react';
|
||||
import { useViewer } from '../../contexts/ViewerContext';
|
||||
|
||||
/**
|
||||
* Component that runs inside EmbedPDF context and bridges search controls to global window
|
||||
* SearchAPIBridge manages search state and provides search functionality.
|
||||
* Listens for search result changes from EmbedPDF and maintains local state.
|
||||
*/
|
||||
export function SearchAPIBridge() {
|
||||
const { provides: search, state } = useSearch();
|
||||
const { provides: search } = useSearch();
|
||||
const { registerBridge } = useViewer();
|
||||
|
||||
const [localState, setLocalState] = useState({
|
||||
results: null as any[] | null,
|
||||
activeIndex: 0
|
||||
});
|
||||
|
||||
// Subscribe to search result changes from EmbedPDF
|
||||
useEffect(() => {
|
||||
if (search && state) {
|
||||
if (!search) return;
|
||||
|
||||
// Export search controls to global window for toolbar access
|
||||
(window as any).embedPdfSearch = {
|
||||
search: async (query: string) => {
|
||||
try {
|
||||
const unsubscribe = search.onSearchResultStateChange?.((state: any) => {
|
||||
setLocalState({
|
||||
results: state?.results || null,
|
||||
activeIndex: (state?.activeResultIndex || 0) + 1 // Convert to 1-based index
|
||||
});
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [search]);
|
||||
|
||||
// Register bridge whenever search API or state changes
|
||||
useEffect(() => {
|
||||
if (search) {
|
||||
registerBridge('search', {
|
||||
state: localState,
|
||||
api: {
|
||||
search: async (query: string) => {
|
||||
search.startSearch();
|
||||
const results = await search.searchAllPages(query);
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error('Search error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
clearSearch: () => {
|
||||
search.stopSearch();
|
||||
},
|
||||
nextResult: () => {
|
||||
return search.nextResult();
|
||||
},
|
||||
previousResult: () => {
|
||||
return search.previousResult();
|
||||
},
|
||||
goToResult: (index: number) => {
|
||||
return search.goToResult(index);
|
||||
},
|
||||
// State getters
|
||||
getSearchQuery: () => state.query,
|
||||
isActive: () => state.active,
|
||||
isLoading: () => state.loading,
|
||||
// Current state for UI updates
|
||||
state: state,
|
||||
// Debug info
|
||||
searchAPI: search,
|
||||
availableMethods: search ? Object.keys(search) : [],
|
||||
};
|
||||
|
||||
return search.searchAllPages(query);
|
||||
},
|
||||
clear: () => {
|
||||
search.stopSearch();
|
||||
setLocalState({ results: null, activeIndex: 0 });
|
||||
},
|
||||
next: () => search.nextResult(),
|
||||
previous: () => search.previousResult(),
|
||||
goToResult: (index: number) => search.goToResult(index),
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [search, state]);
|
||||
}, [search, localState, registerBridge]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { Box, TextInput, ActionIcon, Text, Group } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LocalIcon } from '../shared/LocalIcon';
|
||||
import { ViewerContext } from '../../contexts/ViewerContext';
|
||||
|
||||
interface SearchInterfaceProps {
|
||||
visible: boolean;
|
||||
@ -10,6 +11,11 @@ interface SearchInterfaceProps {
|
||||
|
||||
export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
||||
const { t } = useTranslation();
|
||||
const viewerContext = React.useContext(ViewerContext);
|
||||
|
||||
const searchResults = viewerContext?.getSearchResults();
|
||||
const searchActiveIndex = viewerContext?.getSearchActiveIndex();
|
||||
const searchActions = viewerContext?.searchActions;
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [jumpToValue, setJumpToValue] = useState('');
|
||||
const [resultInfo, setResultInfo] = useState<{
|
||||
@ -24,26 +30,24 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
||||
if (!visible) return;
|
||||
|
||||
const checkSearchState = () => {
|
||||
const searchAPI = (window as any).embedPdfSearch;
|
||||
if (searchAPI) {
|
||||
const state = searchAPI.state;
|
||||
|
||||
if (state && state.query && state.active) {
|
||||
// Try to get result info from the global search data
|
||||
// The CustomSearchLayer stores results, let's try to access them
|
||||
const searchResults = (window as any).currentSearchResults;
|
||||
const activeIndex = (window as any).currentActiveIndex || 1;
|
||||
// Use ViewerContext state instead of window APIs
|
||||
if (searchResults && searchResults.length > 0) {
|
||||
const activeIndex = searchActiveIndex || 1;
|
||||
|
||||
setResultInfo({
|
||||
currentIndex: activeIndex,
|
||||
totalResults: searchResults ? searchResults.length : 0,
|
||||
query: state.query
|
||||
});
|
||||
} else if (state && !state.active) {
|
||||
setResultInfo(null);
|
||||
}
|
||||
|
||||
setIsSearching(state ? state.loading : false);
|
||||
setResultInfo({
|
||||
currentIndex: activeIndex,
|
||||
totalResults: searchResults.length,
|
||||
query: searchQuery // Use local search query
|
||||
});
|
||||
} else if (searchQuery && searchResults?.length === 0) {
|
||||
// Show "no results" state
|
||||
setResultInfo({
|
||||
currentIndex: 0,
|
||||
totalResults: 0,
|
||||
query: searchQuery
|
||||
});
|
||||
} else {
|
||||
setResultInfo(null);
|
||||
}
|
||||
};
|
||||
|
||||
@ -52,7 +56,7 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
||||
const interval = setInterval(checkSearchState, 200);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [visible]);
|
||||
}, [visible, searchResults, searchActiveIndex, searchQuery]);
|
||||
|
||||
const handleSearch = async (query: string) => {
|
||||
if (!query.trim()) {
|
||||
@ -61,11 +65,10 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
const searchAPI = (window as any).embedPdfSearch;
|
||||
if (searchAPI) {
|
||||
if (query.trim() && searchActions) {
|
||||
setIsSearching(true);
|
||||
try {
|
||||
await searchAPI.search(query.trim());
|
||||
await searchActions.search(query.trim());
|
||||
} catch (error) {
|
||||
console.error('Search failed:', error);
|
||||
} finally {
|
||||
@ -83,45 +86,26 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
const searchAPI = (window as any).embedPdfSearch;
|
||||
if (searchAPI) {
|
||||
searchAPI.nextResult();
|
||||
}
|
||||
searchActions?.next();
|
||||
};
|
||||
|
||||
const handlePrevious = () => {
|
||||
const searchAPI = (window as any).embedPdfSearch;
|
||||
if (searchAPI) {
|
||||
searchAPI.previousResult();
|
||||
}
|
||||
searchActions?.previous();
|
||||
};
|
||||
|
||||
const handleClearSearch = () => {
|
||||
const searchAPI = (window as any).embedPdfSearch;
|
||||
if (searchAPI) {
|
||||
searchAPI.clearSearch();
|
||||
// Also try to explicitly clear highlights if available
|
||||
if (searchAPI.searchAPI && searchAPI.searchAPI.clearHighlights) {
|
||||
searchAPI.searchAPI.clearHighlights();
|
||||
}
|
||||
}
|
||||
searchActions?.clear();
|
||||
setSearchQuery('');
|
||||
setResultInfo(null);
|
||||
};
|
||||
|
||||
// Sync search query with API state on mount
|
||||
useEffect(() => {
|
||||
const searchAPI = (window as any).embedPdfSearch;
|
||||
if (searchAPI && searchAPI.state && searchAPI.state.query) {
|
||||
setSearchQuery(searchAPI.state.query);
|
||||
}
|
||||
}, []);
|
||||
// No longer need to sync with external API on mount - removed
|
||||
|
||||
const handleJumpToResult = (index: number) => {
|
||||
const searchAPI = (window as any).embedPdfSearch;
|
||||
if (searchAPI && resultInfo && index >= 1 && index <= resultInfo.totalResults) {
|
||||
// Convert 1-based user input to 0-based API index
|
||||
searchAPI.goToResult(index - 1);
|
||||
// Use context actions instead of window API - functionality simplified for now
|
||||
if (resultInfo && index >= 1 && index <= resultInfo.totalResults) {
|
||||
// Note: goToResult functionality would need to be implemented in SearchAPIBridge
|
||||
console.log('Jump to result:', index);
|
||||
}
|
||||
};
|
||||
|
||||
@ -138,7 +122,7 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
const _handleClose = () => {
|
||||
handleClearSearch();
|
||||
onClose();
|
||||
};
|
||||
|
@ -1,31 +1,53 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSelectionCapability, SelectionRangeX } from '@embedpdf/plugin-selection/react';
|
||||
import { useViewer } from '../../contexts/ViewerContext';
|
||||
|
||||
/**
|
||||
* Component that runs inside EmbedPDF context and exports selection controls globally
|
||||
* Component that runs inside EmbedPDF context and updates selection state in ViewerContext
|
||||
*/
|
||||
export function SelectionAPIBridge() {
|
||||
const { provides: selection } = useSelectionCapability();
|
||||
const { registerBridge } = useViewer();
|
||||
const [hasSelection, setHasSelection] = useState(false);
|
||||
|
||||
// Store state locally
|
||||
const [_localState, setLocalState] = useState({
|
||||
hasSelection: false
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (selection) {
|
||||
// Export selection controls to global window
|
||||
(window as any).embedPdfSelection = {
|
||||
copyToClipboard: () => selection.copyToClipboard(),
|
||||
getSelectedText: () => selection.getSelectedText(),
|
||||
getFormattedSelection: () => selection.getFormattedSelection(),
|
||||
hasSelection: hasSelection,
|
||||
// Update local state
|
||||
const newState = {
|
||||
hasSelection
|
||||
};
|
||||
setLocalState(newState);
|
||||
|
||||
// Register this bridge with ViewerContext
|
||||
registerBridge('selection', {
|
||||
state: newState,
|
||||
api: {
|
||||
copyToClipboard: () => selection.copyToClipboard(),
|
||||
getSelectedText: () => selection.getSelectedText(),
|
||||
getFormattedSelection: () => selection.getFormattedSelection(),
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for selection changes to track when text is selected
|
||||
const unsubscribe = selection.onSelectionChange((sel: SelectionRangeX | null) => {
|
||||
const hasText = !!sel;
|
||||
setHasSelection(hasText);
|
||||
// Update global state
|
||||
if ((window as any).embedPdfSelection) {
|
||||
(window as any).embedPdfSelection.hasSelection = hasText;
|
||||
}
|
||||
const updatedState = { hasSelection: hasText };
|
||||
setLocalState(updatedState);
|
||||
// Re-register with updated state
|
||||
registerBridge('selection', {
|
||||
state: updatedState,
|
||||
api: {
|
||||
copyToClipboard: () => selection.copyToClipboard(),
|
||||
getSelectedText: () => selection.getSelectedText(),
|
||||
getFormattedSelection: () => selection.getFormattedSelection(),
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Intercept Ctrl+C only when we have PDF text selected
|
||||
@ -45,7 +67,7 @@ export function SelectionAPIBridge() {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}
|
||||
}, [selection, hasSelection]);
|
||||
}, [selection, hasSelection, registerBridge]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
@ -1,40 +1,47 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSpread, SpreadMode } from '@embedpdf/plugin-spread/react';
|
||||
import { useViewer } from '../../contexts/ViewerContext';
|
||||
|
||||
/**
|
||||
* Component that runs inside EmbedPDF context and exports spread controls globally
|
||||
* Component that runs inside EmbedPDF context and updates spread state in ViewerContext
|
||||
*/
|
||||
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) {
|
||||
// Export spread controls to global window for toolbar access
|
||||
(window as any).embedPdfSpread = {
|
||||
setSpreadMode: (mode: SpreadMode) => {
|
||||
console.log('EmbedPDF: Setting spread mode to:', mode);
|
||||
spread.setSpreadMode(mode);
|
||||
},
|
||||
getSpreadMode: () => spread.getSpreadMode(),
|
||||
toggleSpreadMode: () => {
|
||||
// Toggle between None and Odd (most common dual-page mode)
|
||||
const newMode = spreadMode === SpreadMode.None ? SpreadMode.Odd : SpreadMode.None;
|
||||
console.log('EmbedPDF: Toggling spread mode from', spreadMode, 'to', newMode);
|
||||
spread.setSpreadMode(newMode);
|
||||
},
|
||||
currentSpreadMode: spreadMode,
|
||||
isDualPage: spreadMode !== SpreadMode.None,
|
||||
SpreadMode: SpreadMode, // Export enum for reference
|
||||
// Update local state
|
||||
const newState = {
|
||||
spreadMode,
|
||||
isDualPage: spreadMode !== SpreadMode.None
|
||||
};
|
||||
setLocalState(newState);
|
||||
|
||||
console.log('EmbedPDF spread controls exported to window.embedPdfSpread', {
|
||||
currentSpreadMode: spreadMode,
|
||||
isDualPage: spreadMode !== SpreadMode.None,
|
||||
spreadAPI: spread,
|
||||
availableMethods: Object.keys(spread)
|
||||
// Register this bridge with ViewerContext
|
||||
registerBridge('spread', {
|
||||
state: newState,
|
||||
api: {
|
||||
setSpreadMode: (mode: SpreadMode) => {
|
||||
spread.setSpreadMode(mode);
|
||||
},
|
||||
getSpreadMode: () => spread.getSpreadMode(),
|
||||
toggleSpreadMode: () => {
|
||||
// Toggle between None and Odd (most common dual-page mode)
|
||||
const newMode = spreadMode === SpreadMode.None ? SpreadMode.Odd : SpreadMode.None;
|
||||
spread.setSpreadMode(newMode);
|
||||
},
|
||||
SpreadMode: SpreadMode, // Export enum for reference
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [spread, spreadMode]);
|
||||
}, [spread, spreadMode, registerBridge]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
@ -1,26 +1,23 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useThumbnailCapability } from '@embedpdf/plugin-thumbnail/react';
|
||||
import { useViewer } from '../../contexts/ViewerContext';
|
||||
|
||||
/**
|
||||
* Component that runs inside EmbedPDF context and exports thumbnail controls globally
|
||||
* ThumbnailAPIBridge provides thumbnail generation functionality.
|
||||
* Exposes thumbnail API to UI components without managing state.
|
||||
*/
|
||||
export function ThumbnailAPIBridge() {
|
||||
const { provides: thumbnail } = useThumbnailCapability();
|
||||
const { registerBridge } = useViewer();
|
||||
|
||||
useEffect(() => {
|
||||
console.log('📄 ThumbnailAPIBridge useEffect:', { thumbnail: !!thumbnail });
|
||||
if (thumbnail) {
|
||||
console.log('📄 Exporting thumbnail controls to window:', {
|
||||
availableMethods: Object.keys(thumbnail),
|
||||
renderThumb: typeof thumbnail.renderThumb
|
||||
registerBridge('thumbnail', {
|
||||
state: null, // No state - just provides API
|
||||
api: thumbnail
|
||||
});
|
||||
// Export thumbnail controls to global window for debugging
|
||||
(window as any).embedPdfThumbnail = {
|
||||
thumbnailAPI: thumbnail,
|
||||
availableMethods: Object.keys(thumbnail),
|
||||
};
|
||||
}
|
||||
}, [thumbnail]);
|
||||
}, [thumbnail, registerBridge]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, ScrollArea, ActionIcon, Tooltip } from '@mantine/core';
|
||||
import { LocalIcon } from '../shared/LocalIcon';
|
||||
import { Box, ScrollArea } from '@mantine/core';
|
||||
import { useViewer } from '../../contexts/ViewerContext';
|
||||
import '../../types/embedPdf';
|
||||
|
||||
interface ThumbnailSidebarProps {
|
||||
@ -9,40 +9,33 @@ interface ThumbnailSidebarProps {
|
||||
colorScheme: 'light' | 'dark' | 'auto';
|
||||
}
|
||||
|
||||
export function ThumbnailSidebar({ visible, onToggle, colorScheme }: ThumbnailSidebarProps) {
|
||||
const [selectedPage, setSelectedPage] = useState<number>(1);
|
||||
export function ThumbnailSidebar({ visible, onToggle: _onToggle, colorScheme }: ThumbnailSidebarProps) {
|
||||
const { getScrollState, scrollActions, getThumbnailAPI } = useViewer();
|
||||
const [thumbnails, setThumbnails] = useState<{ [key: number]: string }>({});
|
||||
const [totalPages, setTotalPages] = useState<number>(0);
|
||||
|
||||
const scrollState = getScrollState();
|
||||
const thumbnailAPI = getThumbnailAPI();
|
||||
|
||||
// Convert color scheme
|
||||
const actualColorScheme = colorScheme === 'auto' ? 'light' : colorScheme;
|
||||
|
||||
// Get total pages from scroll API
|
||||
useEffect(() => {
|
||||
const scrollAPI = window.embedPdfScroll;
|
||||
if (scrollAPI && scrollAPI.totalPages) {
|
||||
setTotalPages(scrollAPI.totalPages);
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
// Generate thumbnails when sidebar becomes visible
|
||||
useEffect(() => {
|
||||
if (!visible || totalPages === 0) return;
|
||||
if (!visible || scrollState.totalPages === 0) return;
|
||||
|
||||
const thumbnailAPI = window.embedPdfThumbnail?.thumbnailAPI;
|
||||
console.log('📄 ThumbnailSidebar useEffect triggered:', {
|
||||
visible,
|
||||
thumbnailAPI: !!thumbnailAPI,
|
||||
totalPages,
|
||||
totalPages: scrollState.totalPages,
|
||||
existingThumbnails: Object.keys(thumbnails).length
|
||||
});
|
||||
|
||||
if (!thumbnailAPI) return;
|
||||
|
||||
const generateThumbnails = async () => {
|
||||
console.log('📄 Starting thumbnail generation for', totalPages, 'pages');
|
||||
console.log('📄 Starting thumbnail generation for', scrollState.totalPages, 'pages');
|
||||
|
||||
for (let pageIndex = 0; pageIndex < totalPages; pageIndex++) {
|
||||
for (let pageIndex = 0; pageIndex < scrollState.totalPages; pageIndex++) {
|
||||
if (thumbnails[pageIndex]) continue; // Skip if already generated
|
||||
|
||||
try {
|
||||
@ -89,17 +82,11 @@ export function ThumbnailSidebar({ visible, onToggle, colorScheme }: ThumbnailSi
|
||||
}
|
||||
});
|
||||
};
|
||||
}, [visible, totalPages]);
|
||||
}, [visible, scrollState.totalPages, thumbnailAPI]);
|
||||
|
||||
const handlePageClick = (pageIndex: number) => {
|
||||
const pageNumber = pageIndex + 1; // Convert to 1-based
|
||||
setSelectedPage(pageNumber);
|
||||
|
||||
// Use scroll API to navigate to page
|
||||
const scrollAPI = window.embedPdfScroll;
|
||||
if (scrollAPI && scrollAPI.scrollToPage) {
|
||||
scrollAPI.scrollToPage(pageNumber);
|
||||
}
|
||||
scrollActions.scrollToPage(pageNumber);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -129,7 +116,7 @@ export function ThumbnailSidebar({ visible, onToggle, colorScheme }: ThumbnailSi
|
||||
flexDirection: 'column',
|
||||
gap: '12px'
|
||||
}}>
|
||||
{Array.from({ length: totalPages }, (_, pageIndex) => (
|
||||
{Array.from({ length: scrollState.totalPages }, (_, pageIndex) => (
|
||||
<Box
|
||||
key={pageIndex}
|
||||
onClick={() => handlePageClick(pageIndex)}
|
||||
@ -137,10 +124,10 @@ export function ThumbnailSidebar({ visible, onToggle, colorScheme }: ThumbnailSi
|
||||
cursor: 'pointer',
|
||||
borderRadius: '8px',
|
||||
padding: '8px',
|
||||
backgroundColor: selectedPage === pageIndex + 1
|
||||
backgroundColor: scrollState.currentPage === pageIndex + 1
|
||||
? (actualColorScheme === 'dark' ? '#364FC7' : '#e7f5ff')
|
||||
: 'transparent',
|
||||
border: selectedPage === pageIndex + 1
|
||||
border: scrollState.currentPage === pageIndex + 1
|
||||
? '2px solid #1c7ed6'
|
||||
: '2px solid transparent',
|
||||
transition: 'all 0.2s ease',
|
||||
@ -150,12 +137,12 @@ export function ThumbnailSidebar({ visible, onToggle, colorScheme }: ThumbnailSi
|
||||
gap: '8px'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (selectedPage !== pageIndex + 1) {
|
||||
if (scrollState.currentPage !== pageIndex + 1) {
|
||||
e.currentTarget.style.backgroundColor = actualColorScheme === 'dark' ? '#25262b' : '#f1f3f5';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (selectedPage !== pageIndex + 1) {
|
||||
if (scrollState.currentPage !== pageIndex + 1) {
|
||||
e.currentTarget.style.backgroundColor = 'transparent';
|
||||
}
|
||||
}}
|
||||
@ -209,7 +196,7 @@ export function ThumbnailSidebar({ visible, onToggle, colorScheme }: ThumbnailSi
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
fontWeight: 500,
|
||||
color: selectedPage === pageIndex + 1
|
||||
color: scrollState.currentPage === pageIndex + 1
|
||||
? (actualColorScheme === 'dark' ? '#ffffff' : '#1c7ed6')
|
||||
: (actualColorScheme === 'dark' ? '#adb5bd' : '#6c757d')
|
||||
}}>
|
||||
|
@ -1,12 +1,20 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useZoom } from '@embedpdf/plugin-zoom/react';
|
||||
import { useViewer } from '../../contexts/ViewerContext';
|
||||
|
||||
/**
|
||||
* Component that runs inside EmbedPDF context and exports zoom controls globally
|
||||
* Component that runs inside EmbedPDF context and manages zoom state locally
|
||||
*/
|
||||
export function ZoomAPIBridge() {
|
||||
const { provides: zoom, state: zoomState } = useZoom();
|
||||
const { registerBridge } = 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(() => {
|
||||
@ -20,20 +28,21 @@ export function ZoomAPIBridge() {
|
||||
}, [zoom]);
|
||||
|
||||
useEffect(() => {
|
||||
if (zoom) {
|
||||
|
||||
// Export zoom controls to global window for right rail access
|
||||
(window as any).embedPdfZoom = {
|
||||
zoomIn: () => zoom.zoomIn(),
|
||||
zoomOut: () => zoom.zoomOut(),
|
||||
toggleMarqueeZoom: () => zoom.toggleMarqueeZoom(),
|
||||
requestZoom: (level: any) => zoom.requestZoom(level),
|
||||
currentZoom: zoomState?.currentZoomLevel || 1.4,
|
||||
zoomPercent: Math.round((zoomState?.currentZoomLevel || 1.4) * 100),
|
||||
if (zoom && zoomState) {
|
||||
// Update local state
|
||||
const newState = {
|
||||
currentZoom: zoomState.currentZoomLevel || 1.4,
|
||||
zoomPercent: Math.round((zoomState.currentZoomLevel || 1.4) * 100),
|
||||
};
|
||||
setLocalState(newState);
|
||||
|
||||
// Register this bridge with ViewerContext
|
||||
registerBridge('zoom', {
|
||||
state: newState,
|
||||
api: zoom
|
||||
});
|
||||
}
|
||||
}, [zoom, zoomState]);
|
||||
}, [zoom, zoomState, registerBridge]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
@ -1,33 +1,393 @@
|
||||
import React, { createContext, useContext, useState, ReactNode } from 'react';
|
||||
import React, { createContext, useContext, useState, ReactNode, useRef } from 'react';
|
||||
|
||||
interface ViewerContextType {
|
||||
// Thumbnail sidebar state
|
||||
isThumbnailSidebarVisible: boolean;
|
||||
toggleThumbnailSidebar: () => void;
|
||||
setThumbnailSidebarVisible: (visible: boolean) => void;
|
||||
// State interfaces - represent the shape of data from each bridge
|
||||
interface ScrollState {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
const ViewerContext = createContext<ViewerContextType | null>(null);
|
||||
interface ZoomState {
|
||||
currentZoom: number;
|
||||
zoomPercent: number;
|
||||
}
|
||||
|
||||
interface PanState {
|
||||
isPanning: boolean;
|
||||
}
|
||||
|
||||
interface SelectionState {
|
||||
hasSelection: boolean;
|
||||
}
|
||||
|
||||
interface SpreadState {
|
||||
spreadMode: any;
|
||||
isDualPage: boolean;
|
||||
}
|
||||
|
||||
interface RotationState {
|
||||
rotation: number;
|
||||
}
|
||||
|
||||
// Bridge registration interface - bridges register with state and API
|
||||
interface BridgeRef {
|
||||
state: any;
|
||||
api: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
// State getters - read current state from bridges
|
||||
getScrollState: () => ScrollState;
|
||||
getZoomState: () => ZoomState;
|
||||
getPanState: () => PanState;
|
||||
getSelectionState: () => SelectionState;
|
||||
getSpreadState: () => SpreadState;
|
||||
getRotationState: () => RotationState;
|
||||
getSearchResults: () => any[] | null;
|
||||
getSearchActiveIndex: () => number;
|
||||
getThumbnailAPI: () => any;
|
||||
|
||||
// Action handlers - call EmbedPDF APIs directly
|
||||
scrollActions: {
|
||||
scrollToPage: (page: number) => void;
|
||||
scrollToFirstPage: () => void;
|
||||
scrollToPreviousPage: () => void;
|
||||
scrollToNextPage: () => void;
|
||||
scrollToLastPage: () => void;
|
||||
};
|
||||
|
||||
zoomActions: {
|
||||
zoomIn: () => void;
|
||||
zoomOut: () => void;
|
||||
toggleMarqueeZoom: () => void;
|
||||
requestZoom: (level: number) => void;
|
||||
};
|
||||
|
||||
panActions: {
|
||||
enablePan: () => void;
|
||||
disablePan: () => void;
|
||||
togglePan: () => void;
|
||||
};
|
||||
|
||||
selectionActions: {
|
||||
copyToClipboard: () => void;
|
||||
getSelectedText: () => string;
|
||||
getFormattedSelection: () => any;
|
||||
};
|
||||
|
||||
spreadActions: {
|
||||
setSpreadMode: (mode: any) => void;
|
||||
getSpreadMode: () => any;
|
||||
toggleSpreadMode: () => void;
|
||||
};
|
||||
|
||||
rotationActions: {
|
||||
rotateForward: () => void;
|
||||
rotateBackward: () => void;
|
||||
setRotation: (rotation: number) => void;
|
||||
getRotation: () => number;
|
||||
};
|
||||
|
||||
searchActions: {
|
||||
search: (query: string) => Promise<void>;
|
||||
next: () => void;
|
||||
previous: () => void;
|
||||
clear: () => void;
|
||||
};
|
||||
|
||||
// Bridge registration - internal use by bridges
|
||||
registerBridge: (type: string, ref: BridgeRef) => 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);
|
||||
|
||||
// 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,
|
||||
});
|
||||
|
||||
const registerBridge = (type: string, ref: BridgeRef) => {
|
||||
bridgeRefs.current[type as keyof typeof bridgeRefs.current] = ref;
|
||||
};
|
||||
|
||||
const toggleThumbnailSidebar = () => {
|
||||
setIsThumbnailSidebarVisible(prev => !prev);
|
||||
};
|
||||
|
||||
const setThumbnailSidebarVisible = (visible: boolean) => {
|
||||
setIsThumbnailSidebarVisible(visible);
|
||||
// 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: null, 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 getThumbnailAPI = () => {
|
||||
return bridgeRefs.current.thumbnail?.api || null;
|
||||
};
|
||||
|
||||
// Action handlers - call APIs directly
|
||||
const scrollActions = {
|
||||
scrollToPage: (page: number) => {
|
||||
const api = bridgeRefs.current.scroll?.api;
|
||||
if (api?.scrollToPage) {
|
||||
api.scrollToPage({ pageNumber: page });
|
||||
}
|
||||
},
|
||||
scrollToFirstPage: () => {
|
||||
const api = bridgeRefs.current.scroll?.api;
|
||||
if (api?.scrollToPage) {
|
||||
api.scrollToPage({ pageNumber: 1 });
|
||||
}
|
||||
},
|
||||
scrollToPreviousPage: () => {
|
||||
const api = bridgeRefs.current.scroll?.api;
|
||||
if (api?.scrollToPreviousPage) {
|
||||
api.scrollToPreviousPage();
|
||||
}
|
||||
},
|
||||
scrollToNextPage: () => {
|
||||
const api = bridgeRefs.current.scroll?.api;
|
||||
if (api?.scrollToNextPage) {
|
||||
api.scrollToNextPage();
|
||||
}
|
||||
},
|
||||
scrollToLastPage: () => {
|
||||
const scrollState = getScrollState();
|
||||
const api = bridgeRefs.current.scroll?.api;
|
||||
if (api?.scrollToPage && scrollState.totalPages > 0) {
|
||||
api.scrollToPage({ pageNumber: scrollState.totalPages });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const zoomActions = {
|
||||
zoomIn: () => {
|
||||
const api = bridgeRefs.current.zoom?.api;
|
||||
if (api?.zoomIn) {
|
||||
api.zoomIn();
|
||||
}
|
||||
},
|
||||
zoomOut: () => {
|
||||
const api = bridgeRefs.current.zoom?.api;
|
||||
if (api?.zoomOut) {
|
||||
api.zoomOut();
|
||||
}
|
||||
},
|
||||
toggleMarqueeZoom: () => {
|
||||
const api = bridgeRefs.current.zoom?.api;
|
||||
if (api?.toggleMarqueeZoom) {
|
||||
api.toggleMarqueeZoom();
|
||||
}
|
||||
},
|
||||
requestZoom: (level: number) => {
|
||||
const api = bridgeRefs.current.zoom?.api;
|
||||
if (api?.requestZoom) {
|
||||
api.requestZoom(level);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const panActions = {
|
||||
enablePan: () => {
|
||||
const api = bridgeRefs.current.pan?.api;
|
||||
if (api?.enable) {
|
||||
api.enable();
|
||||
}
|
||||
},
|
||||
disablePan: () => {
|
||||
const api = bridgeRefs.current.pan?.api;
|
||||
if (api?.disable) {
|
||||
api.disable();
|
||||
}
|
||||
},
|
||||
togglePan: () => {
|
||||
const api = bridgeRefs.current.pan?.api;
|
||||
if (api?.toggle) {
|
||||
api.toggle();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const selectionActions = {
|
||||
copyToClipboard: () => {
|
||||
const api = bridgeRefs.current.selection?.api;
|
||||
if (api?.copyToClipboard) {
|
||||
api.copyToClipboard();
|
||||
}
|
||||
},
|
||||
getSelectedText: () => {
|
||||
const api = bridgeRefs.current.selection?.api;
|
||||
if (api?.getSelectedText) {
|
||||
return api.getSelectedText();
|
||||
}
|
||||
return '';
|
||||
},
|
||||
getFormattedSelection: () => {
|
||||
const api = bridgeRefs.current.selection?.api;
|
||||
if (api?.getFormattedSelection) {
|
||||
return api.getFormattedSelection();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const spreadActions = {
|
||||
setSpreadMode: (mode: any) => {
|
||||
const api = bridgeRefs.current.spread?.api;
|
||||
if (api?.setSpreadMode) {
|
||||
api.setSpreadMode(mode);
|
||||
}
|
||||
},
|
||||
getSpreadMode: () => {
|
||||
const api = bridgeRefs.current.spread?.api;
|
||||
if (api?.getSpreadMode) {
|
||||
return api.getSpreadMode();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
toggleSpreadMode: () => {
|
||||
const api = bridgeRefs.current.spread?.api;
|
||||
if (api?.toggleSpreadMode) {
|
||||
api.toggleSpreadMode();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const rotationActions = {
|
||||
rotateForward: () => {
|
||||
const api = bridgeRefs.current.rotation?.api;
|
||||
if (api?.rotateForward) {
|
||||
api.rotateForward();
|
||||
}
|
||||
},
|
||||
rotateBackward: () => {
|
||||
const api = bridgeRefs.current.rotation?.api;
|
||||
if (api?.rotateBackward) {
|
||||
api.rotateBackward();
|
||||
}
|
||||
},
|
||||
setRotation: (rotation: number) => {
|
||||
const api = bridgeRefs.current.rotation?.api;
|
||||
if (api?.setRotation) {
|
||||
api.setRotation(rotation);
|
||||
}
|
||||
},
|
||||
getRotation: () => {
|
||||
const api = bridgeRefs.current.rotation?.api;
|
||||
if (api?.getRotation) {
|
||||
return api.getRotation();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
const searchActions = {
|
||||
search: async (query: string) => {
|
||||
const api = bridgeRefs.current.search?.api;
|
||||
if (api?.search) {
|
||||
return api.search(query);
|
||||
}
|
||||
},
|
||||
next: () => {
|
||||
const api = bridgeRefs.current.search?.api;
|
||||
if (api?.next) {
|
||||
api.next();
|
||||
}
|
||||
},
|
||||
previous: () => {
|
||||
const api = bridgeRefs.current.search?.api;
|
||||
if (api?.previous) {
|
||||
api.previous();
|
||||
}
|
||||
},
|
||||
clear: () => {
|
||||
const api = bridgeRefs.current.search?.api;
|
||||
if (api?.clear) {
|
||||
api.clear();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const value: ViewerContextType = {
|
||||
// UI state
|
||||
isThumbnailSidebarVisible,
|
||||
toggleThumbnailSidebar,
|
||||
setThumbnailSidebarVisible,
|
||||
|
||||
// State getters
|
||||
getScrollState,
|
||||
getZoomState,
|
||||
getPanState,
|
||||
getSelectionState,
|
||||
getSpreadState,
|
||||
getRotationState,
|
||||
getSearchResults,
|
||||
getSearchActiveIndex,
|
||||
getThumbnailAPI,
|
||||
|
||||
// Actions
|
||||
scrollActions,
|
||||
zoomActions,
|
||||
panActions,
|
||||
selectionActions,
|
||||
spreadActions,
|
||||
rotationActions,
|
||||
searchActions,
|
||||
|
||||
// Bridge registration
|
||||
registerBridge,
|
||||
};
|
||||
|
||||
return (
|
||||
|
Loading…
x
Reference in New Issue
Block a user