diff --git a/frontend/src/components/reader/BookMarkersPanel.tsx b/frontend/src/components/reader/BookMarkersPanel.tsx
index 37bfb8f..e7252a6 100644
--- a/frontend/src/components/reader/BookMarkersPanel.tsx
+++ b/frontend/src/components/reader/BookMarkersPanel.tsx
@@ -12,6 +12,7 @@ interface BookMarkersPanelProps {
isOpen: boolean;
onClose: () => void;
onGoToPassage: (marker: MarkerEntry) => void;
+ emptyHintKey?: string;
}
export function BookMarkersPanel({
@@ -19,6 +20,7 @@ export function BookMarkersPanel({
isOpen,
onClose,
onGoToPassage,
+ emptyHintKey = "annotations.selectTextHint",
}: BookMarkersPanelProps) {
const { t } = useTranslation();
const { markers, loadBookmarks, removeBookmark, state } = useAnnotations();
@@ -43,7 +45,7 @@ export function BookMarkersPanel({
{state.bookmarksLoading ? (
{t("annotations.loading")}
) : markers.length === 0 ? (
- {t("annotations.selectTextHint")}
+ {t(emptyHintKey)}
) : (
{markers.map((m) => (
@@ -51,13 +53,15 @@ export function BookMarkersPanel({
{m.chapter_title && (
{m.chapter_title}
)}
- {m.location_text && (
+ {m.location_text ? (
- )}
+ ) : m.chapter_title ? (
+ {m.chapter_title}
+ ) : null}
{m.content ? (
) : (
diff --git a/frontend/src/components/reader/BookmarkReaderRail.tsx b/frontend/src/components/reader/BookmarkReaderRail.tsx
index 16073e7..3d4abfb 100644
--- a/frontend/src/components/reader/BookmarkReaderRail.tsx
+++ b/frontend/src/components/reader/BookmarkReaderRail.tsx
@@ -20,7 +20,9 @@ function RailMarker({
const { t } = useTranslation();
const [hovered, setHovered] = useState(false);
const color = normalizeHighlightColor(item.marker.highlight_color);
- const previewText = truncateRailPreview(item.marker.location_text || "");
+ const previewText = truncateRailPreview(
+ item.marker.location_text || item.marker.chapter_title || "",
+ );
const notePreview = item.marker.content
? truncateRailPreview(item.marker.content, 80)
: "";
diff --git a/frontend/src/components/reader/PdfReadingView.tsx b/frontend/src/components/reader/PdfReadingView.tsx
index e7e753e..f3b7e5e 100644
--- a/frontend/src/components/reader/PdfReadingView.tsx
+++ b/frontend/src/components/reader/PdfReadingView.tsx
@@ -7,16 +7,14 @@ import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18n-lite";
import { useAnnotations } from "@/context/AnnotationsContext";
import { useBookmarkRailLayout } from "../../hooks/useBookmarkRailLayout";
-import { usePdfHighlights } from "../../hooks/usePdfHighlights";
import { usePdfReader } from "../../hooks/usePdfReader";
-import { usePdfSelection } from "../../hooks/usePdfSelection";
import { useReadingSettings } from "../../hooks/useReadingSettings";
+import { useToast } from "../../hooks/useToast";
import { booksApi } from "../../api/books";
import type { EBookDetail } from "../../types/book";
import type { EpubTocItem } from "./TableOfContents";
-import { parsePageHref } from "../../utils/pdfAnchor";
+import { parsePageHref, serializePdfAnchor } from "../../utils/pdfAnchor";
import { fallbackPdfPageToc } from "../../utils/pdfToc";
-import { SelectionPopover } from "./SelectionPopover";
import { BookMarkersPanel } from "./BookMarkersPanel";
import { BookmarkReaderRail } from "./BookmarkReaderRail";
import { ResumeReadingButton } from "./ResumeReadingButton";
@@ -37,6 +35,8 @@ interface PdfReadingViewProps {
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);
@@ -57,7 +57,6 @@ export function PdfReadingView({ book, bookId, initialAnchor }: PdfReadingViewPr
error,
isBookmarkPeekActive,
readingAnchor,
- pageWrapRef,
goToPage,
goToNextPage,
goToPrevPage,
@@ -66,21 +65,18 @@ export function PdfReadingView({ book, bookId, initialAnchor }: PdfReadingViewPr
goToAnchor,
} = usePdfReader(bookId, book.page_count, initialAnchor);
- const { pendingSelection, clearSelection } = usePdfSelection(
- pageWrapRef,
- currentPage,
- chapterTitle,
- );
-
- const { markers } = useAnnotations();
const bookMarkers = useMemo(
() => markers.filter((m) => m.ebook_id === bookId),
[markers, bookId],
);
- const pdfHighlights = usePdfHighlights(bookId, currentPage);
const railItems = useBookmarkRailLayout(bookMarkers);
+ useEffect(() => {
+ if (!bookId || Number.isNaN(bookId)) return;
+ void loadBookmarks(bookId);
+ }, [bookId, loadBookmarks]);
+
useEffect(() => {
void booksApi
.getToc(bookId)
@@ -129,6 +125,29 @@ export function PdfReadingView({ book, bookId, initialAnchor }: PdfReadingViewPr
[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;
@@ -140,6 +159,11 @@ export function PdfReadingView({ book, bookId, initialAnchor }: PdfReadingViewPr
};
}, [progress, currentPage, pageCount]);
+ const isCurrentPageBookmarked = useMemo(() => {
+ const anchor = serializePdfAnchor(currentPage);
+ return bookMarkers.some((m) => m.epub_cfi === anchor);
+ }, [bookMarkers, currentPage]);
+
if (isLoading) {
return (
@@ -179,6 +203,8 @@ export function PdfReadingView({ book, bookId, initialAnchor }: PdfReadingViewPr
onToggleToc={() => setTocOpen((v) => !v)}
onToggleSettings={() => setSettingsOpen((v) => !v)}
onToggleMarkers={() => setMarkersOpen((v) => !v)}
+ onBookmarkPage={handleBookmarkPage}
+ isCurrentPageBookmarked={isCurrentPageBookmarked}
/>
@@ -195,6 +221,7 @@ export function PdfReadingView({ book, bookId, initialAnchor }: PdfReadingViewPr
isOpen={markersOpen}
onClose={() => setMarkersOpen(false)}
onGoToPassage={handleGoToMarker}
+ emptyHintKey="annotations.pdfPageBookmarkHint"
/>
- {pendingSelection && (
-
- )}
-
;
}
-export function PdfViewer({
- document,
- pageNumber,
- scale,
- highlights = [],
- pageWrapRef,
-}: PdfViewerProps) {
+export function PdfViewer({ document, pageNumber, scale }: PdfViewerProps) {
const canvasRef = useRef(null);
- const textLayerRef = useRef(null);
- const internalWrapRef = useRef(null);
- const wrapRef = pageWrapRef ?? internalWrapRef;
+ const wrapRef = useRef(null);
const renderTaskRef = useRef<{ cancel: () => void } | null>(null);
useEffect(() => {
@@ -39,8 +22,7 @@ export function PdfViewer({
}
const canvas = canvasRef.current;
- const textLayer = textLayerRef.current;
- if (!canvas || !textLayer) return;
+ if (!canvas) return;
try {
const page = await document.getPage(pageNumber);
@@ -49,9 +31,6 @@ export function PdfViewer({
const viewport = page.getViewport({ scale });
canvas.width = viewport.width;
canvas.height = viewport.height;
- textLayer.style.width = `${viewport.width}px`;
- textLayer.style.height = `${viewport.height}px`;
- textLayer.innerHTML = "";
const ctx = canvas.getContext("2d");
if (!ctx) return;
@@ -59,27 +38,8 @@ export function PdfViewer({
const renderTask = page.render({ canvasContext: ctx, viewport });
renderTaskRef.current = renderTask;
await renderTask.promise;
- if (cancelled) return;
-
- const textContent = await page.getTextContent();
- const textTask = (
- pdfjs as typeof pdfjs & {
- renderTextLayer: (params: {
- textContentSource: typeof textContent;
- container: HTMLDivElement;
- viewport: typeof viewport;
- }) => { promise: Promise };
- }
- ).renderTextLayer({
- textContentSource: textContent,
- container: textLayer,
- viewport,
- });
- await textTask.promise;
} catch {
- if (!cancelled) {
- textLayerRef.current && (textLayerRef.current.innerHTML = "");
- }
+ /* render failed */
}
}
@@ -97,27 +57,6 @@ export function PdfViewer({
return (
-
-
- {highlights.flatMap((hl, hi) =>
- hl.rects.map((rect, ri) => {
- const [x, y, w, h] = rect;
- return (
-
- );
- }),
- )}
-
);
}
diff --git a/frontend/src/components/reader/ReaderToolbar.tsx b/frontend/src/components/reader/ReaderToolbar.tsx
index e9d7585..862dad5 100644
--- a/frontend/src/components/reader/ReaderToolbar.tsx
+++ b/frontend/src/components/reader/ReaderToolbar.tsx
@@ -13,6 +13,9 @@ interface ReaderToolbarProps {
onToggleToc: () => void;
onToggleSettings: () => void;
onToggleMarkers?: () => void;
+ onBookmarkPage?: () => void;
+ /** PDF: filled bookmark when the current page is already bookmarked */
+ isCurrentPageBookmarked?: boolean;
}
export default function ReaderToolbar({
@@ -23,6 +26,8 @@ export default function ReaderToolbar({
onToggleToc,
onToggleSettings,
onToggleMarkers,
+ onBookmarkPage,
+ isCurrentPageBookmarked = false,
}: ReaderToolbarProps) {
const { t } = useTranslation();
const percentage = progress?.percentage ?? 0;
@@ -74,6 +79,26 @@ export default function ReaderToolbar({
)}
+ {onBookmarkPage && (
+
+ )}
{onToggleMarkers && (
)}