/** * PdfReadingView — full-screen PDF reading powered by PDF.js. */ import { lazy, useCallback, useEffect, useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18n-lite"; import { useAnnotations } from "@/context/AnnotationsContext"; import { useBookmarkRailLayout } from "../../hooks/useBookmarkRailLayout"; import { usePdfReader } from "../../hooks/usePdfReader"; import { useReadingSettings } from "../../hooks/useReadingSettings"; import { useReaderOrientationCss } from "../../hooks/useReaderOrientationCss"; import { ReaderErrorScreen } from "./ReaderErrorScreen"; import { ReaderLoadingScreen } from "./ReaderLoadingScreen"; import { ReaderSuspenseShell } from "./ReaderSuspenseShell"; import { useToast } from "../../hooks/useToast"; import { booksApi } from "../../api/books"; import type { EBookDetail } from "../../types/book"; import type { EpubTocItem } from "./TableOfContents"; import { parsePageHref, serializePdfAnchor } from "../../utils/pdfAnchor"; import { fallbackPdfPageToc } from "../../utils/pdfToc"; import { BookMarkersPanel } from "./BookMarkersPanel"; import { BookmarkReaderRail } from "./BookmarkReaderRail"; import { ResumeReadingButton } from "./ResumeReadingButton"; import { PdfLimitationsNotice } from "./PdfLimitationsNotice"; import { PdfViewer } from "./PdfViewer"; import type { MarkerEntry } from "@/types"; const ReaderToolbar = lazy(() => import("./ReaderToolbar")); const TableOfContents = lazy(() => import("./TableOfContents")); const ReadingSettingsPanel = lazy(() => import("./ReadingSettingsPanel")); interface PdfReadingViewProps { book: EBookDetail; bookId: number; initialAnchor?: string; } export function PdfReadingView({ book, bookId, initialAnchor }: PdfReadingViewProps) { const { t } = useTranslation(); const navigate = useNavigate(); const { showToast } = useToast(); const { markers, addMarker, loadBookmarks } = useAnnotations(); const [tocOpen, setTocOpen] = useState(false); const [settingsOpen, setSettingsOpen] = useState(false); const [markersOpen, setMarkersOpen] = useState(false); const [tocItems, setTocItems] = useState([]); const { settings, updateSettings, flushSettings } = useReadingSettings(); const { pdfDocument, currentPage, pageCount, scale, setScale, chapterTitle, progress, isLoading, error, isBookmarkPeekActive, readingAnchor, goToPage, goToNextPage, goToPrevPage, beginBookmarkPeek, resumeReadingAnchor, goToAnchor, } = usePdfReader(bookId, book.page_count, initialAnchor); const bookMarkers = useMemo( () => markers.filter((m) => m.ebook_id === bookId), [markers, bookId], ); const railItems = useBookmarkRailLayout(bookMarkers); useEffect(() => { if (!bookId || Number.isNaN(bookId)) return; void loadBookmarks(bookId); }, [bookId, loadBookmarks]); useEffect(() => { void booksApi .getToc(bookId) .then((res) => { const items = res.chapters.map((ch) => ({ label: ch.title, href: ch.href || `pdf:page:${ch.index + 1}`, })); setTocItems( items.length > 0 ? items : fallbackPdfPageToc(pageCount, (p) => t("reader.pdfPageLabel", { page: String(p) })), ); }) .catch(() => setTocItems(fallbackPdfPageToc(pageCount, (p) => t("reader.pdfPageLabel", { page: String(p) }))), ); }, [bookId, pageCount, t]); useReaderOrientationCss(settings.orientation_lock); const handleTocNavigate = useCallback( (href: string) => { const page = parsePageHref(href); if (page) goToPage(page); else goToAnchor(href); }, [goToAnchor, goToPage], ); const handleGoToMarker = useCallback( (marker: MarkerEntry) => { beginBookmarkPeek(marker.epub_cfi); setMarkersOpen(false); }, [beginBookmarkPeek], ); const handleBookmarkPage = useCallback(() => { const anchor = serializePdfAnchor(currentPage); const exists = bookMarkers.some((m) => m.epub_cfi === anchor); if (exists) { showToast({ message: t("reader.pageAlreadyBookmarked"), variant: "warning" }); return; } void addMarker({ ebook: bookId, epub_cfi: anchor, chapter_index: currentPage - 1, chapter_title: t("reader.pdfPageLabel", { page: String(currentPage) }), location_text: "", content: "", }) .then(() => { showToast({ message: t("reader.pageBookmarked"), variant: "success" }); }) .catch(() => { showToast({ message: t("annotations.saveFailed"), variant: "error" }); }); }, [addMarker, bookId, bookMarkers, currentPage, showToast, t]); const progressForToolbar = useMemo(() => { if (!progress) return progress; const pct = pageCount > 0 ? Math.round((currentPage / pageCount) * 100) : 0; return { ...progress, percentage: pct, current_position: pct, current_chapter: currentPage, }; }, [progress, currentPage, pageCount]); const isCurrentPageBookmarked = useMemo(() => { const anchor = serializePdfAnchor(currentPage); return bookMarkers.some((m) => m.epub_cfi === anchor); }, [bookMarkers, currentPage]); if (isLoading) { return ; } if (error || !pdfDocument) { const message = error === "PDF_PASSWORD" ? t("reader.pdfPassword") : (error ?? t("reader.unableToOpen")); return navigate("/")} />; } return ( navigate("/")} onToggleToc={() => setTocOpen((v) => !v)} onToggleSettings={() => setSettingsOpen((v) => !v)} onToggleMarkers={() => setMarkersOpen((v) => !v)} onBookmarkPage={handleBookmarkPage} isCurrentPageBookmarked={isCurrentPageBookmarked} /> setTocOpen(false)} onNavigate={handleTocNavigate} /> setMarkersOpen(false)} onGoToPassage={handleGoToMarker} emptyHintKey="annotations.pdfPageBookmarkHint" /> setSettingsOpen(false)} onUpdate={updateSettings} onFlush={flushSettings} />
); }