Archived
fix: pdf bookmark
for pdfs only select pages not text
This commit is contained in:
@@ -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 ? (
|
||||
<p className="annotations-loading">{t("annotations.loading")}</p>
|
||||
) : markers.length === 0 ? (
|
||||
<p className="annotations-empty">{t("annotations.selectTextHint")}</p>
|
||||
<p className="annotations-empty">{t(emptyHintKey)}</p>
|
||||
) : (
|
||||
<ul className="marker-thread-list">
|
||||
{markers.map((m) => (
|
||||
@@ -51,13 +53,15 @@ export function BookMarkersPanel({
|
||||
{m.chapter_title && (
|
||||
<span className="marker-thread-chapter">{m.chapter_title}</span>
|
||||
)}
|
||||
{m.location_text && (
|
||||
{m.location_text ? (
|
||||
<CollapsibleMarkerPassage
|
||||
text={m.location_text}
|
||||
highlightColor={m.highlight_color}
|
||||
className="annotation-quote"
|
||||
/>
|
||||
)}
|
||||
) : m.chapter_title ? (
|
||||
<span className="annotation-quote marker-page-label">{m.chapter_title}</span>
|
||||
) : null}
|
||||
{m.content ? (
|
||||
<CollapsibleMarkerThought text={m.content} />
|
||||
) : (
|
||||
|
||||
@@ -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)
|
||||
: "";
|
||||
|
||||
@@ -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 (
|
||||
<div className="reader-loading">
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
<PdfLimitationsNotice bookId={bookId} />
|
||||
@@ -195,6 +221,7 @@ export function PdfReadingView({ book, bookId, initialAnchor }: PdfReadingViewPr
|
||||
isOpen={markersOpen}
|
||||
onClose={() => setMarkersOpen(false)}
|
||||
onGoToPassage={handleGoToMarker}
|
||||
emptyHintKey="annotations.pdfPageBookmarkHint"
|
||||
/>
|
||||
|
||||
<ReadingSettingsPanel
|
||||
@@ -208,23 +235,12 @@ export function PdfReadingView({ book, bookId, initialAnchor }: PdfReadingViewPr
|
||||
onFlush={flushSettings}
|
||||
/>
|
||||
|
||||
{pendingSelection && (
|
||||
<SelectionPopover
|
||||
ebookId={bookId}
|
||||
selection={pendingSelection}
|
||||
onClose={clearSelection}
|
||||
onSaved={clearSelection}
|
||||
/>
|
||||
)}
|
||||
|
||||
<main className="reader-pdf-container">
|
||||
<div className="reader-pdf-scroll">
|
||||
<PdfViewer
|
||||
document={pdfDocument}
|
||||
pageNumber={currentPage}
|
||||
scale={scale}
|
||||
highlights={pdfHighlights}
|
||||
pageWrapRef={pageWrapRef}
|
||||
/>
|
||||
</div>
|
||||
<BookmarkReaderRail
|
||||
|
||||
@@ -1,32 +1,15 @@
|
||||
import { useEffect, useRef, type RefObject } from "react";
|
||||
import { pdfjs, type PdfDocumentProxy } from "@/utils/pdfjsSetup";
|
||||
import type { PdfRect } from "@/utils/pdfAnchor";
|
||||
import { highlightBackgroundStyle } from "@/constants/bookmarkHighlightColors";
|
||||
|
||||
interface PdfHighlightOverlay {
|
||||
rects: PdfRect[];
|
||||
color: string;
|
||||
}
|
||||
import { useEffect, useRef } from "react";
|
||||
import type { PdfDocumentProxy } from "@/utils/pdfjsSetup";
|
||||
|
||||
interface PdfViewerProps {
|
||||
document: PdfDocumentProxy;
|
||||
pageNumber: number;
|
||||
scale: number;
|
||||
highlights?: PdfHighlightOverlay[];
|
||||
pageWrapRef?: RefObject<HTMLDivElement | null>;
|
||||
}
|
||||
|
||||
export function PdfViewer({
|
||||
document,
|
||||
pageNumber,
|
||||
scale,
|
||||
highlights = [],
|
||||
pageWrapRef,
|
||||
}: PdfViewerProps) {
|
||||
export function PdfViewer({ document, pageNumber, scale }: PdfViewerProps) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const textLayerRef = useRef<HTMLDivElement>(null);
|
||||
const internalWrapRef = useRef<HTMLDivElement>(null);
|
||||
const wrapRef = pageWrapRef ?? internalWrapRef;
|
||||
const wrapRef = useRef<HTMLDivElement>(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<void> };
|
||||
}
|
||||
).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 (
|
||||
<div ref={wrapRef} className="pdf-page-wrap">
|
||||
<canvas ref={canvasRef} className="pdf-page-canvas" />
|
||||
<div ref={textLayerRef} className="pdf-text-layer" />
|
||||
<div className="pdf-highlight-layer" aria-hidden>
|
||||
{highlights.flatMap((hl, hi) =>
|
||||
hl.rects.map((rect, ri) => {
|
||||
const [x, y, w, h] = rect;
|
||||
return (
|
||||
<div
|
||||
key={`${hi}-${ri}`}
|
||||
className="pdf-highlight-rect"
|
||||
style={{
|
||||
left: `${x * 100}%`,
|
||||
top: `${y * 100}%`,
|
||||
width: `${w * 100}%`,
|
||||
height: `${h * 100}%`,
|
||||
...highlightBackgroundStyle(hl.color),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{onBookmarkPage && (
|
||||
<button
|
||||
type="button"
|
||||
className="reader-bar-btn"
|
||||
onClick={onBookmarkPage}
|
||||
aria-label={t("reader.bookmarkPageAria")}
|
||||
aria-pressed={isCurrentPageBookmarked}
|
||||
title={t("reader.bookmarkPage")}
|
||||
>
|
||||
{isCurrentPageBookmarked ? (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{onToggleMarkers && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -81,9 +106,15 @@ export default function ReaderToolbar({
|
||||
onClick={onToggleMarkers}
|
||||
aria-label={t("annotations.inBookPanel")}
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
{onBookmarkPage ? (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user