mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-18 01:19:24 +00:00
thumbnail sidebar
This commit is contained in:
parent
143f0c5031
commit
423617db52
16
frontend/package-lock.json
generated
16
frontend/package-lock.json
generated
@ -20,6 +20,7 @@
|
||||
"@embedpdf/plugin-search": "^1.1.1",
|
||||
"@embedpdf/plugin-selection": "^1.1.1",
|
||||
"@embedpdf/plugin-spread": "^1.1.1",
|
||||
"@embedpdf/plugin-thumbnail": "^1.1.1",
|
||||
"@embedpdf/plugin-tiling": "^1.1.1",
|
||||
"@embedpdf/plugin-viewport": "^1.1.1",
|
||||
"@embedpdf/plugin-zoom": "^1.1.1",
|
||||
@ -779,6 +780,21 @@
|
||||
"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": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-tiling/-/plugin-tiling-1.1.1.tgz",
|
||||
|
@ -16,6 +16,7 @@
|
||||
"@embedpdf/plugin-search": "^1.1.1",
|
||||
"@embedpdf/plugin-selection": "^1.1.1",
|
||||
"@embedpdf/plugin-spread": "^1.1.1",
|
||||
"@embedpdf/plugin-thumbnail": "^1.1.1",
|
||||
"@embedpdf/plugin-tiling": "^1.1.1",
|
||||
"@embedpdf/plugin-viewport": "^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 "
|
||||
style={{
|
||||
transition: 'opacity 0.15s ease-in-out',
|
||||
marginTop: '1rem',
|
||||
marginTop: 0,
|
||||
}}
|
||||
>
|
||||
{renderMainContent()}
|
||||
|
@ -260,7 +260,7 @@ export default function RightRail() {
|
||||
variant="subtle"
|
||||
radius="md"
|
||||
className="right-rail-icon"
|
||||
onClick={() => (window as any).embedPdfControls?.sidebar()}
|
||||
onClick={() => (window as any).toggleThumbnailSidebar?.()}
|
||||
disabled={currentView !== 'viewer'}
|
||||
>
|
||||
<LocalIcon icon="view-list" width="1.5rem" height="1.5rem" />
|
||||
|
@ -37,43 +37,25 @@ export function CustomSearchLayer({
|
||||
// Subscribe to search result state changes
|
||||
useEffect(() => {
|
||||
if (!searchProvides) {
|
||||
console.log('🔍 CustomSearchLayer: No search provides available for page', pageIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🔍 CustomSearchLayer: Setting up search result subscription for page', pageIndex);
|
||||
console.log('🔍 CustomSearchLayer: Available search methods:', Object.keys(searchProvides));
|
||||
|
||||
const unsubscribe = searchProvides.onSearchResultStateChange?.((state: SearchResultState) => {
|
||||
console.log('🔍 CustomSearchLayer: Search result state changed for page', pageIndex, ':', {
|
||||
state,
|
||||
resultsCount: state?.results?.length || 0,
|
||||
activeIndex: state?.activeResultIndex,
|
||||
results: state?.results
|
||||
});
|
||||
|
||||
// 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
|
||||
console.log('🔍 CustomSearchLayer: Exposed global search data:', {
|
||||
totalResults: state.results.length,
|
||||
activeIndex: (state.activeResultIndex || 0) + 1
|
||||
});
|
||||
|
||||
// Auto-scroll to active result if we have one
|
||||
if (state.activeResultIndex !== undefined && state.activeResultIndex >= 0) {
|
||||
const activeResult = state.results[state.activeResultIndex];
|
||||
if (activeResult) {
|
||||
console.log('🔍 CustomSearchLayer: Auto-scrolling to active result on page', activeResult.pageIndex);
|
||||
|
||||
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);
|
||||
console.log('🔍 CustomSearchLayer: Scrolled to page', pageNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -85,9 +67,6 @@ export function CustomSearchLayer({
|
||||
setSearchResultState(state);
|
||||
});
|
||||
|
||||
if (!unsubscribe) {
|
||||
console.warn('🔍 CustomSearchLayer: No onSearchResultStateChange method available');
|
||||
}
|
||||
|
||||
return unsubscribe;
|
||||
}, [searchProvides, pageIndex]);
|
||||
@ -95,7 +74,6 @@ export function CustomSearchLayer({
|
||||
// Filter results for current page while preserving original indices
|
||||
const pageResults = useMemo(() => {
|
||||
if (!searchResultState?.results) {
|
||||
console.log(`🔍 CustomSearchLayer: No search results for page ${pageIndex} (no results array)`);
|
||||
return [];
|
||||
}
|
||||
|
||||
@ -103,13 +81,6 @@ export function CustomSearchLayer({
|
||||
.map((result, originalIndex) => ({ result, originalIndex }))
|
||||
.filter(({ result }) => result.pageIndex === pageIndex);
|
||||
|
||||
console.log(`🔍 CustomSearchLayer: Page ${pageIndex} filtering:`, {
|
||||
totalResults: searchResultState.results.length,
|
||||
pageResults: filtered.length,
|
||||
allPageIndices: searchResultState.results.map(r => r.pageIndex),
|
||||
currentPage: pageIndex,
|
||||
filtered
|
||||
});
|
||||
return filtered;
|
||||
}, [searchResultState, pageIndex]);
|
||||
|
||||
|
@ -9,6 +9,7 @@ import { useFileWithUrl } from "../../hooks/useFileWithUrl";
|
||||
import { LocalEmbedPDF } from './LocalEmbedPDF';
|
||||
import { PdfViewerToolbar } from './PdfViewerToolbar';
|
||||
import { SearchInterface } from './SearchInterface';
|
||||
import { ThumbnailSidebar } from './ThumbnailSidebar';
|
||||
|
||||
export interface EmbedPdfViewerProps {
|
||||
sidebarsVisible: boolean;
|
||||
@ -29,6 +30,7 @@ const EmbedPdfViewer = ({
|
||||
const viewerRef = React.useRef<HTMLDivElement>(null);
|
||||
const [isViewerHovered, setIsViewerHovered] = React.useState(false);
|
||||
const [isSearchVisible, setIsSearchVisible] = React.useState(false);
|
||||
const [isThumbnailSidebarVisible, setIsThumbnailSidebarVisible] = React.useState(false);
|
||||
|
||||
// Get current file from FileContext
|
||||
const { selectors } = useFileState();
|
||||
@ -118,14 +120,19 @@ const EmbedPdfViewer = ({
|
||||
};
|
||||
}, [isViewerHovered]);
|
||||
|
||||
// Expose search toggle function globally for right rail button
|
||||
// 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;
|
||||
};
|
||||
}, []);
|
||||
|
||||
@ -170,60 +177,68 @@ const EmbedPdfViewer = ({
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* EmbedPDF Viewer with Toolbar Overlay */}
|
||||
{/* EmbedPDF Viewer */}
|
||||
<Box style={{
|
||||
position: 'relative',
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
minHeight: 0,
|
||||
minWidth: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
minWidth: 0
|
||||
}}>
|
||||
<LocalEmbedPDF
|
||||
file={effectiveFile.file}
|
||||
url={effectiveFile.url}
|
||||
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>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
);
|
||||
};
|
||||
|
@ -15,6 +15,7 @@ import { TilingLayer, TilingPluginPackage } from '@embedpdf/plugin-tiling/react'
|
||||
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 { CustomSearchLayer } from './CustomSearchLayer';
|
||||
import { ZoomControlsExporter } from './ZoomControlsExporter';
|
||||
import { ScrollControlsExporter } from './ScrollControlsExporter';
|
||||
@ -22,6 +23,7 @@ import { SelectionControlsExporter } from './SelectionControlsExporter';
|
||||
import { PanControlsExporter } from './PanControlsExporter';
|
||||
import { SpreadControlsExporter } from './SpreadControlsExporter';
|
||||
import { SearchControlsExporter } from './SearchControlsExporter';
|
||||
import { ThumbnailControlsExporter } from './ThumbnailControlsExporter';
|
||||
|
||||
interface LocalEmbedPDFProps {
|
||||
file?: File | Blob;
|
||||
@ -101,6 +103,9 @@ export function LocalEmbedPDF({ file, url, colorScheme }: LocalEmbedPDFProps) {
|
||||
|
||||
// Register search plugin for text search
|
||||
createPluginRegistration(SearchPluginPackage),
|
||||
|
||||
// Register thumbnail plugin for page thumbnails
|
||||
createPluginRegistration(ThumbnailPluginPackage),
|
||||
];
|
||||
}, [pdfUrl]);
|
||||
|
||||
@ -181,6 +186,7 @@ export function LocalEmbedPDF({ file, url, colorScheme }: LocalEmbedPDFProps) {
|
||||
<PanControlsExporter />
|
||||
<SpreadControlsExporter />
|
||||
<SearchControlsExporter />
|
||||
<ThumbnailControlsExporter />
|
||||
<GlobalPointerProvider>
|
||||
<Viewport
|
||||
style={{
|
||||
|
@ -9,62 +9,30 @@ export function SearchControlsExporter() {
|
||||
|
||||
useEffect(() => {
|
||||
if (search && state) {
|
||||
// Debug: log the actual search hook structure
|
||||
console.log('🔍 EmbedPDF search hook structure:', {
|
||||
search,
|
||||
state,
|
||||
searchKeys: Object.keys(search),
|
||||
stateKeys: Object.keys(state),
|
||||
currentQuery: state.query,
|
||||
isActive: state.active,
|
||||
isLoading: state.loading
|
||||
});
|
||||
|
||||
// Export search controls to global window for toolbar access
|
||||
(window as any).embedPdfSearch = {
|
||||
search: async (query: string) => {
|
||||
console.log('🔍 EmbedPDF: Starting search for:', query);
|
||||
console.log('🔍 Search API available:', !!search);
|
||||
console.log('🔍 Search API methods:', search ? Object.keys(search) : 'none');
|
||||
|
||||
try {
|
||||
// First start the search session
|
||||
search.startSearch();
|
||||
console.log('🔍 startSearch() called');
|
||||
|
||||
// Then search all pages with the query
|
||||
const results = await search.searchAllPages(query);
|
||||
console.log('🔍 searchAllPages() results:', results);
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error('🔍 Search error:', error);
|
||||
console.error('Search error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
clearSearch: () => {
|
||||
console.log('🔍 EmbedPDF: Stopping search');
|
||||
search.stopSearch();
|
||||
},
|
||||
nextResult: () => {
|
||||
console.log('🔍 EmbedPDF: Going to next search result');
|
||||
const newIndex = search.nextResult();
|
||||
console.log('🔍 EmbedPDF: New active result index:', newIndex);
|
||||
// The goToResult method should handle scrolling automatically
|
||||
return newIndex;
|
||||
return search.nextResult();
|
||||
},
|
||||
previousResult: () => {
|
||||
console.log('🔍 EmbedPDF: Going to previous search result');
|
||||
const newIndex = search.previousResult();
|
||||
console.log('🔍 EmbedPDF: New active result index:', newIndex);
|
||||
// The goToResult method should handle scrolling automatically
|
||||
return newIndex;
|
||||
return search.previousResult();
|
||||
},
|
||||
goToResult: (index: number) => {
|
||||
console.log('🔍 EmbedPDF: Going to search result:', index);
|
||||
const resultIndex = search.goToResult(index);
|
||||
console.log('🔍 EmbedPDF: Navigated to result index:', resultIndex);
|
||||
return resultIndex;
|
||||
return search.goToResult(index);
|
||||
},
|
||||
// State getters
|
||||
getSearchQuery: () => state.query,
|
||||
@ -77,16 +45,6 @@ export function SearchControlsExporter() {
|
||||
availableMethods: search ? Object.keys(search) : [],
|
||||
};
|
||||
|
||||
console.log('🔍 EmbedPDF search controls exported to window.embedPdfSearch', {
|
||||
currentQuery: state.query,
|
||||
isActive: state.active,
|
||||
isLoading: state.loading,
|
||||
searchAPI: search,
|
||||
availableMethods: search ? Object.keys(search) : 'no search API',
|
||||
state: state
|
||||
});
|
||||
} else {
|
||||
console.warn('EmbedPDF search hook not available yet');
|
||||
}
|
||||
}, [search, state]);
|
||||
|
||||
|
@ -27,25 +27,11 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
||||
if (searchAPI) {
|
||||
const state = searchAPI.state;
|
||||
|
||||
console.log('🔍 SearchInterface: Checking search state:', {
|
||||
state,
|
||||
hasState: !!state,
|
||||
query: state?.query,
|
||||
active: state?.active,
|
||||
loading: state?.loading
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
console.log('🔍 SearchInterface: Search results data:', {
|
||||
searchResults,
|
||||
activeIndex,
|
||||
totalResults: searchResults ? searchResults.length : 0
|
||||
});
|
||||
|
||||
setResultInfo({
|
||||
currentIndex: activeIndex,
|
||||
@ -74,8 +60,7 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
||||
if (searchAPI) {
|
||||
setIsSearching(true);
|
||||
try {
|
||||
const results = await searchAPI.search(query.trim());
|
||||
console.log('Search completed:', results);
|
||||
await searchAPI.search(query.trim());
|
||||
} catch (error) {
|
||||
console.error('Search failed:', error);
|
||||
} finally {
|
||||
@ -95,16 +80,14 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
||||
const handleNext = () => {
|
||||
const searchAPI = (window as any).embedPdfSearch;
|
||||
if (searchAPI) {
|
||||
const newIndex = searchAPI.nextResult();
|
||||
console.log('Next result:', newIndex);
|
||||
searchAPI.nextResult();
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrevious = () => {
|
||||
const searchAPI = (window as any).embedPdfSearch;
|
||||
if (searchAPI) {
|
||||
const newIndex = searchAPI.previousResult();
|
||||
console.log('Previous result:', newIndex);
|
||||
searchAPI.previousResult();
|
||||
}
|
||||
};
|
||||
|
||||
|
26
frontend/src/components/viewer/ThumbnailControlsExporter.tsx
Normal file
26
frontend/src/components/viewer/ThumbnailControlsExporter.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
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
|
||||
}
|
226
frontend/src/components/viewer/ThumbnailSidebar.tsx
Normal file
226
frontend/src/components/viewer/ThumbnailSidebar.tsx
Normal file
@ -0,0 +1,226 @@
|
||||
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