From 21a93d6cac6b7c497babb8cd1fc66bad24d06571 Mon Sep 17 00:00:00 2001 From: Reece Browne Date: Mon, 15 Sep 2025 13:33:39 +0100 Subject: [PATCH] Context based right rail controls for viewer --- .../src/components/viewer/EmbedPdfViewer.tsx | 30 +- frontend/src/components/viewer/OldViewer.tsx | 687 ------------------ .../components/viewer/PdfViewerToolbar.tsx | 43 +- .../components/viewer/ThumbnailSidebar.tsx | 9 +- frontend/src/contexts/ViewerContext.tsx | 46 ++ frontend/src/types/embedPdf.ts | 43 ++ frontend/tsconfig.json | 1 - 7 files changed, 135 insertions(+), 724 deletions(-) delete mode 100644 frontend/src/components/viewer/OldViewer.tsx create mode 100644 frontend/src/contexts/ViewerContext.tsx create mode 100644 frontend/src/types/embedPdf.ts diff --git a/frontend/src/components/viewer/EmbedPdfViewer.tsx b/frontend/src/components/viewer/EmbedPdfViewer.tsx index 822655da9..56fdf9ec0 100644 --- a/frontend/src/components/viewer/EmbedPdfViewer.tsx +++ b/frontend/src/components/viewer/EmbedPdfViewer.tsx @@ -6,9 +6,11 @@ import CloseIcon from '@mui/icons-material/Close'; import { useFileState } from "../../contexts/FileContext"; import { useFileWithUrl } from "../../hooks/useFileWithUrl"; +import { ViewerProvider, useViewer } from "../../contexts/ViewerContext"; import { LocalEmbedPDF } from './LocalEmbedPDF'; import { PdfViewerToolbar } from './PdfViewerToolbar'; import { ThumbnailSidebar } from './ThumbnailSidebar'; +import '../../types/embedPdf'; export interface EmbedPdfViewerProps { sidebarsVisible: boolean; @@ -17,7 +19,7 @@ export interface EmbedPdfViewerProps { previewFile?: File | null; } -const EmbedPdfViewer = ({ +const EmbedPdfViewerContent = ({ sidebarsVisible, setSidebarsVisible, onClose, @@ -28,7 +30,7 @@ const EmbedPdfViewer = ({ const { colorScheme } = useMantineColorScheme(); const viewerRef = React.useRef(null); const [isViewerHovered, setIsViewerHovered] = React.useState(false); - const [isThumbnailSidebarVisible, setIsThumbnailSidebarVisible] = React.useState(false); + const { isThumbnailSidebarVisible, toggleThumbnailSidebar } = useViewer(); // Get current file from FileContext const { selectors } = useFileState(); @@ -68,7 +70,7 @@ const EmbedPdfViewer = ({ event.preventDefault(); event.stopPropagation(); - const zoomAPI = (window as any).embedPdfZoom; + const zoomAPI = window.embedPdfZoom; if (zoomAPI) { if (event.deltaY < 0) { // Scroll up - zoom in @@ -97,7 +99,7 @@ const EmbedPdfViewer = ({ // Check if Ctrl (Windows/Linux) or Cmd (Mac) is pressed if (event.ctrlKey || event.metaKey) { - const zoomAPI = (window as any).embedPdfZoom; + const zoomAPI = window.embedPdfZoom; if (zoomAPI) { if (event.key === '=' || event.key === '+') { // Ctrl+= or Ctrl++ for zoom in @@ -120,14 +122,12 @@ const EmbedPdfViewer = ({ // Expose toggle functions globally for right rail buttons React.useEffect(() => { - (window as any).toggleThumbnailSidebar = () => { - setIsThumbnailSidebarVisible(prev => !prev); - }; + window.toggleThumbnailSidebar = toggleThumbnailSidebar; return () => { - delete (window as any).toggleThumbnailSidebar; + delete window.toggleThumbnailSidebar; }; - }, []); + }, [toggleThumbnailSidebar]); return ( { - (window as any).embedPdfSpread?.toggleSpreadMode(); + window.embedPdfSpread?.toggleSpreadMode(); }} currentZoom={100} /> @@ -224,11 +224,19 @@ const EmbedPdfViewer = ({ {/* Thumbnail Sidebar */} setIsThumbnailSidebarVisible(prev => !prev)} + onToggle={toggleThumbnailSidebar} colorScheme={colorScheme} /> ); }; +const EmbedPdfViewer = (props: EmbedPdfViewerProps) => { + return ( + + + + ); +}; + export default EmbedPdfViewer; \ No newline at end of file diff --git a/frontend/src/components/viewer/OldViewer.tsx b/frontend/src/components/viewer/OldViewer.tsx deleted file mode 100644 index dfcd5dc7d..000000000 --- a/frontend/src/components/viewer/OldViewer.tsx +++ /dev/null @@ -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; - 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(null); - const imgRef = useRef(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 ( - {`Page - ); - } - - // Placeholder while loading - return ( -
- {isVisible ? ( -
-
- Loading page {pageIndex + 1}... -
- ) : ( - Page {pageIndex + 1} - )} -
- ); -}; - -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("0"); - - // Reset PDF state when switching tabs - const handleTabChange = (newTab: string) => { - setActiveTab(newTab); - setNumPages(0); - setPageImages([]); - setCurrentPage(null); - setLoading(true); - }; - const [numPages, setNumPages] = useState(0); - const [pageImages, setPageImages] = useState([]); - const [loading, setLoading] = useState(false); - const [currentPage, setCurrentPage] = useState(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(null); - const pdfDocRef = useRef(null); - const renderingPagesRef = useRef>(new Set()); - const currentArrayBufferRef = useRef(null); - const preloadingRef = useRef(false); - - // Function to render a specific page on-demand - const renderPage = async (pageIndex: number): Promise => { - 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 ( - - {/* Close Button - Only show in preview mode */} - {onClose && previewFile && ( - - - - )} - - {!effectiveFile ? ( -
- Error: No file provided to viewer -
- ) : ( - <> - {/* Tabs for multiple files */} - {activeFiles.length > 1 && !previewFile && ( - - handleTabChange(value || "0")}> - - {activeFiles.map((file: any, index: number) => ( - - {file.name.length > 20 ? `${file.name.substring(0, 20)}...` : file.name} - - ))} - - - - )} - - {loading ? ( -
- -
- ) : ( - - - {numPages === 0 && ( - {t("viewer.noPagesToDisplay", "No pages to display.")} - )} - {dualPage - ? Array.from({ length: Math.ceil(numPages / 2) }).map((_, i) => ( - - - {i * 2 + 1 < numPages && ( - - )} - - )) - : Array.from({ length: numPages }).map((_, idx) => ( - - ))} - - {/* Navigation bar overlays the scroll area */} -
- - - - { - 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}, - }} - /> - - / {numPages} - - - - - - - {Math.round(zoom * 100)}% - - - -
-
- )} - - )} - -
- ); -}; - -export default Viewer; diff --git a/frontend/src/components/viewer/PdfViewerToolbar.tsx b/frontend/src/components/viewer/PdfViewerToolbar.tsx index 725b4250d..81f543eff 100644 --- a/frontend/src/components/viewer/PdfViewerToolbar.tsx +++ b/frontend/src/components/viewer/PdfViewerToolbar.tsx @@ -7,6 +7,7 @@ import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; import LastPageIcon from '@mui/icons-material/LastPage'; import DescriptionIcon from '@mui/icons-material/Description'; import ViewWeekIcon from '@mui/icons-material/ViewWeek'; +import '../../types/embedPdf'; interface PdfViewerToolbarProps { // Page navigation props (placeholders for now) @@ -41,23 +42,23 @@ export function PdfViewerToolbar({ useEffect(() => { const updateState = () => { // Update zoom - if ((window as any).embedPdfZoom) { - const zoomPercent = (window as any).embedPdfZoom.zoomPercent || currentZoom; + if (window.embedPdfZoom) { + const zoomPercent = window.embedPdfZoom.zoomPercent || currentZoom; setDynamicZoom(zoomPercent); } // Update scroll/page state - if ((window as any).embedPdfScroll) { - const currentPageNum = (window as any).embedPdfScroll.currentPage || currentPage; - const totalPagesNum = (window as any).embedPdfScroll.totalPages || totalPages; + if (window.embedPdfScroll) { + const currentPageNum = window.embedPdfScroll.currentPage || currentPage; + const totalPagesNum = window.embedPdfScroll.totalPages || totalPages; setDynamicPage(currentPageNum); setDynamicTotalPages(totalPagesNum); setPageInput(currentPageNum); } // Update pan mode state - if ((window as any).embedPdfPan) { - const panState = (window as any).embedPdfPan.isPanning || false; + if (window.embedPdfPan) { + const panState = window.embedPdfPan.isPanning || false; setIsPanning(panState); } }; @@ -72,20 +73,20 @@ export function PdfViewerToolbar({ }, [currentZoom, currentPage, totalPages]); const handleZoomOut = () => { - if ((window as any).embedPdfZoom) { - (window as any).embedPdfZoom.zoomOut(); + if (window.embedPdfZoom) { + window.embedPdfZoom.zoomOut(); } }; const handleZoomIn = () => { - if ((window as any).embedPdfZoom) { - (window as any).embedPdfZoom.zoomIn(); + if (window.embedPdfZoom) { + window.embedPdfZoom.zoomIn(); } }; const handlePageNavigation = (page: number) => { - if ((window as any).embedPdfScroll) { - (window as any).embedPdfScroll.scrollToPage(page); + if (window.embedPdfScroll) { + window.embedPdfScroll.scrollToPage(page); } else if (onPageChange) { onPageChange(page); } @@ -93,32 +94,32 @@ export function PdfViewerToolbar({ }; const handleFirstPage = () => { - if ((window as any).embedPdfScroll) { - (window as any).embedPdfScroll.scrollToFirstPage(); + if (window.embedPdfScroll) { + window.embedPdfScroll.scrollToFirstPage(); } else { handlePageNavigation(1); } }; const handlePreviousPage = () => { - if ((window as any).embedPdfScroll) { - (window as any).embedPdfScroll.scrollToPreviousPage(); + if (window.embedPdfScroll) { + window.embedPdfScroll.scrollToPreviousPage(); } else { handlePageNavigation(Math.max(1, dynamicPage - 1)); } }; const handleNextPage = () => { - if ((window as any).embedPdfScroll) { - (window as any).embedPdfScroll.scrollToNextPage(); + if (window.embedPdfScroll) { + window.embedPdfScroll.scrollToNextPage(); } else { handlePageNavigation(Math.min(dynamicTotalPages, dynamicPage + 1)); } }; const handleLastPage = () => { - if ((window as any).embedPdfScroll) { - (window as any).embedPdfScroll.scrollToLastPage(); + if (window.embedPdfScroll) { + window.embedPdfScroll.scrollToLastPage(); } else { handlePageNavigation(dynamicTotalPages); } diff --git a/frontend/src/components/viewer/ThumbnailSidebar.tsx b/frontend/src/components/viewer/ThumbnailSidebar.tsx index 070d88505..40d77f559 100644 --- a/frontend/src/components/viewer/ThumbnailSidebar.tsx +++ b/frontend/src/components/viewer/ThumbnailSidebar.tsx @@ -1,6 +1,7 @@ import React, { useState, useEffect } from 'react'; import { Box, ScrollArea, ActionIcon, Tooltip } from '@mantine/core'; import { LocalIcon } from '../shared/LocalIcon'; +import '../../types/embedPdf'; interface ThumbnailSidebarProps { visible: boolean; @@ -18,7 +19,7 @@ export function ThumbnailSidebar({ visible, onToggle, colorScheme }: ThumbnailSi // Get total pages from scroll API useEffect(() => { - const scrollAPI = (window as any).embedPdfScroll; + const scrollAPI = window.embedPdfScroll; if (scrollAPI && scrollAPI.totalPages) { setTotalPages(scrollAPI.totalPages); } @@ -28,7 +29,7 @@ export function ThumbnailSidebar({ visible, onToggle, colorScheme }: ThumbnailSi useEffect(() => { if (!visible || totalPages === 0) return; - const thumbnailAPI = (window as any).embedPdfThumbnail?.thumbnailAPI; + const thumbnailAPI = window.embedPdfThumbnail?.thumbnailAPI; console.log('📄 ThumbnailSidebar useEffect triggered:', { visible, thumbnailAPI: !!thumbnailAPI, @@ -88,14 +89,14 @@ export function ThumbnailSidebar({ visible, onToggle, colorScheme }: ThumbnailSi } }); }; - }, [visible, totalPages, thumbnails]); + }, [visible, totalPages]); 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; + const scrollAPI = window.embedPdfScroll; if (scrollAPI && scrollAPI.scrollToPage) { scrollAPI.scrollToPage(pageNumber); } diff --git a/frontend/src/contexts/ViewerContext.tsx b/frontend/src/contexts/ViewerContext.tsx new file mode 100644 index 000000000..1b470c058 --- /dev/null +++ b/frontend/src/contexts/ViewerContext.tsx @@ -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(null); + +interface ViewerProviderProps { + children: ReactNode; +} + +export const ViewerProvider: React.FC = ({ children }) => { + const [isThumbnailSidebarVisible, setIsThumbnailSidebarVisible] = useState(false); + + const toggleThumbnailSidebar = () => { + setIsThumbnailSidebarVisible(prev => !prev); + }; + + const setThumbnailSidebarVisible = (visible: boolean) => { + setIsThumbnailSidebarVisible(visible); + }; + + const value: ViewerContextType = { + isThumbnailSidebarVisible, + toggleThumbnailSidebar, + setThumbnailSidebarVisible, + }; + + return ( + + {children} + + ); +}; + +export const useViewer = (): ViewerContextType => { + const context = useContext(ViewerContext); + if (!context) { + throw new Error('useViewer must be used within a ViewerProvider'); + } + return context; +}; \ No newline at end of file diff --git a/frontend/src/types/embedPdf.ts b/frontend/src/types/embedPdf.ts new file mode 100644 index 000000000..d33f35e74 --- /dev/null +++ b/frontend/src/types/embedPdf.ts @@ -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; + }; + }; +} + +declare global { + interface Window { + embedPdfZoom?: EmbedPdfZoomAPI; + embedPdfScroll?: EmbedPdfScrollAPI; + embedPdfPan?: EmbedPdfPanAPI; + embedPdfSpread?: EmbedPdfSpreadAPI; + embedPdfThumbnail?: EmbedPdfThumbnailAPI; + toggleThumbnailSidebar?: () => void; + } +} \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 38151b5b3..3e4c6105d 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -31,7 +31,6 @@ "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. */ "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. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */