diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6ff7d4358..be3419d39 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,6 +16,7 @@ "@embedpdf/plugin-loader": "^1.1.1", "@embedpdf/plugin-pan": "^1.1.1", "@embedpdf/plugin-render": "^1.1.1", + "@embedpdf/plugin-rotate": "^1.1.1", "@embedpdf/plugin-scroll": "^1.1.1", "@embedpdf/plugin-search": "^1.1.1", "@embedpdf/plugin-selection": "^1.1.1", @@ -715,6 +716,21 @@ "vue": ">=3.2.0" } }, + "node_modules/@embedpdf/plugin-rotate": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-rotate/-/plugin-rotate-1.1.1.tgz", + "integrity": "sha512-xxM62dv4TAoTEfCNxyC0UbGryT3ucAH4txQAoab7tfvnfqbAIxTonH1PzQMsBmzN0WETCGjUBm1Ympb95cDx2Q==", + "dependencies": { + "@embedpdf/models": "1.1.1" + }, + "peerDependencies": { + "@embedpdf/core": "1.1.1", + "preact": "^10.26.4", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "vue": ">=3.2.0" + } + }, "node_modules/@embedpdf/plugin-scroll": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-1.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index b33135246..6bacd4017 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,7 @@ "@embedpdf/plugin-loader": "^1.1.1", "@embedpdf/plugin-pan": "^1.1.1", "@embedpdf/plugin-render": "^1.1.1", + "@embedpdf/plugin-rotate": "^1.1.1", "@embedpdf/plugin-scroll": "^1.1.1", "@embedpdf/plugin-search": "^1.1.1", "@embedpdf/plugin-selection": "^1.1.1", diff --git a/frontend/src/components/shared/RightRail.tsx b/frontend/src/components/shared/RightRail.tsx index 92f377076..e8f3312fe 100644 --- a/frontend/src/components/shared/RightRail.tsx +++ b/frontend/src/components/shared/RightRail.tsx @@ -7,6 +7,7 @@ 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'; @@ -17,6 +18,7 @@ import { SearchInterface } from '../viewer/SearchInterface'; export default function RightRail() { const { t } = useTranslation(); const [isPanning, setIsPanning] = useState(false); + const [currentRotation, setCurrentRotation] = useState(0); const { toggleTheme } = useRainbowThemeContext(); const { buttons, actions } = useRightRail(); const topButtons = useMemo(() => buttons.filter(b => (b.section || 'top') === 'top' && (b.visible ?? true)), [buttons]); @@ -30,6 +32,24 @@ export default function RightRail() { // Navigation view const { workbench: currentView } = useNavigationState(); + // Sync rotation state with EmbedPDF API + 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); + } + }, [currentView]); + // File state and selection const { state, selectors } = useFileState(); const { selectedFiles, selectedFileIds, setSelectedFiles } = useFileSelection(); @@ -249,7 +269,7 @@ export default function RightRail() { radius="md" className="right-rail-icon" onClick={() => { - (window as any).embedPdfPan?.togglePan(); + window.embedPdfPan?.togglePan(); setIsPanning(!isPanning); }} disabled={currentView !== 'viewer'} @@ -264,20 +284,50 @@ export default function RightRail() { variant="subtle" radius="md" className="right-rail-icon" - onClick={() => (window as any).embedPdfControls?.pointer()} + onClick={() => window.embedPdfControls?.pointer()} disabled={currentView !== 'viewer'} > + {/* Rotate Left */} + + { + window.embedPdfRotate?.rotateBackward(); + }} + disabled={currentView !== 'viewer'} + > + + + + + {/* Rotate Right */} + + { + window.embedPdfRotate?.rotateForward(); + }} + disabled={currentView !== 'viewer'} + > + + + + {/* Sidebar Toggle */} (window as any).toggleThumbnailSidebar?.()} + onClick={() => window.toggleThumbnailSidebar?.()} disabled={currentView !== 'viewer'} > diff --git a/frontend/src/components/viewer/LocalEmbedPDF.tsx b/frontend/src/components/viewer/LocalEmbedPDF.tsx index bafe48996..341810516 100644 --- a/frontend/src/components/viewer/LocalEmbedPDF.tsx +++ b/frontend/src/components/viewer/LocalEmbedPDF.tsx @@ -16,6 +16,8 @@ import { PanPluginPackage } from '@embedpdf/plugin-pan/react'; import { SpreadPluginPackage, SpreadMode } from '@embedpdf/plugin-spread/react'; import { SearchPluginPackage } from '@embedpdf/plugin-search/react'; import { ThumbnailPluginPackage } from '@embedpdf/plugin-thumbnail/react'; +import { RotatePluginPackage, Rotate } from '@embedpdf/plugin-rotate/react'; +import { Rotation } from '@embedpdf/models'; import { CustomSearchLayer } from './CustomSearchLayer'; import { ZoomAPIBridge } from './ZoomAPIBridge'; import { ScrollAPIBridge } from './ScrollAPIBridge'; @@ -24,6 +26,7 @@ import { PanAPIBridge } from './PanAPIBridge'; import { SpreadAPIBridge } from './SpreadAPIBridge'; import { SearchAPIBridge } from './SearchAPIBridge'; import { ThumbnailAPIBridge } from './ThumbnailAPIBridge'; +import { RotateAPIBridge } from './RotateAPIBridge'; interface LocalEmbedPDFProps { file?: File | Blob; @@ -106,6 +109,11 @@ export function LocalEmbedPDF({ file, url, colorScheme }: LocalEmbedPDFProps) { // Register thumbnail plugin for page thumbnails createPluginRegistration(ThumbnailPluginPackage), + + // Register rotate plugin + createPluginRegistration(RotatePluginPackage, { + defaultRotation: Rotation.Degree0, // Start with no rotation + }), ]; }, [pdfUrl]); @@ -187,6 +195,7 @@ export function LocalEmbedPDF({ file, url, colorScheme }: LocalEmbedPDFProps) { + ( - -
e.preventDefault()} - onDrop={(e) => e.preventDefault()} - onDragOver={(e) => e.preventDefault()} - > - {/* 1. Low-resolution base layer for immediate feedback */} - - - {/* 2. High-resolution tile layer on top */} - - - {/* 3. Search highlight layer */} - - - {/* 4. Selection layer for text interaction */} - -
-
+ + +
e.preventDefault()} + onDrop={(e) => e.preventDefault()} + onDragOver={(e) => e.preventDefault()} + > + {/* 1. Low-resolution base layer for immediate feedback */} + + + {/* 2. High-resolution tile layer on top */} + + + {/* 3. Search highlight layer */} + + + {/* 4. Selection layer for text interaction */} + +
+
+
)} />
diff --git a/frontend/src/components/viewer/RotateAPIBridge.tsx b/frontend/src/components/viewer/RotateAPIBridge.tsx new file mode 100644 index 000000000..1f5a23a70 --- /dev/null +++ b/frontend/src/components/viewer/RotateAPIBridge.tsx @@ -0,0 +1,23 @@ +import { useEffect } from 'react'; +import { useRotate } from '@embedpdf/plugin-rotate/react'; + +/** + * Component that runs inside EmbedPDF context and exports rotate controls globally + */ +export function RotateAPIBridge() { + const { provides: rotate, rotation } = useRotate(); + + 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, + }; + } + }, [rotate, rotation]); + + return null; +} \ No newline at end of file diff --git a/frontend/src/types/embedPdf.ts b/frontend/src/types/embedPdf.ts index d33f35e74..49ce2141c 100644 --- a/frontend/src/types/embedPdf.ts +++ b/frontend/src/types/embedPdf.ts @@ -17,12 +17,24 @@ export interface EmbedPdfScrollAPI { 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) => { @@ -37,6 +49,8 @@ declare global { embedPdfScroll?: EmbedPdfScrollAPI; embedPdfPan?: EmbedPdfPanAPI; embedPdfSpread?: EmbedPdfSpreadAPI; + embedPdfRotate?: EmbedPdfRotateAPI; + embedPdfControls?: EmbedPdfControlsAPI; embedPdfThumbnail?: EmbedPdfThumbnailAPI; toggleThumbnailSidebar?: () => void; }