import React, { useState, useEffect } from 'react'; import { Box, ScrollArea } from '@mantine/core'; import { useViewer } from '../../contexts/ViewerContext'; interface ThumbnailSidebarProps { visible: boolean; onToggle: () => void; } export function ThumbnailSidebar({ visible, onToggle: _onToggle }: ThumbnailSidebarProps) { const { getScrollState, scrollActions, getThumbnailAPI } = useViewer(); const [thumbnails, setThumbnails] = useState<{ [key: number]: string }>({}); const scrollState = getScrollState(); const thumbnailAPI = getThumbnailAPI(); // Clear thumbnails when sidebar closes and revoke blob URLs to prevent memory leaks useEffect(() => { if (!visible) { Object.values(thumbnails).forEach((thumbUrl) => { // Only revoke if it's a blob URL (not 'error') if (typeof thumbUrl === 'string' && thumbUrl.startsWith('blob:')) { URL.revokeObjectURL(thumbUrl); } }); setThumbnails({}); } }, [visible, thumbnails]); // Generate thumbnails when sidebar becomes visible useEffect(() => { if (!visible || scrollState.totalPages === 0) return; if (!thumbnailAPI) return; const generateThumbnails = async () => { for (let pageIndex = 0; pageIndex < scrollState.totalPages; pageIndex++) { if (thumbnails[pageIndex]) continue; // Skip if already generated try { const thumbTask = thumbnailAPI.renderThumb(pageIndex, 1.0); // Convert Task to Promise and handle properly thumbTask.toPromise().then((thumbBlob: Blob) => { const thumbUrl = URL.createObjectURL(thumbBlob); 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); setThumbnails(prev => ({ ...prev, [pageIndex]: 'error' })); } } }; generateThumbnails(); }, [visible, scrollState.totalPages, thumbnailAPI]); const handlePageClick = (pageIndex: number) => { const pageNumber = pageIndex + 1; // Convert to 1-based scrollActions.scrollToPage(pageNumber); }; return ( <> {/* Thumbnail Sidebar */} {visible && ( {/* Thumbnails Container */}
{Array.from({ length: scrollState.totalPages }, (_, pageIndex) => ( handlePageClick(pageIndex)} style={{ cursor: 'pointer', borderRadius: '8px', padding: '8px', backgroundColor: scrollState.currentPage === pageIndex + 1 ? 'var(--color-primary-100)' : 'transparent', border: scrollState.currentPage === pageIndex + 1 ? '2px solid var(--color-primary-500)' : '2px solid transparent', transition: 'all 0.2s ease', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px' }} onMouseEnter={(e) => { if (scrollState.currentPage !== pageIndex + 1) { e.currentTarget.style.backgroundColor = 'var(--hover-bg)'; } }} onMouseLeave={(e) => { if (scrollState.currentPage !== pageIndex + 1) { e.currentTarget.style.backgroundColor = 'transparent'; } }} > {/* Thumbnail Image */} {thumbnails[pageIndex] && thumbnails[pageIndex] !== 'error' ? ( {`Page ) : thumbnails[pageIndex] === 'error' ? (
Failed
) : (
Loading...
)} {/* Page Number */}
Page {pageIndex + 1}
))}
)} ); }