mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-18 09:29: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-loader": "^1.1.1",
|
||||||
"@embedpdf/plugin-pan": "^1.1.1",
|
"@embedpdf/plugin-pan": "^1.1.1",
|
||||||
"@embedpdf/plugin-render": "^1.1.1",
|
"@embedpdf/plugin-render": "^1.1.1",
|
||||||
|
"@embedpdf/plugin-rotate": "^1.1.1",
|
||||||
"@embedpdf/plugin-scroll": "^1.1.1",
|
"@embedpdf/plugin-scroll": "^1.1.1",
|
||||||
"@embedpdf/plugin-search": "^1.1.1",
|
"@embedpdf/plugin-search": "^1.1.1",
|
||||||
"@embedpdf/plugin-selection": "^1.1.1",
|
"@embedpdf/plugin-selection": "^1.1.1",
|
||||||
@ -715,6 +716,21 @@
|
|||||||
"vue": ">=3.2.0"
|
"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": {
|
"node_modules/@embedpdf/plugin-scroll": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-1.1.1.tgz",
|
"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-loader": "^1.1.1",
|
||||||
"@embedpdf/plugin-pan": "^1.1.1",
|
"@embedpdf/plugin-pan": "^1.1.1",
|
||||||
"@embedpdf/plugin-render": "^1.1.1",
|
"@embedpdf/plugin-render": "^1.1.1",
|
||||||
|
"@embedpdf/plugin-rotate": "^1.1.1",
|
||||||
"@embedpdf/plugin-scroll": "^1.1.1",
|
"@embedpdf/plugin-scroll": "^1.1.1",
|
||||||
"@embedpdf/plugin-search": "^1.1.1",
|
"@embedpdf/plugin-search": "^1.1.1",
|
||||||
"@embedpdf/plugin-selection": "^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 { useFileState, useFileSelection, useFileManagement } from '../../contexts/FileContext';
|
||||||
import { useNavigationState } from '../../contexts/NavigationContext';
|
import { useNavigationState } from '../../contexts/NavigationContext';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import '../../types/embedPdf';
|
||||||
|
|
||||||
import LanguageSelector from '../shared/LanguageSelector';
|
import LanguageSelector from '../shared/LanguageSelector';
|
||||||
import { useRainbowThemeContext } from '../shared/RainbowThemeProvider';
|
import { useRainbowThemeContext } from '../shared/RainbowThemeProvider';
|
||||||
@ -17,6 +18,7 @@ import { SearchInterface } from '../viewer/SearchInterface';
|
|||||||
export default function RightRail() {
|
export default function RightRail() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isPanning, setIsPanning] = useState(false);
|
const [isPanning, setIsPanning] = useState(false);
|
||||||
|
const [currentRotation, setCurrentRotation] = useState(0);
|
||||||
const { toggleTheme } = useRainbowThemeContext();
|
const { toggleTheme } = useRainbowThemeContext();
|
||||||
const { buttons, actions } = useRightRail();
|
const { buttons, actions } = useRightRail();
|
||||||
const topButtons = useMemo(() => buttons.filter(b => (b.section || 'top') === 'top' && (b.visible ?? true)), [buttons]);
|
const topButtons = useMemo(() => buttons.filter(b => (b.section || 'top') === 'top' && (b.visible ?? true)), [buttons]);
|
||||||
@ -30,6 +32,24 @@ export default function RightRail() {
|
|||||||
// Navigation view
|
// Navigation view
|
||||||
const { workbench: currentView } = useNavigationState();
|
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
|
// File state and selection
|
||||||
const { state, selectors } = useFileState();
|
const { state, selectors } = useFileState();
|
||||||
const { selectedFiles, selectedFileIds, setSelectedFiles } = useFileSelection();
|
const { selectedFiles, selectedFileIds, setSelectedFiles } = useFileSelection();
|
||||||
@ -249,7 +269,7 @@ export default function RightRail() {
|
|||||||
radius="md"
|
radius="md"
|
||||||
className="right-rail-icon"
|
className="right-rail-icon"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
(window as any).embedPdfPan?.togglePan();
|
window.embedPdfPan?.togglePan();
|
||||||
setIsPanning(!isPanning);
|
setIsPanning(!isPanning);
|
||||||
}}
|
}}
|
||||||
disabled={currentView !== 'viewer'}
|
disabled={currentView !== 'viewer'}
|
||||||
@ -264,20 +284,50 @@ export default function RightRail() {
|
|||||||
variant="subtle"
|
variant="subtle"
|
||||||
radius="md"
|
radius="md"
|
||||||
className="right-rail-icon"
|
className="right-rail-icon"
|
||||||
onClick={() => (window as any).embedPdfControls?.pointer()}
|
onClick={() => window.embedPdfControls?.pointer()}
|
||||||
disabled={currentView !== 'viewer'}
|
disabled={currentView !== 'viewer'}
|
||||||
>
|
>
|
||||||
<LocalIcon icon="mouse-pointer" width="1.5rem" height="1.5rem" />
|
<LocalIcon icon="mouse-pointer" width="1.5rem" height="1.5rem" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</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 */}
|
{/* Sidebar Toggle */}
|
||||||
<Tooltip content={t('rightRail.toggleSidebar', 'Toggle Sidebar')} position="left" offset={12} arrow>
|
<Tooltip content={t('rightRail.toggleSidebar', 'Toggle Sidebar')} position="left" offset={12} arrow>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
radius="md"
|
radius="md"
|
||||||
className="right-rail-icon"
|
className="right-rail-icon"
|
||||||
onClick={() => (window as any).toggleThumbnailSidebar?.()}
|
onClick={() => window.toggleThumbnailSidebar?.()}
|
||||||
disabled={currentView !== 'viewer'}
|
disabled={currentView !== 'viewer'}
|
||||||
>
|
>
|
||||||
<LocalIcon icon="view-list" width="1.5rem" height="1.5rem" />
|
<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 { SpreadPluginPackage, SpreadMode } from '@embedpdf/plugin-spread/react';
|
||||||
import { SearchPluginPackage } from '@embedpdf/plugin-search/react';
|
import { SearchPluginPackage } from '@embedpdf/plugin-search/react';
|
||||||
import { ThumbnailPluginPackage } from '@embedpdf/plugin-thumbnail/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 { CustomSearchLayer } from './CustomSearchLayer';
|
||||||
import { ZoomAPIBridge } from './ZoomAPIBridge';
|
import { ZoomAPIBridge } from './ZoomAPIBridge';
|
||||||
import { ScrollAPIBridge } from './ScrollAPIBridge';
|
import { ScrollAPIBridge } from './ScrollAPIBridge';
|
||||||
@ -24,6 +26,7 @@ import { PanAPIBridge } from './PanAPIBridge';
|
|||||||
import { SpreadAPIBridge } from './SpreadAPIBridge';
|
import { SpreadAPIBridge } from './SpreadAPIBridge';
|
||||||
import { SearchAPIBridge } from './SearchAPIBridge';
|
import { SearchAPIBridge } from './SearchAPIBridge';
|
||||||
import { ThumbnailAPIBridge } from './ThumbnailAPIBridge';
|
import { ThumbnailAPIBridge } from './ThumbnailAPIBridge';
|
||||||
|
import { RotateAPIBridge } from './RotateAPIBridge';
|
||||||
|
|
||||||
interface LocalEmbedPDFProps {
|
interface LocalEmbedPDFProps {
|
||||||
file?: File | Blob;
|
file?: File | Blob;
|
||||||
@ -106,6 +109,11 @@ export function LocalEmbedPDF({ file, url, colorScheme }: LocalEmbedPDFProps) {
|
|||||||
|
|
||||||
// Register thumbnail plugin for page thumbnails
|
// Register thumbnail plugin for page thumbnails
|
||||||
createPluginRegistration(ThumbnailPluginPackage),
|
createPluginRegistration(ThumbnailPluginPackage),
|
||||||
|
|
||||||
|
// Register rotate plugin
|
||||||
|
createPluginRegistration(RotatePluginPackage, {
|
||||||
|
defaultRotation: Rotation.Degree0, // Start with no rotation
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
}, [pdfUrl]);
|
}, [pdfUrl]);
|
||||||
|
|
||||||
@ -187,6 +195,7 @@ export function LocalEmbedPDF({ file, url, colorScheme }: LocalEmbedPDFProps) {
|
|||||||
<SpreadAPIBridge />
|
<SpreadAPIBridge />
|
||||||
<SearchAPIBridge />
|
<SearchAPIBridge />
|
||||||
<ThumbnailAPIBridge />
|
<ThumbnailAPIBridge />
|
||||||
|
<RotateAPIBridge />
|
||||||
<GlobalPointerProvider>
|
<GlobalPointerProvider>
|
||||||
<Viewport
|
<Viewport
|
||||||
style={{
|
style={{
|
||||||
@ -205,35 +214,37 @@ export function LocalEmbedPDF({ file, url, colorScheme }: LocalEmbedPDFProps) {
|
|||||||
>
|
>
|
||||||
<Scroller
|
<Scroller
|
||||||
renderPage={({ width, height, pageIndex, scale, rotation }: { width: number; height: number; pageIndex: number; scale: number; rotation?: number }) => (
|
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 }}>
|
<Rotate pageSize={{ width, height }}>
|
||||||
<div
|
<PagePointerProvider {...{ pageWidth: width, pageHeight: height, pageIndex, scale, rotation: rotation || 0 }}>
|
||||||
style={{
|
<div
|
||||||
width,
|
style={{
|
||||||
height,
|
width,
|
||||||
position: 'relative',
|
height,
|
||||||
userSelect: 'none',
|
position: 'relative',
|
||||||
WebkitUserSelect: 'none',
|
userSelect: 'none',
|
||||||
MozUserSelect: 'none',
|
WebkitUserSelect: 'none',
|
||||||
msUserSelect: 'none'
|
MozUserSelect: 'none',
|
||||||
}}
|
msUserSelect: 'none'
|
||||||
draggable={false}
|
}}
|
||||||
onDragStart={(e) => e.preventDefault()}
|
draggable={false}
|
||||||
onDrop={(e) => e.preventDefault()}
|
onDragStart={(e) => e.preventDefault()}
|
||||||
onDragOver={(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} />
|
{/* 1. Low-resolution base layer for immediate feedback */}
|
||||||
|
<RenderLayer pageIndex={pageIndex} scale={0.5} />
|
||||||
|
|
||||||
{/* 2. High-resolution tile layer on top */}
|
{/* 2. High-resolution tile layer on top */}
|
||||||
<TilingLayer pageIndex={pageIndex} scale={scale} />
|
<TilingLayer pageIndex={pageIndex} scale={scale} />
|
||||||
|
|
||||||
{/* 3. Search highlight layer */}
|
{/* 3. Search highlight layer */}
|
||||||
<CustomSearchLayer pageIndex={pageIndex} scale={scale} />
|
<CustomSearchLayer pageIndex={pageIndex} scale={scale} />
|
||||||
|
|
||||||
{/* 4. Selection layer for text interaction */}
|
{/* 4. Selection layer for text interaction */}
|
||||||
<SelectionLayer pageIndex={pageIndex} scale={scale} />
|
<SelectionLayer pageIndex={pageIndex} scale={scale} />
|
||||||
</div>
|
</div>
|
||||||
</PagePointerProvider>
|
</PagePointerProvider>
|
||||||
|
</Rotate>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Viewport>
|
</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 {
|
export interface EmbedPdfPanAPI {
|
||||||
isPanning: boolean;
|
isPanning: boolean;
|
||||||
|
togglePan: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EmbedPdfSpreadAPI {
|
export interface EmbedPdfSpreadAPI {
|
||||||
toggleSpreadMode: () => void;
|
toggleSpreadMode: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EmbedPdfRotateAPI {
|
||||||
|
rotateForward: () => void;
|
||||||
|
rotateBackward: () => void;
|
||||||
|
setRotation: (rotation: number) => void;
|
||||||
|
getRotation: () => number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmbedPdfControlsAPI {
|
||||||
|
pointer: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
export interface EmbedPdfThumbnailAPI {
|
export interface EmbedPdfThumbnailAPI {
|
||||||
thumbnailAPI: {
|
thumbnailAPI: {
|
||||||
renderThumb: (pageIndex: number, scale: number) => {
|
renderThumb: (pageIndex: number, scale: number) => {
|
||||||
@ -37,6 +49,8 @@ declare global {
|
|||||||
embedPdfScroll?: EmbedPdfScrollAPI;
|
embedPdfScroll?: EmbedPdfScrollAPI;
|
||||||
embedPdfPan?: EmbedPdfPanAPI;
|
embedPdfPan?: EmbedPdfPanAPI;
|
||||||
embedPdfSpread?: EmbedPdfSpreadAPI;
|
embedPdfSpread?: EmbedPdfSpreadAPI;
|
||||||
|
embedPdfRotate?: EmbedPdfRotateAPI;
|
||||||
|
embedPdfControls?: EmbedPdfControlsAPI;
|
||||||
embedPdfThumbnail?: EmbedPdfThumbnailAPI;
|
embedPdfThumbnail?: EmbedPdfThumbnailAPI;
|
||||||
toggleThumbnailSidebar?: () => void;
|
toggleThumbnailSidebar?: () => void;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user