mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-24 04:26:14 +00:00
Compare commits
No commits in common. "423617db52203f4de20c20188aae317b83fa851f" and "368e9801a124f267fc033916f754da0de45e0fc4" have entirely different histories.
423617db52
...
368e9801a1
33
frontend/package-lock.json
generated
33
frontend/package-lock.json
generated
@ -17,10 +17,8 @@
|
|||||||
"@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-scroll": "^1.1.1",
|
"@embedpdf/plugin-scroll": "^1.1.1",
|
||||||
"@embedpdf/plugin-search": "^1.1.1",
|
|
||||||
"@embedpdf/plugin-selection": "^1.1.1",
|
"@embedpdf/plugin-selection": "^1.1.1",
|
||||||
"@embedpdf/plugin-spread": "^1.1.1",
|
"@embedpdf/plugin-spread": "^1.1.1",
|
||||||
"@embedpdf/plugin-thumbnail": "^1.1.1",
|
|
||||||
"@embedpdf/plugin-tiling": "^1.1.1",
|
"@embedpdf/plugin-tiling": "^1.1.1",
|
||||||
"@embedpdf/plugin-viewport": "^1.1.1",
|
"@embedpdf/plugin-viewport": "^1.1.1",
|
||||||
"@embedpdf/plugin-zoom": "^1.1.1",
|
"@embedpdf/plugin-zoom": "^1.1.1",
|
||||||
@ -731,22 +729,6 @@
|
|||||||
"vue": ">=3.2.0"
|
"vue": ">=3.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@embedpdf/plugin-search": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-search/-/plugin-search-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-h3gc9HVK8HCOD2MGCikhbkpwWjAYnu/KQ1ZxT2mb5AvbcIog+LQhVElSyV80deeFStqqLn/epz27b2TqQSz0EA==",
|
|
||||||
"dependencies": {
|
|
||||||
"@embedpdf/models": "1.1.1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@embedpdf/core": "1.1.1",
|
|
||||||
"@embedpdf/plugin-loader": "1.1.1",
|
|
||||||
"preact": "^10.26.4",
|
|
||||||
"react": ">=16.8.0",
|
|
||||||
"react-dom": ">=16.8.0",
|
|
||||||
"vue": ">=3.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@embedpdf/plugin-selection": {
|
"node_modules/@embedpdf/plugin-selection": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-1.1.1.tgz",
|
||||||
@ -780,21 +762,6 @@
|
|||||||
"vue": ">=3.2.0"
|
"vue": ">=3.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@embedpdf/plugin-thumbnail": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-thumbnail/-/plugin-thumbnail-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-xH5p2XgxkDgAbZKJSGAYcDNPlnKEsBHm0EH+YCxOI0T5zX/a9j4uFj7mbbs9IcQ9vEFuIi6lGvY3DUWN4TeDpQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"@embedpdf/models": "1.1.1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@embedpdf/core": "1.1.1",
|
|
||||||
"@embedpdf/plugin-render": "1.1.1",
|
|
||||||
"preact": "^10.26.4",
|
|
||||||
"react": ">=16.8.0",
|
|
||||||
"react-dom": ">=16.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@embedpdf/plugin-tiling": {
|
"node_modules/@embedpdf/plugin-tiling": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-tiling/-/plugin-tiling-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-tiling/-/plugin-tiling-1.1.1.tgz",
|
||||||
|
@ -13,10 +13,8 @@
|
|||||||
"@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-scroll": "^1.1.1",
|
"@embedpdf/plugin-scroll": "^1.1.1",
|
||||||
"@embedpdf/plugin-search": "^1.1.1",
|
|
||||||
"@embedpdf/plugin-selection": "^1.1.1",
|
"@embedpdf/plugin-selection": "^1.1.1",
|
||||||
"@embedpdf/plugin-spread": "^1.1.1",
|
"@embedpdf/plugin-spread": "^1.1.1",
|
||||||
"@embedpdf/plugin-thumbnail": "^1.1.1",
|
|
||||||
"@embedpdf/plugin-tiling": "^1.1.1",
|
"@embedpdf/plugin-tiling": "^1.1.1",
|
||||||
"@embedpdf/plugin-viewport": "^1.1.1",
|
"@embedpdf/plugin-viewport": "^1.1.1",
|
||||||
"@embedpdf/plugin-zoom": "^1.1.1",
|
"@embedpdf/plugin-zoom": "^1.1.1",
|
||||||
|
@ -157,7 +157,7 @@ export default function Workbench() {
|
|||||||
className="flex-1 min-h-0 relative z-10 workbench-scrollable "
|
className="flex-1 min-h-0 relative z-10 workbench-scrollable "
|
||||||
style={{
|
style={{
|
||||||
transition: 'opacity 0.15s ease-in-out',
|
transition: 'opacity 0.15s ease-in-out',
|
||||||
marginTop: 0,
|
marginTop: '1rem',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{renderMainContent()}
|
{renderMainContent()}
|
||||||
|
@ -219,13 +219,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).togglePdfSearch?.()}
|
onClick={() => (window as any).embedPdfControls?.search()}
|
||||||
disabled={currentView !== 'viewer'}
|
disabled={currentView !== 'viewer'}
|
||||||
>
|
>
|
||||||
<LocalIcon icon="search" width="1.5rem" height="1.5rem" />
|
<LocalIcon icon="search" width="1.5rem" height="1.5rem" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
{/* Zoom Out */}
|
||||||
|
<Tooltip content={t('rightRail.zoomOut', 'Zoom Out')} position="left" offset={12} arrow>
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
radius="md"
|
||||||
|
className="right-rail-icon"
|
||||||
|
onClick={() => (window as any).embedPdfZoom?.zoomOut()}
|
||||||
|
disabled={currentView !== 'viewer'}
|
||||||
|
>
|
||||||
|
<LocalIcon icon="zoom-out" width="1.5rem" height="1.5rem" />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{/* Zoom In */}
|
||||||
|
<Tooltip content={t('rightRail.zoomIn', 'Zoom In')} position="left" offset={12} arrow>
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
radius="md"
|
||||||
|
className="right-rail-icon"
|
||||||
|
onClick={() => (window as any).embedPdfZoom?.zoomIn()}
|
||||||
|
disabled={currentView !== 'viewer'}
|
||||||
|
>
|
||||||
|
<LocalIcon icon="zoom-in" width="1.5rem" height="1.5rem" />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
{/* Area Zoom */}
|
||||||
|
<Tooltip content={t('rightRail.areaZoom', 'Area Zoom')} position="left" offset={12} arrow>
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
radius="md"
|
||||||
|
className="right-rail-icon"
|
||||||
|
onClick={() => (window as any).embedPdfZoom?.toggleMarqueeZoom()}
|
||||||
|
disabled={currentView !== 'viewer'}
|
||||||
|
>
|
||||||
|
<LocalIcon icon="crop-free" width="1.5rem" height="1.5rem" />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
{/* Pan Mode */}
|
{/* Pan Mode */}
|
||||||
<Tooltip content={t('rightRail.panMode', 'Pan Mode')} position="left" offset={12} arrow>
|
<Tooltip content={t('rightRail.panMode', 'Pan Mode')} position="left" offset={12} arrow>
|
||||||
@ -260,7 +297,7 @@ export default function RightRail() {
|
|||||||
variant="subtle"
|
variant="subtle"
|
||||||
radius="md"
|
radius="md"
|
||||||
className="right-rail-icon"
|
className="right-rail-icon"
|
||||||
onClick={() => (window as any).toggleThumbnailSidebar?.()}
|
onClick={() => (window as any).embedPdfControls?.sidebar()}
|
||||||
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" />
|
||||||
|
@ -1,131 +0,0 @@
|
|||||||
import React, { useState, useEffect, useMemo } from 'react';
|
|
||||||
import { useSearch } from '@embedpdf/plugin-search/react';
|
|
||||||
|
|
||||||
interface SearchLayerProps {
|
|
||||||
pageIndex: number;
|
|
||||||
scale: number;
|
|
||||||
highlightColor?: string;
|
|
||||||
activeHighlightColor?: string;
|
|
||||||
opacity?: number;
|
|
||||||
padding?: number;
|
|
||||||
borderRadius?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SearchResultState {
|
|
||||||
results: Array<{
|
|
||||||
pageIndex: number;
|
|
||||||
rects: Array<{
|
|
||||||
origin: { x: number; y: number };
|
|
||||||
size: { width: number; height: number };
|
|
||||||
}>;
|
|
||||||
}>;
|
|
||||||
activeResultIndex?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CustomSearchLayer({
|
|
||||||
pageIndex,
|
|
||||||
scale,
|
|
||||||
highlightColor = '#FFFF00',
|
|
||||||
activeHighlightColor = '#FFBF00',
|
|
||||||
opacity = 0.6,
|
|
||||||
padding = 2,
|
|
||||||
borderRadius = 4
|
|
||||||
}: SearchLayerProps) {
|
|
||||||
const { provides: searchProvides } = useSearch();
|
|
||||||
const [searchResultState, setSearchResultState] = useState<SearchResultState | null>(null);
|
|
||||||
|
|
||||||
// Subscribe to search result state changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (!searchProvides) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(window as any).currentSearchResults = null;
|
|
||||||
(window as any).currentActiveIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSearchResultState(state);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
return unsubscribe;
|
|
||||||
}, [searchProvides, pageIndex]);
|
|
||||||
|
|
||||||
// Filter results for current page while preserving original indices
|
|
||||||
const pageResults = useMemo(() => {
|
|
||||||
if (!searchResultState?.results) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const filtered = searchResultState.results
|
|
||||||
.map((result, originalIndex) => ({ result, originalIndex }))
|
|
||||||
.filter(({ result }) => result.pageIndex === pageIndex);
|
|
||||||
|
|
||||||
return filtered;
|
|
||||||
}, [searchResultState, pageIndex]);
|
|
||||||
|
|
||||||
if (!pageResults.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
pointerEvents: 'none',
|
|
||||||
zIndex: 10
|
|
||||||
}}>
|
|
||||||
{pageResults.map(({ result, originalIndex }, idx) => (
|
|
||||||
<div key={`result-${idx}`}>
|
|
||||||
{result.rects.map((rect, rectIdx) => (
|
|
||||||
<div
|
|
||||||
key={`rect-${idx}-${rectIdx}`}
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: `${rect.origin.y * scale - padding}px`,
|
|
||||||
left: `${rect.origin.x * scale - padding}px`,
|
|
||||||
width: `${rect.size.width * scale + (padding * 2)}px`,
|
|
||||||
height: `${rect.size.height * scale + (padding * 2)}px`,
|
|
||||||
backgroundColor: originalIndex === searchResultState?.activeResultIndex
|
|
||||||
? activeHighlightColor
|
|
||||||
: highlightColor,
|
|
||||||
opacity: opacity,
|
|
||||||
borderRadius: `${borderRadius}px`,
|
|
||||||
transform: 'scale(1.02)',
|
|
||||||
transformOrigin: 'center',
|
|
||||||
transition: 'opacity 0.3s ease-in-out, background-color 0.2s ease-in-out',
|
|
||||||
pointerEvents: 'none',
|
|
||||||
boxShadow: originalIndex === searchResultState?.activeResultIndex
|
|
||||||
? '0 0 0 1px rgba(255, 191, 0, 0.8)'
|
|
||||||
: 'none'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -8,8 +8,6 @@ import { useFileState } from "../../contexts/FileContext";
|
|||||||
import { useFileWithUrl } from "../../hooks/useFileWithUrl";
|
import { useFileWithUrl } from "../../hooks/useFileWithUrl";
|
||||||
import { LocalEmbedPDF } from './LocalEmbedPDF';
|
import { LocalEmbedPDF } from './LocalEmbedPDF';
|
||||||
import { PdfViewerToolbar } from './PdfViewerToolbar';
|
import { PdfViewerToolbar } from './PdfViewerToolbar';
|
||||||
import { SearchInterface } from './SearchInterface';
|
|
||||||
import { ThumbnailSidebar } from './ThumbnailSidebar';
|
|
||||||
|
|
||||||
export interface EmbedPdfViewerProps {
|
export interface EmbedPdfViewerProps {
|
||||||
sidebarsVisible: boolean;
|
sidebarsVisible: boolean;
|
||||||
@ -29,8 +27,6 @@ const EmbedPdfViewer = ({
|
|||||||
const { colorScheme } = useMantineColorScheme();
|
const { colorScheme } = useMantineColorScheme();
|
||||||
const viewerRef = React.useRef<HTMLDivElement>(null);
|
const viewerRef = React.useRef<HTMLDivElement>(null);
|
||||||
const [isViewerHovered, setIsViewerHovered] = React.useState(false);
|
const [isViewerHovered, setIsViewerHovered] = React.useState(false);
|
||||||
const [isSearchVisible, setIsSearchVisible] = React.useState(false);
|
|
||||||
const [isThumbnailSidebarVisible, setIsThumbnailSidebarVisible] = React.useState(false);
|
|
||||||
|
|
||||||
// Get current file from FileContext
|
// Get current file from FileContext
|
||||||
const { selectors } = useFileState();
|
const { selectors } = useFileState();
|
||||||
@ -92,50 +88,6 @@ const EmbedPdfViewer = ({
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Handle keyboard zoom shortcuts
|
|
||||||
React.useEffect(() => {
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
|
||||||
if (!isViewerHovered) return;
|
|
||||||
|
|
||||||
// Check if Ctrl (Windows/Linux) or Cmd (Mac) is pressed
|
|
||||||
if (event.ctrlKey || event.metaKey) {
|
|
||||||
const zoomAPI = (window as any).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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('keydown', handleKeyDown);
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('keydown', handleKeyDown);
|
|
||||||
};
|
|
||||||
}, [isViewerHovered]);
|
|
||||||
|
|
||||||
// Expose toggle functions globally for right rail buttons
|
|
||||||
React.useEffect(() => {
|
|
||||||
(window as any).togglePdfSearch = () => {
|
|
||||||
setIsSearchVisible(prev => !prev);
|
|
||||||
};
|
|
||||||
|
|
||||||
(window as any).toggleThumbnailSidebar = () => {
|
|
||||||
setIsThumbnailSidebarVisible(prev => !prev);
|
|
||||||
};
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
delete (window as any).togglePdfSearch;
|
|
||||||
delete (window as any).toggleThumbnailSidebar;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
ref={viewerRef}
|
ref={viewerRef}
|
||||||
@ -177,68 +129,54 @@ const EmbedPdfViewer = ({
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* EmbedPDF Viewer */}
|
{/* EmbedPDF Viewer with Toolbar Overlay */}
|
||||||
<Box style={{
|
<Box style={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
minWidth: 0
|
minWidth: 0,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column'
|
||||||
}}>
|
}}>
|
||||||
<LocalEmbedPDF
|
<LocalEmbedPDF
|
||||||
file={effectiveFile.file}
|
file={effectiveFile.file}
|
||||||
url={effectiveFile.url}
|
url={effectiveFile.url}
|
||||||
colorScheme={colorScheme}
|
colorScheme={colorScheme}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Bottom Toolbar Overlay */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "sticky",
|
||||||
|
bottom: 0,
|
||||||
|
zIndex: 50,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
pointerEvents: "none",
|
||||||
|
background: "transparent",
|
||||||
|
marginTop: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ pointerEvents: "auto" }}>
|
||||||
|
<PdfViewerToolbar
|
||||||
|
currentPage={1}
|
||||||
|
totalPages={1}
|
||||||
|
onPageChange={(page) => {
|
||||||
|
// Placeholder - will implement page navigation later
|
||||||
|
console.log('Navigate to page:', page);
|
||||||
|
}}
|
||||||
|
dualPage={false}
|
||||||
|
onDualPageToggle={() => {
|
||||||
|
(window as any).embedPdfSpread?.toggleSpreadMode();
|
||||||
|
}}
|
||||||
|
currentZoom={100}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Bottom Toolbar Overlay */}
|
|
||||||
{effectiveFile && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "fixed",
|
|
||||||
bottom: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
zIndex: 50,
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
pointerEvents: "none",
|
|
||||||
background: "transparent",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ pointerEvents: "auto" }}>
|
|
||||||
<PdfViewerToolbar
|
|
||||||
currentPage={1}
|
|
||||||
totalPages={1}
|
|
||||||
onPageChange={(page) => {
|
|
||||||
// Placeholder - will implement page navigation later
|
|
||||||
console.log('Navigate to page:', page);
|
|
||||||
}}
|
|
||||||
dualPage={false}
|
|
||||||
onDualPageToggle={() => {
|
|
||||||
(window as any).embedPdfSpread?.toggleSpreadMode();
|
|
||||||
}}
|
|
||||||
currentZoom={100}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Search Interface Overlay */}
|
|
||||||
<SearchInterface
|
|
||||||
visible={isSearchVisible}
|
|
||||||
onClose={() => setIsSearchVisible(false)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Thumbnail Sidebar */}
|
|
||||||
<ThumbnailSidebar
|
|
||||||
visible={isThumbnailSidebarVisible}
|
|
||||||
onToggle={() => setIsThumbnailSidebarVisible(prev => !prev)}
|
|
||||||
colorScheme={colorScheme}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -14,16 +14,11 @@ import { SelectionLayer, SelectionPluginPackage } from '@embedpdf/plugin-selecti
|
|||||||
import { TilingLayer, TilingPluginPackage } from '@embedpdf/plugin-tiling/react';
|
import { TilingLayer, TilingPluginPackage } from '@embedpdf/plugin-tiling/react';
|
||||||
import { PanPluginPackage } from '@embedpdf/plugin-pan/react';
|
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 { ThumbnailPluginPackage } from '@embedpdf/plugin-thumbnail/react';
|
|
||||||
import { CustomSearchLayer } from './CustomSearchLayer';
|
|
||||||
import { ZoomControlsExporter } from './ZoomControlsExporter';
|
import { ZoomControlsExporter } from './ZoomControlsExporter';
|
||||||
import { ScrollControlsExporter } from './ScrollControlsExporter';
|
import { ScrollControlsExporter } from './ScrollControlsExporter';
|
||||||
import { SelectionControlsExporter } from './SelectionControlsExporter';
|
import { SelectionControlsExporter } from './SelectionControlsExporter';
|
||||||
import { PanControlsExporter } from './PanControlsExporter';
|
import { PanControlsExporter } from './PanControlsExporter';
|
||||||
import { SpreadControlsExporter } from './SpreadControlsExporter';
|
import { SpreadControlsExporter } from './SpreadControlsExporter';
|
||||||
import { SearchControlsExporter } from './SearchControlsExporter';
|
|
||||||
import { ThumbnailControlsExporter } from './ThumbnailControlsExporter';
|
|
||||||
|
|
||||||
interface LocalEmbedPDFProps {
|
interface LocalEmbedPDFProps {
|
||||||
file?: File | Blob;
|
file?: File | Blob;
|
||||||
@ -84,7 +79,7 @@ export function LocalEmbedPDF({ file, url, colorScheme }: LocalEmbedPDFProps) {
|
|||||||
|
|
||||||
// Register zoom plugin with configuration
|
// Register zoom plugin with configuration
|
||||||
createPluginRegistration(ZoomPluginPackage, {
|
createPluginRegistration(ZoomPluginPackage, {
|
||||||
defaultZoomLevel: 1.0, // Start at exactly 100% zoom
|
defaultZoomLevel: ZoomMode.FitPage,
|
||||||
minZoom: 0.2,
|
minZoom: 0.2,
|
||||||
maxZoom: 3.0,
|
maxZoom: 3.0,
|
||||||
}),
|
}),
|
||||||
@ -100,12 +95,6 @@ export function LocalEmbedPDF({ file, url, colorScheme }: LocalEmbedPDFProps) {
|
|||||||
createPluginRegistration(SpreadPluginPackage, {
|
createPluginRegistration(SpreadPluginPackage, {
|
||||||
defaultSpreadMode: SpreadMode.None, // Start with single page view
|
defaultSpreadMode: SpreadMode.None, // Start with single page view
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Register search plugin for text search
|
|
||||||
createPluginRegistration(SearchPluginPackage),
|
|
||||||
|
|
||||||
// Register thumbnail plugin for page thumbnails
|
|
||||||
createPluginRegistration(ThumbnailPluginPackage),
|
|
||||||
];
|
];
|
||||||
}, [pdfUrl]);
|
}, [pdfUrl]);
|
||||||
|
|
||||||
@ -185,8 +174,6 @@ export function LocalEmbedPDF({ file, url, colorScheme }: LocalEmbedPDFProps) {
|
|||||||
<SelectionControlsExporter />
|
<SelectionControlsExporter />
|
||||||
<PanControlsExporter />
|
<PanControlsExporter />
|
||||||
<SpreadControlsExporter />
|
<SpreadControlsExporter />
|
||||||
<SearchControlsExporter />
|
|
||||||
<ThumbnailControlsExporter />
|
|
||||||
<GlobalPointerProvider>
|
<GlobalPointerProvider>
|
||||||
<Viewport
|
<Viewport
|
||||||
style={{
|
style={{
|
||||||
@ -227,10 +214,7 @@ export function LocalEmbedPDF({ file, url, colorScheme }: LocalEmbedPDFProps) {
|
|||||||
{/* 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. Selection layer for text interaction */}
|
||||||
<CustomSearchLayer pageIndex={pageIndex} scale={scale} />
|
|
||||||
|
|
||||||
{/* 4. Selection layer for text interaction */}
|
|
||||||
<SelectionLayer pageIndex={pageIndex} scale={scale} />
|
<SelectionLayer pageIndex={pageIndex} scale={scale} />
|
||||||
</div>
|
</div>
|
||||||
</PagePointerProvider>
|
</PagePointerProvider>
|
||||||
|
@ -1,52 +0,0 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
import { useSearch } from '@embedpdf/plugin-search/react';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component that runs inside EmbedPDF context and exports search controls globally
|
|
||||||
*/
|
|
||||||
export function SearchControlsExporter() {
|
|
||||||
const { provides: search, state } = useSearch();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (search && state) {
|
|
||||||
|
|
||||||
// Export search controls to global window for toolbar access
|
|
||||||
(window as any).embedPdfSearch = {
|
|
||||||
search: async (query: string) => {
|
|
||||||
try {
|
|
||||||
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) : [],
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
}, [search, state]);
|
|
||||||
|
|
||||||
return null; // This component doesn't render anything
|
|
||||||
}
|
|
@ -1,216 +0,0 @@
|
|||||||
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';
|
|
||||||
|
|
||||||
interface SearchInterfaceProps {
|
|
||||||
visible: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
|
||||||
const [resultInfo, setResultInfo] = useState<{
|
|
||||||
currentIndex: number;
|
|
||||||
totalResults: number;
|
|
||||||
query: string;
|
|
||||||
} | null>(null);
|
|
||||||
const [isSearching, setIsSearching] = useState(false);
|
|
||||||
|
|
||||||
// Monitor search state changes
|
|
||||||
useEffect(() => {
|
|
||||||
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;
|
|
||||||
|
|
||||||
setResultInfo({
|
|
||||||
currentIndex: activeIndex,
|
|
||||||
totalResults: searchResults ? searchResults.length : 0,
|
|
||||||
query: state.query
|
|
||||||
});
|
|
||||||
} else if (state && !state.active) {
|
|
||||||
setResultInfo(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSearching(state ? state.loading : false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check immediately and then poll for updates
|
|
||||||
checkSearchState();
|
|
||||||
const interval = setInterval(checkSearchState, 200);
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, [visible]);
|
|
||||||
|
|
||||||
const handleSearch = async (query: string) => {
|
|
||||||
if (!query.trim()) return;
|
|
||||||
|
|
||||||
const searchAPI = (window as any).embedPdfSearch;
|
|
||||||
if (searchAPI) {
|
|
||||||
setIsSearching(true);
|
|
||||||
try {
|
|
||||||
await searchAPI.search(query.trim());
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Search failed:', error);
|
|
||||||
} finally {
|
|
||||||
setIsSearching(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
handleSearch(searchQuery);
|
|
||||||
} else if (event.key === 'Escape') {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNext = () => {
|
|
||||||
const searchAPI = (window as any).embedPdfSearch;
|
|
||||||
if (searchAPI) {
|
|
||||||
searchAPI.nextResult();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePrevious = () => {
|
|
||||||
const searchAPI = (window as any).embedPdfSearch;
|
|
||||||
if (searchAPI) {
|
|
||||||
searchAPI.previousResult();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClearSearch = () => {
|
|
||||||
const searchAPI = (window as any).embedPdfSearch;
|
|
||||||
if (searchAPI) {
|
|
||||||
searchAPI.clearSearch();
|
|
||||||
}
|
|
||||||
setSearchQuery('');
|
|
||||||
setResultInfo(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
handleClearSearch();
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!visible) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
style={{
|
|
||||||
position: 'fixed',
|
|
||||||
top: '20px',
|
|
||||||
right: '20px',
|
|
||||||
zIndex: 1000,
|
|
||||||
backgroundColor: 'var(--mantine-color-body)',
|
|
||||||
border: '1px solid var(--mantine-color-gray-3)',
|
|
||||||
borderRadius: '8px',
|
|
||||||
padding: '16px',
|
|
||||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
|
||||||
minWidth: '320px',
|
|
||||||
maxWidth: '400px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Header with close button */}
|
|
||||||
<Group justify="space-between" mb="md">
|
|
||||||
<Text size="sm" fw={600}>
|
|
||||||
{t('search.title', 'Search PDF')}
|
|
||||||
</Text>
|
|
||||||
<ActionIcon
|
|
||||||
variant="subtle"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleClose}
|
|
||||||
aria-label="Close search"
|
|
||||||
>
|
|
||||||
<LocalIcon icon="close" width="1rem" height="1rem" />
|
|
||||||
</ActionIcon>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
{/* Search input */}
|
|
||||||
<Group mb="md">
|
|
||||||
<TextInput
|
|
||||||
placeholder={t('search.placeholder', 'Enter search term...')}
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.currentTarget.value)}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
rightSection={
|
|
||||||
<ActionIcon
|
|
||||||
variant="subtle"
|
|
||||||
onClick={() => handleSearch(searchQuery)}
|
|
||||||
disabled={!searchQuery.trim() || isSearching}
|
|
||||||
loading={isSearching}
|
|
||||||
>
|
|
||||||
<LocalIcon icon="search" width="1rem" height="1rem" />
|
|
||||||
</ActionIcon>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
{/* Results info and navigation */}
|
|
||||||
{resultInfo && (
|
|
||||||
<Group justify="space-between" align="center">
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
{resultInfo.totalResults === 0
|
|
||||||
? t('search.noResults', 'No results found')
|
|
||||||
: t('search.resultCount', '{{current}} of {{total}}', {
|
|
||||||
current: resultInfo.currentIndex,
|
|
||||||
total: resultInfo.totalResults
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{resultInfo.totalResults > 0 && (
|
|
||||||
<Group gap="xs">
|
|
||||||
<ActionIcon
|
|
||||||
variant="subtle"
|
|
||||||
size="sm"
|
|
||||||
onClick={handlePrevious}
|
|
||||||
disabled={resultInfo.currentIndex <= 1}
|
|
||||||
aria-label="Previous result"
|
|
||||||
>
|
|
||||||
<LocalIcon icon="keyboard-arrow-up" width="1rem" height="1rem" />
|
|
||||||
</ActionIcon>
|
|
||||||
<ActionIcon
|
|
||||||
variant="subtle"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleNext}
|
|
||||||
disabled={resultInfo.currentIndex >= resultInfo.totalResults}
|
|
||||||
aria-label="Next result"
|
|
||||||
>
|
|
||||||
<LocalIcon icon="keyboard-arrow-down" width="1rem" height="1rem" />
|
|
||||||
</ActionIcon>
|
|
||||||
<ActionIcon
|
|
||||||
variant="subtle"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleClearSearch}
|
|
||||||
aria-label="Clear search"
|
|
||||||
>
|
|
||||||
<LocalIcon icon="close" width="1rem" height="1rem" />
|
|
||||||
</ActionIcon>
|
|
||||||
</Group>
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Loading state */}
|
|
||||||
{isSearching && (
|
|
||||||
<Text size="xs" c="dimmed" ta="center" mt="sm">
|
|
||||||
{t('search.searching', 'Searching...')}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
import { useThumbnailCapability } from '@embedpdf/plugin-thumbnail/react';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component that runs inside EmbedPDF context and exports thumbnail controls globally
|
|
||||||
*/
|
|
||||||
export function ThumbnailControlsExporter() {
|
|
||||||
const { provides: thumbnail } = useThumbnailCapability();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log('📄 ThumbnailControlsExporter useEffect:', { thumbnail: !!thumbnail });
|
|
||||||
if (thumbnail) {
|
|
||||||
console.log('📄 Exporting thumbnail controls to window:', {
|
|
||||||
availableMethods: Object.keys(thumbnail),
|
|
||||||
renderThumb: typeof thumbnail.renderThumb
|
|
||||||
});
|
|
||||||
// Export thumbnail controls to global window for debugging
|
|
||||||
(window as any).embedPdfThumbnail = {
|
|
||||||
thumbnailAPI: thumbnail,
|
|
||||||
availableMethods: Object.keys(thumbnail),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [thumbnail]);
|
|
||||||
|
|
||||||
return null; // This component doesn't render anything
|
|
||||||
}
|
|
@ -1,226 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { Box, ScrollArea, ActionIcon, Tooltip } from '@mantine/core';
|
|
||||||
import { LocalIcon } from '../shared/LocalIcon';
|
|
||||||
|
|
||||||
interface ThumbnailSidebarProps {
|
|
||||||
visible: boolean;
|
|
||||||
onToggle: () => void;
|
|
||||||
colorScheme: 'light' | 'dark' | 'auto';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ThumbnailSidebar({ visible, onToggle, colorScheme }: ThumbnailSidebarProps) {
|
|
||||||
const [selectedPage, setSelectedPage] = useState<number>(1);
|
|
||||||
const [thumbnails, setThumbnails] = useState<{ [key: number]: string }>({});
|
|
||||||
const [totalPages, setTotalPages] = useState<number>(0);
|
|
||||||
|
|
||||||
// Convert color scheme
|
|
||||||
const actualColorScheme = colorScheme === 'auto' ? 'light' : colorScheme;
|
|
||||||
|
|
||||||
// Get total pages from scroll API
|
|
||||||
useEffect(() => {
|
|
||||||
const scrollAPI = (window as any).embedPdfScroll;
|
|
||||||
if (scrollAPI && scrollAPI.totalPages) {
|
|
||||||
setTotalPages(scrollAPI.totalPages);
|
|
||||||
}
|
|
||||||
}, [visible]);
|
|
||||||
|
|
||||||
// Generate thumbnails when sidebar becomes visible
|
|
||||||
useEffect(() => {
|
|
||||||
if (!visible || totalPages === 0) return;
|
|
||||||
|
|
||||||
const thumbnailAPI = (window as any).embedPdfThumbnail?.thumbnailAPI;
|
|
||||||
console.log('📄 ThumbnailSidebar useEffect triggered:', {
|
|
||||||
visible,
|
|
||||||
thumbnailAPI: !!thumbnailAPI,
|
|
||||||
totalPages,
|
|
||||||
existingThumbnails: Object.keys(thumbnails).length
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!thumbnailAPI) return;
|
|
||||||
|
|
||||||
const generateThumbnails = async () => {
|
|
||||||
console.log('📄 Starting thumbnail generation for', totalPages, 'pages');
|
|
||||||
|
|
||||||
for (let pageIndex = 0; pageIndex < totalPages; pageIndex++) {
|
|
||||||
if (thumbnails[pageIndex]) continue; // Skip if already generated
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log('📄 Attempting to generate thumbnail for page', pageIndex + 1);
|
|
||||||
const thumbTask = thumbnailAPI.renderThumb(pageIndex, 1.0);
|
|
||||||
console.log('📄 Received thumbTask:', thumbTask);
|
|
||||||
|
|
||||||
// Convert Task to Promise and handle properly
|
|
||||||
thumbTask.toPromise().then((thumbBlob: Blob) => {
|
|
||||||
console.log('📄 Thumbnail generated successfully for page', pageIndex + 1, 'blob:', thumbBlob);
|
|
||||||
const thumbUrl = URL.createObjectURL(thumbBlob);
|
|
||||||
console.log('📄 Created blob URL:', thumbUrl);
|
|
||||||
|
|
||||||
setThumbnails(prev => ({
|
|
||||||
...prev,
|
|
||||||
[pageIndex]: thumbUrl
|
|
||||||
}));
|
|
||||||
}).catch((error: any) => {
|
|
||||||
console.error('📄 Failed to generate thumbnail for page', pageIndex + 1, error);
|
|
||||||
setThumbnails(prev => ({
|
|
||||||
...prev,
|
|
||||||
[pageIndex]: 'error'
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to generate thumbnail for page', pageIndex + 1, error);
|
|
||||||
// Set a placeholder or error state
|
|
||||||
setThumbnails(prev => ({
|
|
||||||
...prev,
|
|
||||||
[pageIndex]: 'error'
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
generateThumbnails();
|
|
||||||
|
|
||||||
// Cleanup blob URLs when component unmounts
|
|
||||||
return () => {
|
|
||||||
Object.values(thumbnails).forEach(url => {
|
|
||||||
if (url.startsWith('blob:')) {
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}, [visible, totalPages, thumbnails]);
|
|
||||||
|
|
||||||
const handlePageClick = (pageIndex: number) => {
|
|
||||||
const pageNumber = pageIndex + 1; // Convert to 1-based
|
|
||||||
setSelectedPage(pageNumber);
|
|
||||||
|
|
||||||
// Use scroll API to navigate to page
|
|
||||||
const scrollAPI = (window as any).embedPdfScroll;
|
|
||||||
if (scrollAPI && scrollAPI.scrollToPage) {
|
|
||||||
scrollAPI.scrollToPage(pageNumber);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* Thumbnail Sidebar */}
|
|
||||||
{visible && (
|
|
||||||
<Box
|
|
||||||
style={{
|
|
||||||
position: 'fixed',
|
|
||||||
right: 0,
|
|
||||||
top: 0,
|
|
||||||
bottom: 0,
|
|
||||||
width: '240px',
|
|
||||||
backgroundColor: actualColorScheme === 'dark' ? '#1a1b1e' : '#f8f9fa',
|
|
||||||
borderLeft: `1px solid ${actualColorScheme === 'dark' ? '#373A40' : '#e9ecef'}`,
|
|
||||||
zIndex: 998,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
boxShadow: '-2px 0 8px rgba(0, 0, 0, 0.1)'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Thumbnails Container */}
|
|
||||||
<ScrollArea style={{ flex: 1 }}>
|
|
||||||
<Box p="sm">
|
|
||||||
<div style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: '12px'
|
|
||||||
}}>
|
|
||||||
{Array.from({ length: totalPages }, (_, pageIndex) => (
|
|
||||||
<Box
|
|
||||||
key={pageIndex}
|
|
||||||
onClick={() => handlePageClick(pageIndex)}
|
|
||||||
style={{
|
|
||||||
cursor: 'pointer',
|
|
||||||
borderRadius: '8px',
|
|
||||||
padding: '8px',
|
|
||||||
backgroundColor: selectedPage === pageIndex + 1
|
|
||||||
? (actualColorScheme === 'dark' ? '#364FC7' : '#e7f5ff')
|
|
||||||
: 'transparent',
|
|
||||||
border: selectedPage === pageIndex + 1
|
|
||||||
? '2px solid #1c7ed6'
|
|
||||||
: '2px solid transparent',
|
|
||||||
transition: 'all 0.2s ease',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '8px'
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
if (selectedPage !== pageIndex + 1) {
|
|
||||||
e.currentTarget.style.backgroundColor = actualColorScheme === 'dark' ? '#25262b' : '#f1f3f5';
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
if (selectedPage !== pageIndex + 1) {
|
|
||||||
e.currentTarget.style.backgroundColor = 'transparent';
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Thumbnail Image */}
|
|
||||||
{thumbnails[pageIndex] && thumbnails[pageIndex] !== 'error' ? (
|
|
||||||
<img
|
|
||||||
src={thumbnails[pageIndex]}
|
|
||||||
alt={`Page ${pageIndex + 1} thumbnail`}
|
|
||||||
style={{
|
|
||||||
maxWidth: '100%',
|
|
||||||
height: 'auto',
|
|
||||||
borderRadius: '4px',
|
|
||||||
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
|
|
||||||
border: `1px solid ${actualColorScheme === 'dark' ? '#373A40' : '#e9ecef'}`
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : thumbnails[pageIndex] === 'error' ? (
|
|
||||||
<div style={{
|
|
||||||
width: '180px',
|
|
||||||
height: '240px',
|
|
||||||
backgroundColor: actualColorScheme === 'dark' ? '#2d1b1b' : '#ffebee',
|
|
||||||
border: `1px solid ${actualColorScheme === 'dark' ? '#5d3737' : '#ffcdd2'}`,
|
|
||||||
borderRadius: '4px',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
color: '#d32f2f',
|
|
||||||
fontSize: '12px'
|
|
||||||
}}>
|
|
||||||
Failed
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div style={{
|
|
||||||
width: '180px',
|
|
||||||
height: '240px',
|
|
||||||
backgroundColor: actualColorScheme === 'dark' ? '#25262b' : '#f8f9fa',
|
|
||||||
border: `1px solid ${actualColorScheme === 'dark' ? '#373A40' : '#e9ecef'}`,
|
|
||||||
borderRadius: '4px',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
color: actualColorScheme === 'dark' ? '#adb5bd' : '#6c757d',
|
|
||||||
fontSize: '12px'
|
|
||||||
}}>
|
|
||||||
Loading...
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Page Number */}
|
|
||||||
<div style={{
|
|
||||||
fontSize: '12px',
|
|
||||||
fontWeight: 500,
|
|
||||||
color: selectedPage === pageIndex + 1
|
|
||||||
? (actualColorScheme === 'dark' ? '#ffffff' : '#1c7ed6')
|
|
||||||
: (actualColorScheme === 'dark' ? '#adb5bd' : '#6c757d')
|
|
||||||
}}>
|
|
||||||
Page {pageIndex + 1}
|
|
||||||
</div>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Box>
|
|
||||||
</ScrollArea>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user