mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-09-26 17:52:59 +02:00
Context based right rail controls for viewer
This commit is contained in:
parent
1709ca9049
commit
21a93d6cac
@ -6,9 +6,11 @@ import CloseIcon from '@mui/icons-material/Close';
|
|||||||
|
|
||||||
import { useFileState } from "../../contexts/FileContext";
|
import { useFileState } from "../../contexts/FileContext";
|
||||||
import { useFileWithUrl } from "../../hooks/useFileWithUrl";
|
import { useFileWithUrl } from "../../hooks/useFileWithUrl";
|
||||||
|
import { ViewerProvider, useViewer } from "../../contexts/ViewerContext";
|
||||||
import { LocalEmbedPDF } from './LocalEmbedPDF';
|
import { LocalEmbedPDF } from './LocalEmbedPDF';
|
||||||
import { PdfViewerToolbar } from './PdfViewerToolbar';
|
import { PdfViewerToolbar } from './PdfViewerToolbar';
|
||||||
import { ThumbnailSidebar } from './ThumbnailSidebar';
|
import { ThumbnailSidebar } from './ThumbnailSidebar';
|
||||||
|
import '../../types/embedPdf';
|
||||||
|
|
||||||
export interface EmbedPdfViewerProps {
|
export interface EmbedPdfViewerProps {
|
||||||
sidebarsVisible: boolean;
|
sidebarsVisible: boolean;
|
||||||
@ -17,7 +19,7 @@ export interface EmbedPdfViewerProps {
|
|||||||
previewFile?: File | null;
|
previewFile?: File | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EmbedPdfViewer = ({
|
const EmbedPdfViewerContent = ({
|
||||||
sidebarsVisible,
|
sidebarsVisible,
|
||||||
setSidebarsVisible,
|
setSidebarsVisible,
|
||||||
onClose,
|
onClose,
|
||||||
@ -28,7 +30,7 @@ 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 [isThumbnailSidebarVisible, setIsThumbnailSidebarVisible] = React.useState(false);
|
const { isThumbnailSidebarVisible, toggleThumbnailSidebar } = useViewer();
|
||||||
|
|
||||||
// Get current file from FileContext
|
// Get current file from FileContext
|
||||||
const { selectors } = useFileState();
|
const { selectors } = useFileState();
|
||||||
@ -68,7 +70,7 @@ const EmbedPdfViewer = ({
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
const zoomAPI = (window as any).embedPdfZoom;
|
const zoomAPI = window.embedPdfZoom;
|
||||||
if (zoomAPI) {
|
if (zoomAPI) {
|
||||||
if (event.deltaY < 0) {
|
if (event.deltaY < 0) {
|
||||||
// Scroll up - zoom in
|
// Scroll up - zoom in
|
||||||
@ -97,7 +99,7 @@ const EmbedPdfViewer = ({
|
|||||||
|
|
||||||
// Check if Ctrl (Windows/Linux) or Cmd (Mac) is pressed
|
// Check if Ctrl (Windows/Linux) or Cmd (Mac) is pressed
|
||||||
if (event.ctrlKey || event.metaKey) {
|
if (event.ctrlKey || event.metaKey) {
|
||||||
const zoomAPI = (window as any).embedPdfZoom;
|
const zoomAPI = window.embedPdfZoom;
|
||||||
if (zoomAPI) {
|
if (zoomAPI) {
|
||||||
if (event.key === '=' || event.key === '+') {
|
if (event.key === '=' || event.key === '+') {
|
||||||
// Ctrl+= or Ctrl++ for zoom in
|
// Ctrl+= or Ctrl++ for zoom in
|
||||||
@ -120,14 +122,12 @@ const EmbedPdfViewer = ({
|
|||||||
|
|
||||||
// Expose toggle functions globally for right rail buttons
|
// Expose toggle functions globally for right rail buttons
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
(window as any).toggleThumbnailSidebar = () => {
|
window.toggleThumbnailSidebar = toggleThumbnailSidebar;
|
||||||
setIsThumbnailSidebarVisible(prev => !prev);
|
|
||||||
};
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
delete (window as any).toggleThumbnailSidebar;
|
delete window.toggleThumbnailSidebar;
|
||||||
};
|
};
|
||||||
}, []);
|
}, [toggleThumbnailSidebar]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@ -212,7 +212,7 @@ const EmbedPdfViewer = ({
|
|||||||
}}
|
}}
|
||||||
dualPage={false}
|
dualPage={false}
|
||||||
onDualPageToggle={() => {
|
onDualPageToggle={() => {
|
||||||
(window as any).embedPdfSpread?.toggleSpreadMode();
|
window.embedPdfSpread?.toggleSpreadMode();
|
||||||
}}
|
}}
|
||||||
currentZoom={100}
|
currentZoom={100}
|
||||||
/>
|
/>
|
||||||
@ -224,11 +224,19 @@ const EmbedPdfViewer = ({
|
|||||||
{/* Thumbnail Sidebar */}
|
{/* Thumbnail Sidebar */}
|
||||||
<ThumbnailSidebar
|
<ThumbnailSidebar
|
||||||
visible={isThumbnailSidebarVisible}
|
visible={isThumbnailSidebarVisible}
|
||||||
onToggle={() => setIsThumbnailSidebarVisible(prev => !prev)}
|
onToggle={toggleThumbnailSidebar}
|
||||||
colorScheme={colorScheme}
|
colorScheme={colorScheme}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const EmbedPdfViewer = (props: EmbedPdfViewerProps) => {
|
||||||
|
return (
|
||||||
|
<ViewerProvider>
|
||||||
|
<EmbedPdfViewerContent {...props} />
|
||||||
|
</ViewerProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default EmbedPdfViewer;
|
export default EmbedPdfViewer;
|
@ -1,687 +0,0 @@
|
|||||||
import React, { useEffect, useState, useRef, useCallback } from "react";
|
|
||||||
import { Paper, Stack, Text, ScrollArea, Center, Button, Group, NumberInput, useMantineTheme, ActionIcon, Box, Tabs } from "@mantine/core";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { pdfWorkerManager } from "../../services/pdfWorkerManager";
|
|
||||||
import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew";
|
|
||||||
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
|
|
||||||
import FirstPageIcon from "@mui/icons-material/FirstPage";
|
|
||||||
import LastPageIcon from "@mui/icons-material/LastPage";
|
|
||||||
import ViewWeekIcon from "@mui/icons-material/ViewWeek"; // for dual page (book)
|
|
||||||
import DescriptionIcon from "@mui/icons-material/Description"; // for single page
|
|
||||||
import CloseIcon from "@mui/icons-material/Close";
|
|
||||||
import { fileStorage } from "../../services/fileStorage";
|
|
||||||
import SkeletonLoader from '../shared/SkeletonLoader';
|
|
||||||
import { useFileState } from "../../contexts/FileContext";
|
|
||||||
import { useFileWithUrl } from "../../hooks/useFileWithUrl";
|
|
||||||
import { isFileObject } from "../../types/fileContext";
|
|
||||||
import { FileId } from "../../types/file";
|
|
||||||
|
|
||||||
|
|
||||||
// Lazy loading page image component
|
|
||||||
interface LazyPageImageProps {
|
|
||||||
pageIndex: number;
|
|
||||||
zoom: number;
|
|
||||||
theme: any;
|
|
||||||
isFirst: boolean;
|
|
||||||
renderPage: (pageIndex: number) => Promise<string | null>;
|
|
||||||
pageImages: (string | null)[];
|
|
||||||
setPageRef: (index: number, ref: HTMLImageElement | null) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const LazyPageImage = ({
|
|
||||||
pageIndex, zoom, theme, isFirst, renderPage, pageImages, setPageRef
|
|
||||||
}: LazyPageImageProps) => {
|
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
|
||||||
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
|
||||||
const imgRef = useRef<HTMLImageElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
if (entry.isIntersecting && !imageUrl) {
|
|
||||||
setIsVisible(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rootMargin: '200px', // Start loading 200px before visible
|
|
||||||
threshold: 0.1
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (imgRef.current) {
|
|
||||||
observer.observe(imgRef.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => observer.disconnect();
|
|
||||||
}, [imageUrl]);
|
|
||||||
|
|
||||||
// Update local state when pageImages changes (from preloading)
|
|
||||||
useEffect(() => {
|
|
||||||
if (pageImages[pageIndex]) {
|
|
||||||
setImageUrl(pageImages[pageIndex]);
|
|
||||||
}
|
|
||||||
}, [pageImages, pageIndex]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isVisible && !imageUrl) {
|
|
||||||
renderPage(pageIndex).then((url) => {
|
|
||||||
if (url) setImageUrl(url);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [isVisible, imageUrl, pageIndex, renderPage]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (imgRef.current) {
|
|
||||||
setPageRef(pageIndex, imgRef.current);
|
|
||||||
}
|
|
||||||
}, [pageIndex, setPageRef]);
|
|
||||||
|
|
||||||
if (imageUrl) {
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
ref={imgRef}
|
|
||||||
src={imageUrl}
|
|
||||||
alt={`Page ${pageIndex + 1}`}
|
|
||||||
style={{
|
|
||||||
width: `${100 * zoom}%`,
|
|
||||||
maxWidth: 700 * zoom,
|
|
||||||
boxShadow: "0 2px 8px rgba(0,0,0,0.08)",
|
|
||||||
borderRadius: 8,
|
|
||||||
marginTop: isFirst ? theme.spacing.xl : 0,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Placeholder while loading
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={imgRef}
|
|
||||||
style={{
|
|
||||||
width: `${100 * zoom}%`,
|
|
||||||
maxWidth: 700 * zoom,
|
|
||||||
height: 800 * zoom, // Estimated height
|
|
||||||
backgroundColor: '#f5f5f5',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
borderRadius: 8,
|
|
||||||
marginTop: isFirst ? theme.spacing.xl : 0,
|
|
||||||
border: '1px dashed #ccc'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isVisible ? (
|
|
||||||
<div style={{ textAlign: 'center' }}>
|
|
||||||
<div style={{
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
border: '2px solid #ddd',
|
|
||||||
borderTop: '2px solid #666',
|
|
||||||
borderRadius: '50%',
|
|
||||||
animation: 'spin 1s linear infinite',
|
|
||||||
margin: '0 auto 8px'
|
|
||||||
}} />
|
|
||||||
<Text size="sm" c="dimmed">Loading page {pageIndex + 1}...</Text>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Text size="sm" c="dimmed">Page {pageIndex + 1}</Text>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface ViewerProps {
|
|
||||||
sidebarsVisible: boolean;
|
|
||||||
setSidebarsVisible: (v: boolean) => void;
|
|
||||||
onClose?: () => void;
|
|
||||||
previewFile: File | null; // For preview mode - bypasses context
|
|
||||||
}
|
|
||||||
|
|
||||||
const Viewer = ({
|
|
||||||
onClose,
|
|
||||||
previewFile,
|
|
||||||
}: ViewerProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const theme = useMantineTheme();
|
|
||||||
|
|
||||||
// Get current file from FileContext
|
|
||||||
const { selectors } = useFileState();
|
|
||||||
|
|
||||||
const activeFiles = selectors.getFiles();
|
|
||||||
|
|
||||||
// Tab management for multiple files
|
|
||||||
const [activeTab, setActiveTab] = useState<string>("0");
|
|
||||||
|
|
||||||
// Reset PDF state when switching tabs
|
|
||||||
const handleTabChange = (newTab: string) => {
|
|
||||||
setActiveTab(newTab);
|
|
||||||
setNumPages(0);
|
|
||||||
setPageImages([]);
|
|
||||||
setCurrentPage(null);
|
|
||||||
setLoading(true);
|
|
||||||
};
|
|
||||||
const [numPages, setNumPages] = useState<number>(0);
|
|
||||||
const [pageImages, setPageImages] = useState<string[]>([]);
|
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
|
||||||
const [currentPage, setCurrentPage] = useState<number | null>(null);
|
|
||||||
const [dualPage, setDualPage] = useState(false);
|
|
||||||
const [zoom, setZoom] = useState(1); // 1 = 100%
|
|
||||||
const pageRefs = useRef<(HTMLImageElement | null)[]>([]);
|
|
||||||
|
|
||||||
// Memoize setPageRef to prevent infinite re-renders
|
|
||||||
const setPageRef = useCallback((index: number, ref: HTMLImageElement | null) => {
|
|
||||||
pageRefs.current[index] = ref;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Get files with URLs for tabs - we'll need to create these individually
|
|
||||||
const file0WithUrl = useFileWithUrl(activeFiles[0]);
|
|
||||||
const file1WithUrl = useFileWithUrl(activeFiles[1]);
|
|
||||||
const file2WithUrl = useFileWithUrl(activeFiles[2]);
|
|
||||||
const file3WithUrl = useFileWithUrl(activeFiles[3]);
|
|
||||||
const file4WithUrl = useFileWithUrl(activeFiles[4]);
|
|
||||||
|
|
||||||
const filesWithUrls = React.useMemo(() => {
|
|
||||||
return [file0WithUrl, file1WithUrl, file2WithUrl, file3WithUrl, file4WithUrl]
|
|
||||||
.slice(0, activeFiles.length)
|
|
||||||
.filter(Boolean);
|
|
||||||
}, [file0WithUrl, file1WithUrl, file2WithUrl, file3WithUrl, file4WithUrl, activeFiles.length]);
|
|
||||||
|
|
||||||
// Use preview file if available, otherwise use active tab file
|
|
||||||
const effectiveFile = React.useMemo(() => {
|
|
||||||
if (previewFile) {
|
|
||||||
// Validate the preview file
|
|
||||||
if (!isFileObject(previewFile)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previewFile.size === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { file: previewFile, url: null };
|
|
||||||
} else {
|
|
||||||
// Use the file from the active tab
|
|
||||||
const tabIndex = parseInt(activeTab);
|
|
||||||
return filesWithUrls[tabIndex] || null;
|
|
||||||
}
|
|
||||||
}, [previewFile, filesWithUrls, activeTab]);
|
|
||||||
|
|
||||||
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
|
||||||
const pdfDocRef = useRef<any>(null);
|
|
||||||
const renderingPagesRef = useRef<Set<number>>(new Set());
|
|
||||||
const currentArrayBufferRef = useRef<ArrayBuffer | null>(null);
|
|
||||||
const preloadingRef = useRef<boolean>(false);
|
|
||||||
|
|
||||||
// Function to render a specific page on-demand
|
|
||||||
const renderPage = async (pageIndex: number): Promise<string | null> => {
|
|
||||||
if (!pdfDocRef.current || renderingPagesRef.current.has(pageIndex)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pageNum = pageIndex + 1;
|
|
||||||
if (pageImages[pageIndex]) {
|
|
||||||
return pageImages[pageIndex]; // Already rendered
|
|
||||||
}
|
|
||||||
|
|
||||||
renderingPagesRef.current.add(pageIndex);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const page = await pdfDocRef.current.getPage(pageNum);
|
|
||||||
const viewport = page.getViewport({ scale: 1.2 });
|
|
||||||
const canvas = document.createElement("canvas");
|
|
||||||
canvas.width = viewport.width;
|
|
||||||
canvas.height = viewport.height;
|
|
||||||
const ctx = canvas.getContext("2d");
|
|
||||||
|
|
||||||
if (ctx) {
|
|
||||||
await page.render({ canvasContext: ctx, viewport }).promise;
|
|
||||||
const dataUrl = canvas.toDataURL();
|
|
||||||
|
|
||||||
// Update the pageImages array
|
|
||||||
setPageImages(prev => {
|
|
||||||
const newImages = [...prev];
|
|
||||||
newImages[pageIndex] = dataUrl;
|
|
||||||
return newImages;
|
|
||||||
});
|
|
||||||
|
|
||||||
renderingPagesRef.current.delete(pageIndex);
|
|
||||||
return dataUrl;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to render page ${pageNum}:`, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderingPagesRef.current.delete(pageIndex);
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Progressive preloading function
|
|
||||||
const startProgressivePreload = async () => {
|
|
||||||
if (!pdfDocRef.current || preloadingRef.current || numPages === 0) return;
|
|
||||||
|
|
||||||
preloadingRef.current = true;
|
|
||||||
|
|
||||||
// Start with first few pages for immediate viewing
|
|
||||||
const priorityPages = [0, 1, 2, 3, 4]; // First 5 pages
|
|
||||||
|
|
||||||
// Render priority pages first
|
|
||||||
for (const pageIndex of priorityPages) {
|
|
||||||
if (pageIndex < numPages && !pageImages[pageIndex]) {
|
|
||||||
await renderPage(pageIndex);
|
|
||||||
// Small delay to allow UI to update
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 50));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then render remaining pages in background
|
|
||||||
for (let pageIndex = 5; pageIndex < numPages; pageIndex++) {
|
|
||||||
if (!pageImages[pageIndex]) {
|
|
||||||
await renderPage(pageIndex);
|
|
||||||
// Longer delay for background loading to not block UI
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
preloadingRef.current = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize current page when PDF loads
|
|
||||||
useEffect(() => {
|
|
||||||
if (numPages > 0 && !currentPage) {
|
|
||||||
setCurrentPage(1);
|
|
||||||
}
|
|
||||||
}, [numPages, currentPage]);
|
|
||||||
|
|
||||||
// Function to scroll to a specific page
|
|
||||||
const scrollToPage = (pageNumber: number) => {
|
|
||||||
const el = pageRefs.current[pageNumber - 1];
|
|
||||||
const scrollArea = scrollAreaRef.current;
|
|
||||||
|
|
||||||
if (el && scrollArea) {
|
|
||||||
const scrollAreaRect = scrollArea.getBoundingClientRect();
|
|
||||||
const elRect = el.getBoundingClientRect();
|
|
||||||
const currentScrollTop = scrollArea.scrollTop;
|
|
||||||
|
|
||||||
// Position page near top of viewport with some padding
|
|
||||||
const targetScrollTop = currentScrollTop + (elRect.top - scrollAreaRect.top) - 20;
|
|
||||||
|
|
||||||
scrollArea.scrollTo({
|
|
||||||
top: targetScrollTop,
|
|
||||||
behavior: "smooth"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Throttled scroll handler to prevent jerky updates
|
|
||||||
const handleScrollThrottled = useCallback(() => {
|
|
||||||
const scrollArea = scrollAreaRef.current;
|
|
||||||
if (!scrollArea || !pageRefs.current.length) return;
|
|
||||||
|
|
||||||
const areaRect = scrollArea.getBoundingClientRect();
|
|
||||||
const viewportCenter = areaRect.top + areaRect.height / 2;
|
|
||||||
let closestIdx = 0;
|
|
||||||
let minDist = Infinity;
|
|
||||||
|
|
||||||
pageRefs.current.forEach((img, idx) => {
|
|
||||||
if (img) {
|
|
||||||
const imgRect = img.getBoundingClientRect();
|
|
||||||
const imgCenter = imgRect.top + imgRect.height / 2;
|
|
||||||
const dist = Math.abs(imgCenter - viewportCenter);
|
|
||||||
if (dist < minDist) {
|
|
||||||
minDist = dist;
|
|
||||||
closestIdx = idx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update page number display only if changed
|
|
||||||
if (currentPage !== closestIdx + 1) {
|
|
||||||
setCurrentPage(closestIdx + 1);
|
|
||||||
}
|
|
||||||
}, [currentPage]);
|
|
||||||
|
|
||||||
// Throttle scroll events to reduce jerkiness
|
|
||||||
const handleScroll = useCallback(() => {
|
|
||||||
if (window.requestAnimationFrame) {
|
|
||||||
window.requestAnimationFrame(handleScrollThrottled);
|
|
||||||
} else {
|
|
||||||
handleScrollThrottled();
|
|
||||||
}
|
|
||||||
}, [handleScrollThrottled]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let cancelled = false;
|
|
||||||
async function loadPdfInfo() {
|
|
||||||
if (!effectiveFile) {
|
|
||||||
setNumPages(0);
|
|
||||||
setPageImages([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
let pdfData;
|
|
||||||
|
|
||||||
// For preview files, use ArrayBuffer directly to avoid blob URL issues
|
|
||||||
if (previewFile && effectiveFile.file === previewFile) {
|
|
||||||
const arrayBuffer = await previewFile.arrayBuffer();
|
|
||||||
pdfData = { data: arrayBuffer };
|
|
||||||
}
|
|
||||||
// Handle special IndexedDB URLs for large files
|
|
||||||
else if (effectiveFile.url?.startsWith('indexeddb:')) {
|
|
||||||
const fileId = effectiveFile.url.replace('indexeddb:', '') as FileId /* FIX ME: Not sure this is right - at least probably not the right place for this logic */;
|
|
||||||
|
|
||||||
// Get data directly from IndexedDB
|
|
||||||
const arrayBuffer = await fileStorage.getFileData(fileId);
|
|
||||||
if (!arrayBuffer) {
|
|
||||||
throw new Error('File not found in IndexedDB - may have been purged by browser');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store reference for cleanup
|
|
||||||
currentArrayBufferRef.current = arrayBuffer;
|
|
||||||
pdfData = { data: arrayBuffer };
|
|
||||||
} else if (effectiveFile.url) {
|
|
||||||
// Standard blob URL or regular URL
|
|
||||||
pdfData = effectiveFile.url;
|
|
||||||
} else {
|
|
||||||
throw new Error('No valid PDF source available');
|
|
||||||
}
|
|
||||||
|
|
||||||
const pdf = await pdfWorkerManager.createDocument(pdfData);
|
|
||||||
pdfDocRef.current = pdf;
|
|
||||||
setNumPages(pdf.numPages);
|
|
||||||
if (!cancelled) {
|
|
||||||
setPageImages(new Array(pdf.numPages).fill(null));
|
|
||||||
// Start progressive preloading after a short delay
|
|
||||||
setTimeout(() => startProgressivePreload(), 100);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
if (!cancelled) {
|
|
||||||
setPageImages([]);
|
|
||||||
setNumPages(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!cancelled) setLoading(false);
|
|
||||||
}
|
|
||||||
loadPdfInfo();
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
// Stop any ongoing preloading
|
|
||||||
preloadingRef.current = false;
|
|
||||||
// Cleanup PDF document using worker manager
|
|
||||||
if (pdfDocRef.current) {
|
|
||||||
pdfWorkerManager.destroyDocument(pdfDocRef.current);
|
|
||||||
pdfDocRef.current = null;
|
|
||||||
}
|
|
||||||
// Cleanup ArrayBuffer reference to help garbage collection
|
|
||||||
currentArrayBufferRef.current = null;
|
|
||||||
};
|
|
||||||
}, [effectiveFile, previewFile]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const viewport = scrollAreaRef.current;
|
|
||||||
if (!viewport) return;
|
|
||||||
const handler = () => {
|
|
||||||
handleScroll();
|
|
||||||
};
|
|
||||||
viewport.addEventListener("scroll", handler);
|
|
||||||
return () => viewport.removeEventListener("scroll", handler);
|
|
||||||
}, [pageImages]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box style={{ position: 'relative', height: '100%', display: 'flex', flexDirection: 'column' }}>
|
|
||||||
{/* Close Button - Only show in preview mode */}
|
|
||||||
{onClose && previewFile && (
|
|
||||||
<ActionIcon
|
|
||||||
variant="filled"
|
|
||||||
color="gray"
|
|
||||||
size="lg"
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: '1rem',
|
|
||||||
right: '1rem',
|
|
||||||
zIndex: 1000,
|
|
||||||
borderRadius: '50%',
|
|
||||||
}}
|
|
||||||
onClick={onClose}
|
|
||||||
>
|
|
||||||
<CloseIcon />
|
|
||||||
</ActionIcon>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!effectiveFile ? (
|
|
||||||
<Center style={{ flex: 1 }}>
|
|
||||||
<Text c="red">Error: No file provided to viewer</Text>
|
|
||||||
</Center>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{/* Tabs for multiple files */}
|
|
||||||
{activeFiles.length > 1 && !previewFile && (
|
|
||||||
<Box
|
|
||||||
style={{
|
|
||||||
borderBottom: '1px solid var(--mantine-color-gray-3)',
|
|
||||||
backgroundColor: 'var(--mantine-color-body)',
|
|
||||||
position: 'relative',
|
|
||||||
zIndex: 100,
|
|
||||||
marginTop: '60px' // Push tabs below TopControls
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tabs value={activeTab} onChange={(value) => handleTabChange(value || "0")}>
|
|
||||||
<Tabs.List>
|
|
||||||
{activeFiles.map((file: any, index: number) => (
|
|
||||||
<Tabs.Tab key={index} value={index.toString()}>
|
|
||||||
{file.name.length > 20 ? `${file.name.substring(0, 20)}...` : file.name}
|
|
||||||
</Tabs.Tab>
|
|
||||||
))}
|
|
||||||
</Tabs.List>
|
|
||||||
</Tabs>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{loading ? (
|
|
||||||
<div style={{ flex: 1, padding: '1rem' }}>
|
|
||||||
<SkeletonLoader type="viewer" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<ScrollArea
|
|
||||||
style={{ flex: 1, position: "relative"}}
|
|
||||||
viewportRef={scrollAreaRef}
|
|
||||||
>
|
|
||||||
<Stack gap="xl" align="center" >
|
|
||||||
{numPages === 0 && (
|
|
||||||
<Text color="dimmed">{t("viewer.noPagesToDisplay", "No pages to display.")}</Text>
|
|
||||||
)}
|
|
||||||
{dualPage
|
|
||||||
? Array.from({ length: Math.ceil(numPages / 2) }).map((_, i) => (
|
|
||||||
<Group key={i} gap="md" align="flex-start" style={{ width: "100%", justifyContent: "center" }}>
|
|
||||||
<LazyPageImage
|
|
||||||
pageIndex={i * 2}
|
|
||||||
zoom={zoom}
|
|
||||||
theme={theme}
|
|
||||||
isFirst={i === 0}
|
|
||||||
renderPage={renderPage}
|
|
||||||
pageImages={pageImages}
|
|
||||||
setPageRef={setPageRef}
|
|
||||||
/>
|
|
||||||
{i * 2 + 1 < numPages && (
|
|
||||||
<LazyPageImage
|
|
||||||
pageIndex={i * 2 + 1}
|
|
||||||
zoom={zoom}
|
|
||||||
theme={theme}
|
|
||||||
isFirst={i === 0}
|
|
||||||
renderPage={renderPage}
|
|
||||||
pageImages={pageImages}
|
|
||||||
setPageRef={setPageRef}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
))
|
|
||||||
: Array.from({ length: numPages }).map((_, idx) => (
|
|
||||||
<LazyPageImage
|
|
||||||
key={idx}
|
|
||||||
pageIndex={idx}
|
|
||||||
zoom={zoom}
|
|
||||||
theme={theme}
|
|
||||||
isFirst={idx === 0}
|
|
||||||
renderPage={renderPage}
|
|
||||||
pageImages={pageImages}
|
|
||||||
setPageRef={setPageRef}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
{/* Navigation bar overlays the scroll area */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
zIndex: 50,
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
pointerEvents: "none",
|
|
||||||
background: "transparent",
|
|
||||||
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Paper
|
|
||||||
radius="xl xl 0 0"
|
|
||||||
shadow="sm"
|
|
||||||
p={12}
|
|
||||||
pb={12}
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 10,
|
|
||||||
borderTopLeftRadius: 16,
|
|
||||||
borderTopRightRadius: 16,
|
|
||||||
borderBottomLeftRadius: 0,
|
|
||||||
borderBottomRightRadius: 0,
|
|
||||||
boxShadow: "0 -2px 8px rgba(0,0,0,0.04)",
|
|
||||||
pointerEvents: "auto",
|
|
||||||
minWidth: 420,
|
|
||||||
maxWidth: 700,
|
|
||||||
flexWrap: "wrap",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="subtle"
|
|
||||||
color="blue"
|
|
||||||
size="md"
|
|
||||||
px={8}
|
|
||||||
radius="xl"
|
|
||||||
onClick={() => {
|
|
||||||
scrollToPage(1);
|
|
||||||
}}
|
|
||||||
disabled={currentPage === 1}
|
|
||||||
style={{ minWidth: 36 }}
|
|
||||||
>
|
|
||||||
<FirstPageIcon fontSize="small" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="subtle"
|
|
||||||
color="blue"
|
|
||||||
size="md"
|
|
||||||
px={8}
|
|
||||||
radius="xl"
|
|
||||||
onClick={() => {
|
|
||||||
const prevPage = Math.max(1, (currentPage || 1) - 1);
|
|
||||||
scrollToPage(prevPage);
|
|
||||||
}}
|
|
||||||
disabled={currentPage === 1}
|
|
||||||
style={{ minWidth: 36 }}
|
|
||||||
>
|
|
||||||
<ArrowBackIosNewIcon fontSize="small" />
|
|
||||||
</Button>
|
|
||||||
<NumberInput
|
|
||||||
value={currentPage || 1}
|
|
||||||
onChange={value => {
|
|
||||||
const page = Number(value);
|
|
||||||
if (!isNaN(page) && page >= 1 && page <= numPages) {
|
|
||||||
scrollToPage(page);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
min={1}
|
|
||||||
max={numPages}
|
|
||||||
hideControls
|
|
||||||
styles={{
|
|
||||||
input: { width: 48, textAlign: "center", fontWeight: 500, fontSize: 16},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span style={{ fontWeight: 500, fontSize: 16 }}>
|
|
||||||
/ {numPages}
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
variant="subtle"
|
|
||||||
color="blue"
|
|
||||||
size="md"
|
|
||||||
px={8}
|
|
||||||
radius="xl"
|
|
||||||
onClick={() => {
|
|
||||||
const nextPage = Math.min(numPages, (currentPage || 1) + 1);
|
|
||||||
scrollToPage(nextPage);
|
|
||||||
}}
|
|
||||||
disabled={currentPage === numPages}
|
|
||||||
style={{ minWidth: 36 }}
|
|
||||||
>
|
|
||||||
<ArrowForwardIosIcon fontSize="small" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="subtle"
|
|
||||||
color="blue"
|
|
||||||
size="md"
|
|
||||||
px={8}
|
|
||||||
radius="xl"
|
|
||||||
onClick={() => {
|
|
||||||
scrollToPage(numPages);
|
|
||||||
}}
|
|
||||||
disabled={currentPage === numPages}
|
|
||||||
style={{ minWidth: 36 }}
|
|
||||||
>
|
|
||||||
<LastPageIcon fontSize="small" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={dualPage ? "filled" : "light"}
|
|
||||||
color="blue"
|
|
||||||
size="md"
|
|
||||||
radius="xl"
|
|
||||||
onClick={() => setDualPage(v => !v)}
|
|
||||||
style={{ minWidth: 36 }}
|
|
||||||
title={dualPage ? t("viewer.singlePageView", "Single Page View") : t("viewer.dualPageView", "Dual Page View")}
|
|
||||||
>
|
|
||||||
{dualPage ? <DescriptionIcon fontSize="small" /> : <ViewWeekIcon fontSize="small" />}
|
|
||||||
</Button>
|
|
||||||
<Group gap={4} align="center" style={{ marginLeft: 16 }}>
|
|
||||||
<Button
|
|
||||||
variant="subtle"
|
|
||||||
color="blue"
|
|
||||||
size="md"
|
|
||||||
radius="xl"
|
|
||||||
onClick={() => setZoom(z => Math.max(0.1, z - 0.1))}
|
|
||||||
style={{ minWidth: 32, padding: 0 }}
|
|
||||||
title={t("viewer.zoomOut", "Zoom out")}
|
|
||||||
>−</Button>
|
|
||||||
<span style={{ minWidth: 40, textAlign: "center" }}>{Math.round(zoom * 100)}%</span>
|
|
||||||
<Button
|
|
||||||
variant="subtle"
|
|
||||||
color="blue"
|
|
||||||
size="md"
|
|
||||||
radius="xl"
|
|
||||||
onClick={() => setZoom(z => Math.min(5, z + 0.1))}
|
|
||||||
style={{ minWidth: 32, padding: 0 }}
|
|
||||||
title={t("viewer.zoomIn", "Zoom in")}
|
|
||||||
>+</Button>
|
|
||||||
</Group>
|
|
||||||
</Paper>
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Viewer;
|
|
@ -7,6 +7,7 @@ import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
|
|||||||
import LastPageIcon from '@mui/icons-material/LastPage';
|
import LastPageIcon from '@mui/icons-material/LastPage';
|
||||||
import DescriptionIcon from '@mui/icons-material/Description';
|
import DescriptionIcon from '@mui/icons-material/Description';
|
||||||
import ViewWeekIcon from '@mui/icons-material/ViewWeek';
|
import ViewWeekIcon from '@mui/icons-material/ViewWeek';
|
||||||
|
import '../../types/embedPdf';
|
||||||
|
|
||||||
interface PdfViewerToolbarProps {
|
interface PdfViewerToolbarProps {
|
||||||
// Page navigation props (placeholders for now)
|
// Page navigation props (placeholders for now)
|
||||||
@ -41,23 +42,23 @@ export function PdfViewerToolbar({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateState = () => {
|
const updateState = () => {
|
||||||
// Update zoom
|
// Update zoom
|
||||||
if ((window as any).embedPdfZoom) {
|
if (window.embedPdfZoom) {
|
||||||
const zoomPercent = (window as any).embedPdfZoom.zoomPercent || currentZoom;
|
const zoomPercent = window.embedPdfZoom.zoomPercent || currentZoom;
|
||||||
setDynamicZoom(zoomPercent);
|
setDynamicZoom(zoomPercent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update scroll/page state
|
// Update scroll/page state
|
||||||
if ((window as any).embedPdfScroll) {
|
if (window.embedPdfScroll) {
|
||||||
const currentPageNum = (window as any).embedPdfScroll.currentPage || currentPage;
|
const currentPageNum = window.embedPdfScroll.currentPage || currentPage;
|
||||||
const totalPagesNum = (window as any).embedPdfScroll.totalPages || totalPages;
|
const totalPagesNum = window.embedPdfScroll.totalPages || totalPages;
|
||||||
setDynamicPage(currentPageNum);
|
setDynamicPage(currentPageNum);
|
||||||
setDynamicTotalPages(totalPagesNum);
|
setDynamicTotalPages(totalPagesNum);
|
||||||
setPageInput(currentPageNum);
|
setPageInput(currentPageNum);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update pan mode state
|
// Update pan mode state
|
||||||
if ((window as any).embedPdfPan) {
|
if (window.embedPdfPan) {
|
||||||
const panState = (window as any).embedPdfPan.isPanning || false;
|
const panState = window.embedPdfPan.isPanning || false;
|
||||||
setIsPanning(panState);
|
setIsPanning(panState);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -72,20 +73,20 @@ export function PdfViewerToolbar({
|
|||||||
}, [currentZoom, currentPage, totalPages]);
|
}, [currentZoom, currentPage, totalPages]);
|
||||||
|
|
||||||
const handleZoomOut = () => {
|
const handleZoomOut = () => {
|
||||||
if ((window as any).embedPdfZoom) {
|
if (window.embedPdfZoom) {
|
||||||
(window as any).embedPdfZoom.zoomOut();
|
window.embedPdfZoom.zoomOut();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleZoomIn = () => {
|
const handleZoomIn = () => {
|
||||||
if ((window as any).embedPdfZoom) {
|
if (window.embedPdfZoom) {
|
||||||
(window as any).embedPdfZoom.zoomIn();
|
window.embedPdfZoom.zoomIn();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePageNavigation = (page: number) => {
|
const handlePageNavigation = (page: number) => {
|
||||||
if ((window as any).embedPdfScroll) {
|
if (window.embedPdfScroll) {
|
||||||
(window as any).embedPdfScroll.scrollToPage(page);
|
window.embedPdfScroll.scrollToPage(page);
|
||||||
} else if (onPageChange) {
|
} else if (onPageChange) {
|
||||||
onPageChange(page);
|
onPageChange(page);
|
||||||
}
|
}
|
||||||
@ -93,32 +94,32 @@ export function PdfViewerToolbar({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleFirstPage = () => {
|
const handleFirstPage = () => {
|
||||||
if ((window as any).embedPdfScroll) {
|
if (window.embedPdfScroll) {
|
||||||
(window as any).embedPdfScroll.scrollToFirstPage();
|
window.embedPdfScroll.scrollToFirstPage();
|
||||||
} else {
|
} else {
|
||||||
handlePageNavigation(1);
|
handlePageNavigation(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePreviousPage = () => {
|
const handlePreviousPage = () => {
|
||||||
if ((window as any).embedPdfScroll) {
|
if (window.embedPdfScroll) {
|
||||||
(window as any).embedPdfScroll.scrollToPreviousPage();
|
window.embedPdfScroll.scrollToPreviousPage();
|
||||||
} else {
|
} else {
|
||||||
handlePageNavigation(Math.max(1, dynamicPage - 1));
|
handlePageNavigation(Math.max(1, dynamicPage - 1));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNextPage = () => {
|
const handleNextPage = () => {
|
||||||
if ((window as any).embedPdfScroll) {
|
if (window.embedPdfScroll) {
|
||||||
(window as any).embedPdfScroll.scrollToNextPage();
|
window.embedPdfScroll.scrollToNextPage();
|
||||||
} else {
|
} else {
|
||||||
handlePageNavigation(Math.min(dynamicTotalPages, dynamicPage + 1));
|
handlePageNavigation(Math.min(dynamicTotalPages, dynamicPage + 1));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLastPage = () => {
|
const handleLastPage = () => {
|
||||||
if ((window as any).embedPdfScroll) {
|
if (window.embedPdfScroll) {
|
||||||
(window as any).embedPdfScroll.scrollToLastPage();
|
window.embedPdfScroll.scrollToLastPage();
|
||||||
} else {
|
} else {
|
||||||
handlePageNavigation(dynamicTotalPages);
|
handlePageNavigation(dynamicTotalPages);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Box, ScrollArea, ActionIcon, Tooltip } from '@mantine/core';
|
import { Box, ScrollArea, ActionIcon, Tooltip } from '@mantine/core';
|
||||||
import { LocalIcon } from '../shared/LocalIcon';
|
import { LocalIcon } from '../shared/LocalIcon';
|
||||||
|
import '../../types/embedPdf';
|
||||||
|
|
||||||
interface ThumbnailSidebarProps {
|
interface ThumbnailSidebarProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@ -18,7 +19,7 @@ export function ThumbnailSidebar({ visible, onToggle, colorScheme }: ThumbnailSi
|
|||||||
|
|
||||||
// Get total pages from scroll API
|
// Get total pages from scroll API
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const scrollAPI = (window as any).embedPdfScroll;
|
const scrollAPI = window.embedPdfScroll;
|
||||||
if (scrollAPI && scrollAPI.totalPages) {
|
if (scrollAPI && scrollAPI.totalPages) {
|
||||||
setTotalPages(scrollAPI.totalPages);
|
setTotalPages(scrollAPI.totalPages);
|
||||||
}
|
}
|
||||||
@ -28,7 +29,7 @@ export function ThumbnailSidebar({ visible, onToggle, colorScheme }: ThumbnailSi
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!visible || totalPages === 0) return;
|
if (!visible || totalPages === 0) return;
|
||||||
|
|
||||||
const thumbnailAPI = (window as any).embedPdfThumbnail?.thumbnailAPI;
|
const thumbnailAPI = window.embedPdfThumbnail?.thumbnailAPI;
|
||||||
console.log('📄 ThumbnailSidebar useEffect triggered:', {
|
console.log('📄 ThumbnailSidebar useEffect triggered:', {
|
||||||
visible,
|
visible,
|
||||||
thumbnailAPI: !!thumbnailAPI,
|
thumbnailAPI: !!thumbnailAPI,
|
||||||
@ -88,14 +89,14 @@ export function ThumbnailSidebar({ visible, onToggle, colorScheme }: ThumbnailSi
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}, [visible, totalPages, thumbnails]);
|
}, [visible, totalPages]);
|
||||||
|
|
||||||
const handlePageClick = (pageIndex: number) => {
|
const handlePageClick = (pageIndex: number) => {
|
||||||
const pageNumber = pageIndex + 1; // Convert to 1-based
|
const pageNumber = pageIndex + 1; // Convert to 1-based
|
||||||
setSelectedPage(pageNumber);
|
setSelectedPage(pageNumber);
|
||||||
|
|
||||||
// Use scroll API to navigate to page
|
// Use scroll API to navigate to page
|
||||||
const scrollAPI = (window as any).embedPdfScroll;
|
const scrollAPI = window.embedPdfScroll;
|
||||||
if (scrollAPI && scrollAPI.scrollToPage) {
|
if (scrollAPI && scrollAPI.scrollToPage) {
|
||||||
scrollAPI.scrollToPage(pageNumber);
|
scrollAPI.scrollToPage(pageNumber);
|
||||||
}
|
}
|
||||||
|
46
frontend/src/contexts/ViewerContext.tsx
Normal file
46
frontend/src/contexts/ViewerContext.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import React, { createContext, useContext, useState, ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface ViewerContextType {
|
||||||
|
// Thumbnail sidebar state
|
||||||
|
isThumbnailSidebarVisible: boolean;
|
||||||
|
toggleThumbnailSidebar: () => void;
|
||||||
|
setThumbnailSidebarVisible: (visible: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ViewerContext = createContext<ViewerContextType | null>(null);
|
||||||
|
|
||||||
|
interface ViewerProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
||||||
|
const [isThumbnailSidebarVisible, setIsThumbnailSidebarVisible] = useState(false);
|
||||||
|
|
||||||
|
const toggleThumbnailSidebar = () => {
|
||||||
|
setIsThumbnailSidebarVisible(prev => !prev);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setThumbnailSidebarVisible = (visible: boolean) => {
|
||||||
|
setIsThumbnailSidebarVisible(visible);
|
||||||
|
};
|
||||||
|
|
||||||
|
const value: ViewerContextType = {
|
||||||
|
isThumbnailSidebarVisible,
|
||||||
|
toggleThumbnailSidebar,
|
||||||
|
setThumbnailSidebarVisible,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ViewerContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</ViewerContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useViewer = (): ViewerContextType => {
|
||||||
|
const context = useContext(ViewerContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useViewer must be used within a ViewerProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
43
frontend/src/types/embedPdf.ts
Normal file
43
frontend/src/types/embedPdf.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
// Types for EmbedPDF global APIs
|
||||||
|
export interface EmbedPdfZoomAPI {
|
||||||
|
zoomPercent: number;
|
||||||
|
zoomIn: () => void;
|
||||||
|
zoomOut: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmbedPdfScrollAPI {
|
||||||
|
currentPage: number;
|
||||||
|
totalPages: number;
|
||||||
|
scrollToPage: (page: number) => void;
|
||||||
|
scrollToFirstPage: () => void;
|
||||||
|
scrollToPreviousPage: () => void;
|
||||||
|
scrollToNextPage: () => void;
|
||||||
|
scrollToLastPage: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmbedPdfPanAPI {
|
||||||
|
isPanning: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmbedPdfSpreadAPI {
|
||||||
|
toggleSpreadMode: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmbedPdfThumbnailAPI {
|
||||||
|
thumbnailAPI: {
|
||||||
|
renderThumb: (pageIndex: number, scale: number) => {
|
||||||
|
toPromise: () => Promise<Blob>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
embedPdfZoom?: EmbedPdfZoomAPI;
|
||||||
|
embedPdfScroll?: EmbedPdfScrollAPI;
|
||||||
|
embedPdfPan?: EmbedPdfPanAPI;
|
||||||
|
embedPdfSpread?: EmbedPdfSpreadAPI;
|
||||||
|
embedPdfThumbnail?: EmbedPdfThumbnailAPI;
|
||||||
|
toggleThumbnailSidebar?: () => void;
|
||||||
|
}
|
||||||
|
}
|
@ -31,7 +31,6 @@
|
|||||||
"moduleResolution": "bundler", /* Specify how TypeScript looks up a file from a given module specifier. */
|
"moduleResolution": "bundler", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||||
"baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
"baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||||
"paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */
|
"paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||||
// "@framework": ["vendor/embed-pdf-viewer/packages/core/src/react/adapter.ts"]
|
|
||||||
},
|
},
|
||||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||||
|
Loading…
Reference in New Issue
Block a user