mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-18 01:19:24 +00:00
Rotate
This commit is contained in:
parent
85a74c1d46
commit
c17dd25069
16
frontend/package-lock.json
generated
16
frontend/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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'}
|
||||
>
|
||||
<LocalIcon icon="mouse-pointer" width="1.5rem" height="1.5rem" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
{/* Rotate Left */}
|
||||
<Tooltip content={t('rightRail.rotateLeft', 'Rotate Left')} position="left" offset={12} arrow>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
radius="md"
|
||||
className="right-rail-icon"
|
||||
onClick={() => {
|
||||
window.embedPdfRotate?.rotateBackward();
|
||||
}}
|
||||
disabled={currentView !== 'viewer'}
|
||||
>
|
||||
<LocalIcon icon="rotate-left" width="1.5rem" height="1.5rem" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
{/* Rotate Right */}
|
||||
<Tooltip content={t('rightRail.rotateRight', 'Rotate Right')} position="left" offset={12} arrow>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
radius="md"
|
||||
className="right-rail-icon"
|
||||
onClick={() => {
|
||||
window.embedPdfRotate?.rotateForward();
|
||||
}}
|
||||
disabled={currentView !== 'viewer'}
|
||||
>
|
||||
<LocalIcon icon="rotate-right" width="1.5rem" height="1.5rem" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
{/* Sidebar Toggle */}
|
||||
<Tooltip content={t('rightRail.toggleSidebar', 'Toggle Sidebar')} position="left" offset={12} arrow>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
radius="md"
|
||||
className="right-rail-icon"
|
||||
onClick={() => (window as any).toggleThumbnailSidebar?.()}
|
||||
onClick={() => window.toggleThumbnailSidebar?.()}
|
||||
disabled={currentView !== 'viewer'}
|
||||
>
|
||||
<LocalIcon icon="view-list" width="1.5rem" height="1.5rem" />
|
||||
|
@ -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) {
|
||||
<SpreadAPIBridge />
|
||||
<SearchAPIBridge />
|
||||
<ThumbnailAPIBridge />
|
||||
<RotateAPIBridge />
|
||||
<GlobalPointerProvider>
|
||||
<Viewport
|
||||
style={{
|
||||
@ -205,35 +214,37 @@ export function LocalEmbedPDF({ file, url, colorScheme }: LocalEmbedPDFProps) {
|
||||
>
|
||||
<Scroller
|
||||
renderPage={({ width, height, pageIndex, scale, rotation }: { width: number; height: number; pageIndex: number; scale: number; rotation?: number }) => (
|
||||
<PagePointerProvider {...{ pageWidth: width, pageHeight: height, pageIndex, scale, rotation: rotation || 0 }}>
|
||||
<div
|
||||
style={{
|
||||
width,
|
||||
height,
|
||||
position: 'relative',
|
||||
userSelect: 'none',
|
||||
WebkitUserSelect: 'none',
|
||||
MozUserSelect: 'none',
|
||||
msUserSelect: 'none'
|
||||
}}
|
||||
draggable={false}
|
||||
onDragStart={(e) => e.preventDefault()}
|
||||
onDrop={(e) => e.preventDefault()}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
>
|
||||
{/* 1. Low-resolution base layer for immediate feedback */}
|
||||
<RenderLayer pageIndex={pageIndex} scale={0.5} />
|
||||
<Rotate pageSize={{ width, height }}>
|
||||
<PagePointerProvider {...{ pageWidth: width, pageHeight: height, pageIndex, scale, rotation: rotation || 0 }}>
|
||||
<div
|
||||
style={{
|
||||
width,
|
||||
height,
|
||||
position: 'relative',
|
||||
userSelect: 'none',
|
||||
WebkitUserSelect: 'none',
|
||||
MozUserSelect: 'none',
|
||||
msUserSelect: 'none'
|
||||
}}
|
||||
draggable={false}
|
||||
onDragStart={(e) => e.preventDefault()}
|
||||
onDrop={(e) => e.preventDefault()}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
>
|
||||
{/* 1. Low-resolution base layer for immediate feedback */}
|
||||
<RenderLayer pageIndex={pageIndex} scale={0.5} />
|
||||
|
||||
{/* 2. High-resolution tile layer on top */}
|
||||
<TilingLayer pageIndex={pageIndex} scale={scale} />
|
||||
{/* 2. High-resolution tile layer on top */}
|
||||
<TilingLayer pageIndex={pageIndex} scale={scale} />
|
||||
|
||||
{/* 3. Search highlight layer */}
|
||||
<CustomSearchLayer pageIndex={pageIndex} scale={scale} />
|
||||
{/* 3. Search highlight layer */}
|
||||
<CustomSearchLayer pageIndex={pageIndex} scale={scale} />
|
||||
|
||||
{/* 4. Selection layer for text interaction */}
|
||||
<SelectionLayer pageIndex={pageIndex} scale={scale} />
|
||||
</div>
|
||||
</PagePointerProvider>
|
||||
{/* 4. Selection layer for text interaction */}
|
||||
<SelectionLayer pageIndex={pageIndex} scale={scale} />
|
||||
</div>
|
||||
</PagePointerProvider>
|
||||
</Rotate>
|
||||
)}
|
||||
/>
|
||||
</Viewport>
|
||||
|
23
frontend/src/components/viewer/RotateAPIBridge.tsx
Normal file
23
frontend/src/components/viewer/RotateAPIBridge.tsx
Normal file
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user