Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215import React, { useCallback, useEffect, useMemo, useRef, useState,} from "react";import { Document, pdfjs } from "react-pdf";import { FilePdf } from "@phosphor-icons/react";import { api } from "./api.js";import { Spinner } from "./ui.jsx";import { useContinuousPdf } from "./pdf/useContinuousPdf.js";import { PdfSheet } from "./pdf/PdfSheet.jsx";import { PdfToolbar } from "./pdf/PdfToolbar.jsx";import { BookSearchDialog } from "./pdf/BookSearchDialog.jsx";import { readPdfOutline } from "./pdf/outline.js";import "react-pdf/dist/Page/TextLayer.css";import "react-pdf/dist/Page/AnnotationLayer.css";
pdfjs.GlobalWorkerOptions.workerSrc = new URL( "pdfjs-dist/build/pdf.worker.min.mjs", import.meta.url,).toString();const pdfOptions = { cMapUrl: "/pdf-assets/cmaps/", cMapPacked: true, standardFontDataUrl: "/pdf-assets/standard_fonts/", // Bundled fallback fonts keep base-14 PDFs consistent across local platforms. useSystemFonts: false, wasmUrl: "/pdf-assets/wasm/", disableAutoFetch: true, disableStream: true, rangeChunkSize: 128 * 1024,};const EMPTY_HIGHLIGHTS = [];
export function PdfReader({ material, highlights, page, navigationToken, onPage, onVisiblePage, onSelection, onContext, onOutline, notify, screenshotRef,}) { const viewer = useContinuousPdf({ material, page, navigationToken, onVisiblePage, notify, screenshotRef, }); const [search, setSearch] = useState(false); const alive = useRef(true); useEffect(() => { alive.current = true; return () => { alive.current = false; }; }, []); const pageHighlights = useMemo(() => { const byPage = new Map(); for (const highlight of highlights) { if (!byPage.has(highlight.page)) byPage.set(highlight.page, []); byPage.get(highlight.page).push(highlight); } return byPage; }, [highlights]); const loaded = useCallback( async (pdf) => { try { await viewer.loadDocument(pdf); const outline = await readPdfOutline(pdf); if (alive.current) onOutline(outline); } catch (error) { if (alive.current) notify(error.message, "error"); } }, [viewer.loadDocument, onOutline, notify], ); const pages = []; for ( let number = viewer.windowRange.start; number <= viewer.windowRange.end; number++ ) { pages.push( <PdfSheet key={number} number={number} width={viewer.pageWidth} height={viewer.layout.heights[number - 1]} top={viewer.layout.offsets[number - 1]} renderKey={viewer.renderKey(number, viewer.pageWidth, viewer.layout)} highlights={pageHighlights.get(number) || EMPTY_HIGHLIGHTS} registerPage={viewer.registerPage} measurePage={viewer.measurePage} renderedPage={viewer.renderedPage} onSelection={onSelection} />, ); } return ( <div className="pdf-reader"> <PdfToolbar page={page} count={viewer.count} zoom={viewer.zoom} loaded={viewer.currentLoaded} onPage={onPage} onZoom={viewer.changeZoom} onSearch={() => setSearch(true)} onContext={onContext} /> <PdfNotice material={material} notify={notify} /> <div className="pdf-scroll" ref={viewer.scrollRoot} onScroll={viewer.onScroll} onWheel={viewer.cancelNavigation} onTouchStart={viewer.cancelNavigation} onPointerDown={viewer.cancelNavigation} onKeyDown={viewer.cancelNavigation} tabIndex={0} aria-label="PDF pages" > <Document file={`/api/reader/files/${material.id}/book.pdf`} options={pdfOptions} loading={<Spinner label="Opening PDF…" />} error={<PdfError materialId={material.id} />} onLoadSuccess={loaded} onItemClick={({ pageNumber }) => { if (pageNumber) onPage(pageNumber); }} > {viewer.ready && ( <div className="pdf-virtual-document" style={{ height: viewer.layout.totalHeight, minWidth: viewer.pageWidth, }} > {pages} </div> )} </Document> </div> <BookSearchDialog materialId={material.id} open={search} onOpenChange={setSearch} onPage={onPage} notify={notify} /> </div> );}
function PdfError({ materialId }) { return ( <div className="empty-state"> <FilePdf size={35} /> <h2>Could not open this PDF</h2> <p>It may be password protected or damaged.</p> <a href={`/api/reader/files/${materialId}/book.pdf`} target="_blank" rel="noreferrer" > Open original file </a> </div> );}
function PdfNotice({ material, notify }) { if (material.status === "image-only") return ( <div className="notice"> This PDF has no searchable text. Enable image input in Settings, then attach the current page when asking a question. </div> ); if (material.status === "indexing") return ( <div className="notice" role="status"> Indexing locally: {material.progress} / {material.page_count || "?"}{" "} pages. You can read while the index is built. </div> ); if (material.status === "error") return ( <div className="notice error"> {material.error} <button onClick={() => api("/materials/" + material.id + "/reindex", { method: "POST", }).catch((error) => notify(error.message, "error")) } > Retry indexing </button> </div> ); return null;}