Feature/uv implementation #26

Merged
crisleo94 merged 7 commits from feature/uv-implementation into main 2026-06-04 12:10:52 +00:00
11 changed files with 111 additions and 198 deletions
Showing only changes of commit f989b144cf - Show all commits
@@ -12,6 +12,7 @@ interface BookMarkersPanelProps {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
onGoToPassage: (marker: MarkerEntry) => void; onGoToPassage: (marker: MarkerEntry) => void;
emptyHintKey?: string;
} }
export function BookMarkersPanel({ export function BookMarkersPanel({
@@ -19,6 +20,7 @@ export function BookMarkersPanel({
isOpen, isOpen,
onClose, onClose,
onGoToPassage, onGoToPassage,
emptyHintKey = "annotations.selectTextHint",
}: BookMarkersPanelProps) { }: BookMarkersPanelProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { markers, loadBookmarks, removeBookmark, state } = useAnnotations(); const { markers, loadBookmarks, removeBookmark, state } = useAnnotations();
@@ -43,7 +45,7 @@ export function BookMarkersPanel({
{state.bookmarksLoading ? ( {state.bookmarksLoading ? (
<p className="annotations-loading">{t("annotations.loading")}</p> <p className="annotations-loading">{t("annotations.loading")}</p>
) : markers.length === 0 ? ( ) : markers.length === 0 ? (
<p className="annotations-empty">{t("annotations.selectTextHint")}</p> <p className="annotations-empty">{t(emptyHintKey)}</p>
) : ( ) : (
<ul className="marker-thread-list"> <ul className="marker-thread-list">
{markers.map((m) => ( {markers.map((m) => (
@@ -51,13 +53,15 @@ export function BookMarkersPanel({
{m.chapter_title && ( {m.chapter_title && (
<span className="marker-thread-chapter">{m.chapter_title}</span> <span className="marker-thread-chapter">{m.chapter_title}</span>
)} )}
{m.location_text && ( {m.location_text ? (
<CollapsibleMarkerPassage <CollapsibleMarkerPassage
text={m.location_text} text={m.location_text}
highlightColor={m.highlight_color} highlightColor={m.highlight_color}
className="annotation-quote" className="annotation-quote"
/> />
)} ) : m.chapter_title ? (
<span className="annotation-quote marker-page-label">{m.chapter_title}</span>
) : null}
{m.content ? ( {m.content ? (
<CollapsibleMarkerThought text={m.content} /> <CollapsibleMarkerThought text={m.content} />
) : ( ) : (
@@ -20,7 +20,9 @@ function RailMarker({
const { t } = useTranslation(); const { t } = useTranslation();
const [hovered, setHovered] = useState(false); const [hovered, setHovered] = useState(false);
const color = normalizeHighlightColor(item.marker.highlight_color); 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 const notePreview = item.marker.content
? truncateRailPreview(item.marker.content, 80) ? truncateRailPreview(item.marker.content, 80)
: ""; : "";
@@ -7,16 +7,14 @@ import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18n-lite"; import { useTranslation } from "react-i18n-lite";
import { useAnnotations } from "@/context/AnnotationsContext"; import { useAnnotations } from "@/context/AnnotationsContext";
import { useBookmarkRailLayout } from "../../hooks/useBookmarkRailLayout"; import { useBookmarkRailLayout } from "../../hooks/useBookmarkRailLayout";
import { usePdfHighlights } from "../../hooks/usePdfHighlights";
import { usePdfReader } from "../../hooks/usePdfReader"; import { usePdfReader } from "../../hooks/usePdfReader";
import { usePdfSelection } from "../../hooks/usePdfSelection";
import { useReadingSettings } from "../../hooks/useReadingSettings"; import { useReadingSettings } from "../../hooks/useReadingSettings";
import { useToast } from "../../hooks/useToast";
import { booksApi } from "../../api/books"; import { booksApi } from "../../api/books";
import type { EBookDetail } from "../../types/book"; import type { EBookDetail } from "../../types/book";
import type { EpubTocItem } from "./TableOfContents"; import type { EpubTocItem } from "./TableOfContents";
import { parsePageHref } from "../../utils/pdfAnchor"; import { parsePageHref, serializePdfAnchor } from "../../utils/pdfAnchor";
import { fallbackPdfPageToc } from "../../utils/pdfToc"; import { fallbackPdfPageToc } from "../../utils/pdfToc";
import { SelectionPopover } from "./SelectionPopover";
import { BookMarkersPanel } from "./BookMarkersPanel"; import { BookMarkersPanel } from "./BookMarkersPanel";
import { BookmarkReaderRail } from "./BookmarkReaderRail"; import { BookmarkReaderRail } from "./BookmarkReaderRail";
import { ResumeReadingButton } from "./ResumeReadingButton"; import { ResumeReadingButton } from "./ResumeReadingButton";
@@ -37,6 +35,8 @@ interface PdfReadingViewProps {
export function PdfReadingView({ book, bookId, initialAnchor }: PdfReadingViewProps) { export function PdfReadingView({ book, bookId, initialAnchor }: PdfReadingViewProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const { showToast } = useToast();
const { markers, addMarker, loadBookmarks } = useAnnotations();
const [tocOpen, setTocOpen] = useState(false); const [tocOpen, setTocOpen] = useState(false);
const [settingsOpen, setSettingsOpen] = useState(false); const [settingsOpen, setSettingsOpen] = useState(false);
@@ -57,7 +57,6 @@ export function PdfReadingView({ book, bookId, initialAnchor }: PdfReadingViewPr
error, error,
isBookmarkPeekActive, isBookmarkPeekActive,
readingAnchor, readingAnchor,
pageWrapRef,
goToPage, goToPage,
goToNextPage, goToNextPage,
goToPrevPage, goToPrevPage,
@@ -66,21 +65,18 @@ export function PdfReadingView({ book, bookId, initialAnchor }: PdfReadingViewPr
goToAnchor, goToAnchor,
} = usePdfReader(bookId, book.page_count, initialAnchor); } = usePdfReader(bookId, book.page_count, initialAnchor);
const { pendingSelection, clearSelection } = usePdfSelection(
pageWrapRef,
currentPage,
chapterTitle,
);
const { markers } = useAnnotations();
const bookMarkers = useMemo( const bookMarkers = useMemo(
() => markers.filter((m) => m.ebook_id === bookId), () => markers.filter((m) => m.ebook_id === bookId),
[markers, bookId], [markers, bookId],
); );
const pdfHighlights = usePdfHighlights(bookId, currentPage);
const railItems = useBookmarkRailLayout(bookMarkers); const railItems = useBookmarkRailLayout(bookMarkers);
useEffect(() => {
if (!bookId || Number.isNaN(bookId)) return;
void loadBookmarks(bookId);
}, [bookId, loadBookmarks]);
useEffect(() => { useEffect(() => {
void booksApi void booksApi
.getToc(bookId) .getToc(bookId)
@@ -129,6 +125,29 @@ export function PdfReadingView({ book, bookId, initialAnchor }: PdfReadingViewPr
[beginBookmarkPeek], [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(() => { const progressForToolbar = useMemo(() => {
if (!progress) return progress; if (!progress) return progress;
const pct = pageCount > 0 ? Math.round((currentPage / pageCount) * 100) : 0; const pct = pageCount > 0 ? Math.round((currentPage / pageCount) * 100) : 0;
@@ -140,6 +159,11 @@ export function PdfReadingView({ book, bookId, initialAnchor }: PdfReadingViewPr
}; };
}, [progress, currentPage, pageCount]); }, [progress, currentPage, pageCount]);
const isCurrentPageBookmarked = useMemo(() => {
const anchor = serializePdfAnchor(currentPage);
return bookMarkers.some((m) => m.epub_cfi === anchor);
}, [bookMarkers, currentPage]);
if (isLoading) { if (isLoading) {
return ( return (
<div className="reader-loading"> <div className="reader-loading">
@@ -179,6 +203,8 @@ export function PdfReadingView({ book, bookId, initialAnchor }: PdfReadingViewPr
onToggleToc={() => setTocOpen((v) => !v)} onToggleToc={() => setTocOpen((v) => !v)}
onToggleSettings={() => setSettingsOpen((v) => !v)} onToggleSettings={() => setSettingsOpen((v) => !v)}
onToggleMarkers={() => setMarkersOpen((v) => !v)} onToggleMarkers={() => setMarkersOpen((v) => !v)}
onBookmarkPage={handleBookmarkPage}
isCurrentPageBookmarked={isCurrentPageBookmarked}
/> />
<PdfLimitationsNotice bookId={bookId} /> <PdfLimitationsNotice bookId={bookId} />
@@ -195,6 +221,7 @@ export function PdfReadingView({ book, bookId, initialAnchor }: PdfReadingViewPr
isOpen={markersOpen} isOpen={markersOpen}
onClose={() => setMarkersOpen(false)} onClose={() => setMarkersOpen(false)}
onGoToPassage={handleGoToMarker} onGoToPassage={handleGoToMarker}
emptyHintKey="annotations.pdfPageBookmarkHint"
/> />
<ReadingSettingsPanel <ReadingSettingsPanel
@@ -208,23 +235,12 @@ export function PdfReadingView({ book, bookId, initialAnchor }: PdfReadingViewPr
onFlush={flushSettings} onFlush={flushSettings}
/> />
{pendingSelection && (
<SelectionPopover
ebookId={bookId}
selection={pendingSelection}
onClose={clearSelection}
onSaved={clearSelection}
/>
)}
<main className="reader-pdf-container"> <main className="reader-pdf-container">
<div className="reader-pdf-scroll"> <div className="reader-pdf-scroll">
<PdfViewer <PdfViewer
document={pdfDocument} document={pdfDocument}
pageNumber={currentPage} pageNumber={currentPage}
scale={scale} scale={scale}
highlights={pdfHighlights}
pageWrapRef={pageWrapRef}
/> />
</div> </div>
<BookmarkReaderRail <BookmarkReaderRail
+6 -67
View File
@@ -1,32 +1,15 @@
import { useEffect, useRef, type RefObject } from "react"; import { useEffect, useRef } from "react";
import { pdfjs, type PdfDocumentProxy } from "@/utils/pdfjsSetup"; import type { PdfDocumentProxy } from "@/utils/pdfjsSetup";
import type { PdfRect } from "@/utils/pdfAnchor";
import { highlightBackgroundStyle } from "@/constants/bookmarkHighlightColors";
interface PdfHighlightOverlay {
rects: PdfRect[];
color: string;
}
interface PdfViewerProps { interface PdfViewerProps {
document: PdfDocumentProxy; document: PdfDocumentProxy;
pageNumber: number; pageNumber: number;
scale: number; scale: number;
highlights?: PdfHighlightOverlay[];
pageWrapRef?: RefObject<HTMLDivElement | null>;
} }
export function PdfViewer({ export function PdfViewer({ document, pageNumber, scale }: PdfViewerProps) {
document,
pageNumber,
scale,
highlights = [],
pageWrapRef,
}: PdfViewerProps) {
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
const textLayerRef = useRef<HTMLDivElement>(null); const wrapRef = useRef<HTMLDivElement>(null);
const internalWrapRef = useRef<HTMLDivElement>(null);
const wrapRef = pageWrapRef ?? internalWrapRef;
const renderTaskRef = useRef<{ cancel: () => void } | null>(null); const renderTaskRef = useRef<{ cancel: () => void } | null>(null);
useEffect(() => { useEffect(() => {
@@ -39,8 +22,7 @@ export function PdfViewer({
} }
const canvas = canvasRef.current; const canvas = canvasRef.current;
const textLayer = textLayerRef.current; if (!canvas) return;
if (!canvas || !textLayer) return;
try { try {
const page = await document.getPage(pageNumber); const page = await document.getPage(pageNumber);
@@ -49,9 +31,6 @@ export function PdfViewer({
const viewport = page.getViewport({ scale }); const viewport = page.getViewport({ scale });
canvas.width = viewport.width; canvas.width = viewport.width;
canvas.height = viewport.height; canvas.height = viewport.height;
textLayer.style.width = `${viewport.width}px`;
textLayer.style.height = `${viewport.height}px`;
textLayer.innerHTML = "";
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
if (!ctx) return; if (!ctx) return;
@@ -59,27 +38,8 @@ export function PdfViewer({
const renderTask = page.render({ canvasContext: ctx, viewport }); const renderTask = page.render({ canvasContext: ctx, viewport });
renderTaskRef.current = renderTask; renderTaskRef.current = renderTask;
await renderTask.promise; 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 { } catch {
if (!cancelled) { /* render failed */
textLayerRef.current && (textLayerRef.current.innerHTML = "");
}
} }
} }
@@ -97,27 +57,6 @@ export function PdfViewer({
return ( return (
<div ref={wrapRef} className="pdf-page-wrap"> <div ref={wrapRef} className="pdf-page-wrap">
<canvas ref={canvasRef} className="pdf-page-canvas" /> <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> </div>
); );
} }
@@ -13,6 +13,9 @@ interface ReaderToolbarProps {
onToggleToc: () => void; onToggleToc: () => void;
onToggleSettings: () => void; onToggleSettings: () => void;
onToggleMarkers?: () => void; onToggleMarkers?: () => void;
onBookmarkPage?: () => void;
/** PDF: filled bookmark when the current page is already bookmarked */
isCurrentPageBookmarked?: boolean;
} }
export default function ReaderToolbar({ export default function ReaderToolbar({
@@ -23,6 +26,8 @@ export default function ReaderToolbar({
onToggleToc, onToggleToc,
onToggleSettings, onToggleSettings,
onToggleMarkers, onToggleMarkers,
onBookmarkPage,
isCurrentPageBookmarked = false,
}: ReaderToolbarProps) { }: ReaderToolbarProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const percentage = progress?.percentage ?? 0; const percentage = progress?.percentage ?? 0;
@@ -74,6 +79,26 @@ export default function ReaderToolbar({
</svg> </svg>
</button> </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 && ( {onToggleMarkers && (
<button <button
type="button" type="button"
@@ -81,9 +106,15 @@ export default function ReaderToolbar({
onClick={onToggleMarkers} onClick={onToggleMarkers}
aria-label={t("annotations.inBookPanel")} aria-label={t("annotations.inBookPanel")}
> >
{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"> <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" /> <path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
</svg> </svg>
)}
</button> </button>
)} )}
<button <button
-37
View File
@@ -1,37 +0,0 @@
import { useEffect, useMemo } from "react";
import { useAnnotations } from "@/context/AnnotationsContext";
import { isPdfAnchor, parsePdfAnchor } from "@/utils/pdfAnchor";
import { normalizeHighlightColor } from "@/constants/bookmarkHighlightColors";
export interface PdfPageHighlight {
rects: [number, number, number, number][];
color: string;
}
export function usePdfHighlights(ebookId: number, currentPage: number): PdfPageHighlight[] {
const { markers, loadBookmarks } = useAnnotations();
const bookMarkers = useMemo(
() => markers.filter((m) => m.ebook_id === ebookId),
[markers, ebookId],
);
useEffect(() => {
if (!ebookId || Number.isNaN(ebookId)) return;
void loadBookmarks(ebookId);
}, [ebookId, loadBookmarks]);
return useMemo(() => {
const highlights: PdfPageHighlight[] = [];
for (const marker of bookMarkers) {
if (!isPdfAnchor(marker.epub_cfi)) continue;
const parsed = parsePdfAnchor(marker.epub_cfi);
if (!parsed || parsed.page !== currentPage || !parsed.rects.length) continue;
highlights.push({
rects: parsed.rects,
color: normalizeHighlightColor(marker.highlight_color),
});
}
return highlights;
}, [bookMarkers, currentPage]);
}
-3
View File
@@ -26,7 +26,6 @@ export interface UsePdfReaderReturn {
error: string | null; error: string | null;
isBookmarkPeekActive: boolean; isBookmarkPeekActive: boolean;
readingAnchor: PdfReadingAnchor | null; readingAnchor: PdfReadingAnchor | null;
pageWrapRef: React.RefObject<HTMLDivElement | null>;
goToPage: (page: number) => void; goToPage: (page: number) => void;
goToNextPage: () => void; goToNextPage: () => void;
goToPrevPage: () => void; goToPrevPage: () => void;
@@ -55,7 +54,6 @@ export function usePdfReader(
const [isBookmarkPeekActive, setIsBookmarkPeekActive] = useState(false); const [isBookmarkPeekActive, setIsBookmarkPeekActive] = useState(false);
const [readingAnchor, setReadingAnchor] = useState<PdfReadingAnchor | null>(null); const [readingAnchor, setReadingAnchor] = useState<PdfReadingAnchor | null>(null);
const pageWrapRef = useRef<HTMLDivElement | null>(null);
const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null); const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const isBookmarkPeekRef = useRef(false); const isBookmarkPeekRef = useRef(false);
const readingAnchorRef = useRef<PdfReadingAnchor | null>(null); const readingAnchorRef = useRef<PdfReadingAnchor | null>(null);
@@ -247,7 +245,6 @@ export function usePdfReader(
error, error,
isBookmarkPeekActive, isBookmarkPeekActive,
readingAnchor, readingAnchor,
pageWrapRef,
goToPage, goToPage,
goToNextPage, goToNextPage,
goToPrevPage, goToPrevPage,
-57
View File
@@ -1,57 +0,0 @@
import { useCallback, useEffect, useRef, useState } from "react";
import type { PendingSelection } from "./useEpubSelection";
import { rectsFromSelection, serializePdfAnchor } from "../utils/pdfAnchor";
export function usePdfSelection(
pageWrapRef: React.RefObject<HTMLDivElement | null>,
currentPage: number,
chapterTitle: string,
) {
const [pendingSelection, setPendingSelection] = useState<PendingSelection | null>(null);
const chapterTitleRef = useRef(chapterTitle);
chapterTitleRef.current = chapterTitle;
const clearSelection = useCallback(() => {
setPendingSelection(null);
}, []);
useEffect(() => {
const wrap = pageWrapRef.current;
if (!wrap) return;
const handler = () => {
window.setTimeout(() => {
const sel = window.getSelection();
const text = sel?.toString()?.trim() ?? "";
if (!text || !sel || sel.rangeCount === 0) {
setPendingSelection(null);
return;
}
const range = sel.getRangeAt(0);
if (!wrap.contains(range.commonAncestorContainer)) {
setPendingSelection(null);
return;
}
const rects = rectsFromSelection(range, wrap);
const anchor = serializePdfAnchor(currentPage, rects.length > 0 ? rects : undefined);
const rect = range.getBoundingClientRect();
setPendingSelection({
epubCfi: anchor,
locationText: text.slice(0, 2000),
chapterIndex: currentPage - 1,
chapterTitle: chapterTitleRef.current,
anchorTop: rect.bottom + 8,
anchorLeft: Math.min(rect.left, window.innerWidth - 280),
});
}, 10);
};
wrap.addEventListener("mouseup", handler);
return () => wrap.removeEventListener("mouseup", handler);
}, [pageWrapRef, currentPage]);
return { pendingSelection, clearSelection };
}
+6 -1
View File
@@ -125,8 +125,12 @@ const enUS = {
pdfZoomAria: "PDF zoom level", pdfZoomAria: "PDF zoom level",
pdfLimitationsTitle: "PDF reading", pdfLimitationsTitle: "PDF reading",
pdfLimitationsBody: pdfLimitationsBody:
"This file is a PDF. Some features are limited compared to EPUB: bookmarks and notes are not available, and there are no chapters in the table of contents—only pages.", "PDF reading uses page bookmarks (filled bookmark icon in the toolbar). Passage highlights need selectable text and are not available on scanned pages. The table of contents lists pages, not EPUB-style chapters.",
pdfLimitationsDismissAria: "Dismiss PDF limitations notice", pdfLimitationsDismissAria: "Dismiss PDF limitations notice",
bookmarkPage: "Bookmark this page",
bookmarkPageAria: "Bookmark current page",
pageBookmarked: "Page bookmarked",
pageAlreadyBookmarked: "This page is already bookmarked",
backToLibrary: "Back to library", backToLibrary: "Back to library",
prevPage: "Previous page", prevPage: "Previous page",
nextPage: "Next page", nextPage: "Next page",
@@ -217,6 +221,7 @@ const enUS = {
highlightBlue: "Blue", highlightBlue: "Blue",
highlightPurple: "Purple", highlightPurple: "Purple",
selectTextHint: "Select text in the reader to add a bookmark or note.", selectTextHint: "Select text in the reader to add a bookmark or note.",
pdfPageBookmarkHint: "Use the filled bookmark button in the toolbar to save the current page.",
showMore: "Show more", showMore: "Show more",
showLess: "Show less", showLess: "Show less",
goToPassage: "Go to passage", goToPassage: "Go to passage",
+6 -1
View File
@@ -127,8 +127,12 @@ const esES: Locale = {
pdfZoomAria: "Nivel de zoom del PDF", pdfZoomAria: "Nivel de zoom del PDF",
pdfLimitationsTitle: "Lectura en PDF", pdfLimitationsTitle: "Lectura en PDF",
pdfLimitationsBody: pdfLimitationsBody:
"Este archivo es un PDF. Algunas funciones están limitadas respecto al EPUB: no hay marcadores ni notas, y la tabla de contenidos no muestra capítulos, solo páginas.", "En PDF puedes guardar marcadores de página (icono de marcador relleno en la barra). Los resaltados de pasaje requieren texto seleccionable y no están disponibles en páginas escaneadas. La tabla de contenidos muestra páginas, no capítulos como en EPUB.",
pdfLimitationsDismissAria: "Cerrar aviso de limitaciones del PDF", pdfLimitationsDismissAria: "Cerrar aviso de limitaciones del PDF",
bookmarkPage: "Marcar esta página",
bookmarkPageAria: "Marcar la página actual",
pageBookmarked: "Página marcada",
pageAlreadyBookmarked: "Esta página ya está marcada",
backToLibrary: "Volver a la biblioteca", backToLibrary: "Volver a la biblioteca",
prevPage: "Página anterior", prevPage: "Página anterior",
nextPage: "Página siguiente", nextPage: "Página siguiente",
@@ -219,6 +223,7 @@ const esES: Locale = {
highlightBlue: "Azul", highlightBlue: "Azul",
highlightPurple: "Morado", highlightPurple: "Morado",
selectTextHint: "Selecciona texto en el lector para añadir un marcador o nota.", selectTextHint: "Selecciona texto en el lector para añadir un marcador o nota.",
pdfPageBookmarkHint: "Usa el botón de marcador relleno en la barra para guardar la página actual.",
showMore: "Ver más", showMore: "Ver más",
showLess: "Ver menos", showLess: "Ver menos",
goToPassage: "Ir al pasaje", goToPassage: "Ir al pasaje",
+9 -1
View File
@@ -226,11 +226,19 @@
gap: 4px; gap: 4px;
max-height: min(45vh, 360px); max-height: min(45vh, 360px);
overflow-y: auto; overflow-y: auto;
overflow-x: visible; overflow-x: hidden;
margin: 0; margin: 0;
padding: 0; padding: 0;
list-style: none; list-style: none;
pointer-events: none; pointer-events: none;
scrollbar-width: none;
-ms-overflow-style: none;
}
.reader-bookmark-strip::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
} }
.reader-bookmark-marker-wrap { .reader-bookmark-marker-wrap {