diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index d82b23376..b4b083e6a 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -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...", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c3cbf3e89..767fa918a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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() { - - - + + + + + diff --git a/frontend/src/components/shared/RightRail.tsx b/frontend/src/components/shared/RightRail.tsx index f180bc894..67bf1d41e 100644 --- a/frontend/src/components/shared/RightRail.tsx +++ b/frontend/src/components/shared/RightRail.tsx @@ -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'} > diff --git a/frontend/src/components/viewer/CustomSearchLayer.tsx b/frontend/src/components/viewer/CustomSearchLayer.tsx index 8fe6aef17..31c15c0cc 100644 --- a/frontend/src/components/viewer/CustomSearchLayer.tsx +++ b/frontend/src/components/viewer/CustomSearchLayer.tsx @@ -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(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); diff --git a/frontend/src/components/viewer/EmbedPdfViewer.tsx b/frontend/src/components/viewer/EmbedPdfViewer.tsx index 3033589d6..09860e517 100644 --- a/frontend/src/components/viewer/EmbedPdfViewer.tsx +++ b/frontend/src/components/viewer/EmbedPdfViewer.tsx @@ -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(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 ( { - window.embedPdfSpread?.toggleSpreadMode(); + spreadActions.toggleSpreadMode(); }} currentZoom={100} /> @@ -230,11 +217,7 @@ const EmbedPdfViewerContent = ({ }; const EmbedPdfViewer = (props: EmbedPdfViewerProps) => { - return ( - - - - ); + return ; }; export default EmbedPdfViewer; \ No newline at end of file diff --git a/frontend/src/components/viewer/PanAPIBridge.tsx b/frontend/src/components/viewer/PanAPIBridge.tsx index 25040520f..7cff83c98 100644 --- a/frontend/src/components/viewer/PanAPIBridge.tsx +++ b/frontend/src/components/viewer/PanAPIBridge.tsx @@ -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 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; } diff --git a/frontend/src/components/viewer/PdfViewerToolbar.tsx b/frontend/src/components/viewer/PdfViewerToolbar.tsx index 3ab93305b..41a40a7fa 100644 --- a/frontend/src/components/viewer/PdfViewerToolbar.tsx +++ b/frontend/src/components/viewer/PdfViewerToolbar.tsx @@ -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({ /> - / {dynamicTotalPages} + / {scrollState.totalPages} {/* 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({ − - {dynamicZoom}% + {zoomState.zoomPercent}%