feat(client/right_pane): display pages

This commit is contained in:
Elian Doran 2025-12-29 22:34:36 +02:00
parent 77ad6950e8
commit bcf72f4624
No known key found for this signature in database
7 changed files with 313 additions and 3 deletions

View File

@ -25,6 +25,12 @@ export type GetTextEditorCallback = (editor: CKTextEditor) => void;
export interface NoteContextDataMap {
toc: HeadingContext;
pdfPages: {
totalPages: number;
currentPage: number;
scrollToPage(page: number): void;
requestThumbnail(page: number): void;
};
}
type ContextDataKey = keyof NoteContextDataMap;

View File

@ -0,0 +1,56 @@
.pdf-pages-list {
width: 100%;
height: 100%;
}
.pdf-page-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 8px;
cursor: pointer;
border: 2px solid transparent;
transition: border-color 0.2s;
}
.pdf-page-item:hover {
background-color: var(--hover-item-background-color);
}
.pdf-page-item.active {
border-color: var(--main-border-color);
background-color: var(--active-item-background-color);
}
.pdf-page-number {
font-size: 12px;
margin-bottom: 4px;
color: var(--main-text-color);
}
.pdf-page-thumbnail {
width: 100%;
height: 100px;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--accented-background-color);
border: 1px solid var(--main-border-color);
}
.pdf-page-thumbnail img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.pdf-page-loading {
color: var(--muted-text-color);
font-size: 11px;
}
.no-pages {
padding: 16px;
text-align: center;
color: var(--muted-text-color);
}

View File

@ -0,0 +1,109 @@
import "./PdfPages.css";
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
import { useActiveNoteContext, useGetContextData, useNoteProperty } from "../react/hooks";
import RightPanelWidget from "./RightPanelWidget";
export default function PdfPages() {
const { note } = useActiveNoteContext();
const noteType = useNoteProperty(note, "type");
const noteMime = useNoteProperty(note, "mime");
if (noteType !== "file" || noteMime !== "application/pdf") {
return null;
}
return (
<RightPanelWidget id="pdf-pages" title="Pages" grow>
<PdfPagesList />
</RightPanelWidget>
);
}
function PdfPagesList() {
const pagesData = useGetContextData("pdfPages");
const [thumbnails, setThumbnails] = useState<Map<number, string>>(new Map());
const requestedThumbnails = useRef<Set<number>>(new Set());
useEffect(() => {
// Listen for thumbnail responses via custom event
function handleThumbnail(event: CustomEvent) {
const { pageNumber, dataUrl } = event.detail;
console.log("[PdfPages] Received thumbnail for page:", pageNumber);
setThumbnails(prev => new Map(prev).set(pageNumber, dataUrl));
}
window.addEventListener("pdf-thumbnail", handleThumbnail as EventListener);
return () => {
window.removeEventListener("pdf-thumbnail", handleThumbnail as EventListener);
};
}, []);
const requestThumbnail = useCallback((pageNumber: number) => {
// Only request if we haven't already requested it and don't have it
if (!requestedThumbnails.current.has(pageNumber) && !thumbnails.has(pageNumber) && pagesData) {
console.log("[PdfPages] Requesting thumbnail for page:", pageNumber);
requestedThumbnails.current.add(pageNumber);
pagesData.requestThumbnail(pageNumber);
}
}, [pagesData, thumbnails]);
if (!pagesData || pagesData.totalPages === 0) {
return <div className="no-pages">No pages available</div>;
}
const pages = Array.from({ length: pagesData.totalPages }, (_, i) => i + 1);
return (
<div className="pdf-pages-list">
{pages.map(pageNumber => (
<PdfPageItem
key={pageNumber}
pageNumber={pageNumber}
isActive={pageNumber === pagesData.currentPage}
thumbnail={thumbnails.get(pageNumber)}
onRequestThumbnail={requestThumbnail}
onPageClick={() => pagesData.scrollToPage(pageNumber)}
/>
))}
</div>
);
}
function PdfPageItem({
pageNumber,
isActive,
thumbnail,
onRequestThumbnail,
onPageClick
}: {
pageNumber: number;
isActive: boolean;
thumbnail?: string;
}) {
const hasRequested = useRef(false);
useEffect(() => {
if (!thumbnail && !hasRequested.current) {
hasRequested.current = true;
onRequestThumbnail(pageNumber);
}
}, [pageNumber, thumbnail, onRequestThumbnail]);
return (
<div
className={`pdf-page-item ${isActive ? 'active' : ''}`}
onClick={onPageClick}
>
<div className="pdf-page-number">{pageNumber}</div>
<div className="pdf-page-thumbnail">
{thumbnail ? (
<img src={thumbnail} alt={`Page ${pageNumber}`} />
) : (
<div className="pdf-page-loading">Loading...</div>
)}
</div>
</div>
);
}

View File

@ -15,6 +15,7 @@ import { useActiveNoteContext, useLegacyWidget, useNoteProperty, useTriliumEvent
import Icon from "../react/Icon";
import LegacyRightPanelWidget from "../right_panel_widget";
import HighlightsList from "./HighlightsList";
import PdfPages from "./PdfPages";
import RightPanelWidget from "./RightPanelWidget";
import TableOfContents from "./TableOfContents";
@ -59,14 +60,17 @@ function useItems(rightPaneVisible: boolean, widgetsByParent: WidgetsByParent) {
const noteType = useNoteProperty(note, "type");
const noteMime = useNoteProperty(note, "mime");
const [ highlightsList ] = useTriliumOptionJson<string[]>("highlightsList");
const isPdf = noteType === "file" && noteMime === "application/pdf";
if (!rightPaneVisible) return [];
const definitions: RightPanelWidgetDefinition[] = [
{
el: <TableOfContents />,
enabled: (noteType === "text"
|| noteType === "doc"
|| (noteType === "file" && noteMime === "application/pdf")),
enabled: (noteType === "text" || noteType === "doc" || isPdf),
},
{
el: <PdfPages />,
enabled: isPdf,
},
{
el: <HighlightsList />,

View File

@ -70,6 +70,46 @@ export default function PdfPreview({ note, blob, componentId, noteContext }: {
});
}
}
if (event.data.type === "pdfjs-viewer-page-info") {
console.log("[Pdf.tsx] Received page info:", event.data);
noteContext.setContextData("pdfPages", {
totalPages: event.data.totalPages,
currentPage: event.data.currentPage,
scrollToPage: (page: number) => {
iframeRef.current?.contentWindow?.postMessage({
type: "trilium-scroll-to-page",
pageNumber: page
}, "*");
},
requestThumbnail: (page: number) => {
iframeRef.current?.contentWindow?.postMessage({
type: "trilium-request-thumbnail",
pageNumber: page
}, "*");
}
});
}
if (event.data.type === "pdfjs-viewer-current-page") {
const currentPages = noteContext.getContextData("pdfPages");
if (currentPages) {
noteContext.setContextData("pdfPages", {
...currentPages,
currentPage: event.data.currentPage
});
}
}
if (event.data.type === "pdfjs-viewer-thumbnail") {
// Forward thumbnail to any listeners
window.dispatchEvent(new CustomEvent("pdf-thumbnail", {
detail: {
pageNumber: event.data.pageNumber,
dataUrl: event.data.dataUrl
}
}));
}
}
window.addEventListener("message", handleMessage);

View File

@ -1,5 +1,6 @@
import interceptPersistence from "./persistence";
import { extractAndSendToc, setupScrollToHeading, setupActiveHeadingTracking } from "./toc";
import { setupPdfPages } from "./pages";
const LOG_EVENT_BUS = false;
@ -21,6 +22,7 @@ async function main() {
extractAndSendToc();
setupScrollToHeading();
setupActiveHeadingTracking();
setupPdfPages();
});
await app.initializedPromise;
};

View File

@ -0,0 +1,93 @@
export function setupPdfPages() {
const app = window.PDFViewerApplication;
// Send initial page info when pages are initialized
app.eventBus.on("pagesinit", () => {
sendPageInfo();
});
// Also send immediately if document is already loaded
if (app.pdfDocument && app.pdfViewer) {
sendPageInfo();
}
// Track current page changes
app.eventBus.on("pagechanging", (evt: any) => {
window.parent.postMessage({
type: "pdfjs-viewer-current-page",
currentPage: evt.pageNumber
}, "*");
});
// Listen for scroll-to-page requests
window.addEventListener("message", (event) => {
if (event.data?.type === "trilium-scroll-to-page") {
const pageNumber = event.data.pageNumber;
app.pdfViewer.currentPageNumber = pageNumber;
}
});
// Listen for thumbnail requests
window.addEventListener("message", async (event) => {
if (event.data?.type === "trilium-request-thumbnail") {
const pageNumber = event.data.pageNumber;
console.log("[PDF Pages] Received thumbnail request for page:", pageNumber);
await generateThumbnail(pageNumber);
}
});
}
function sendPageInfo() {
const app = window.PDFViewerApplication;
console.log("[PDF Pages] Sending page info:", {
totalPages: app.pdfDocument?.numPages,
currentPage: app.pdfViewer?.currentPageNumber
});
window.parent.postMessage({
type: "pdfjs-viewer-page-info",
totalPages: app.pdfDocument.numPages,
currentPage: app.pdfViewer.currentPageNumber
}, "*");
}
async function generateThumbnail(pageNumber: number) {
const app = window.PDFViewerApplication;
console.log("[PDF Pages] Generating thumbnail for page:", pageNumber);
try {
const page = await app.pdfDocument.getPage(pageNumber);
// Create canvas for thumbnail
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (!context) return;
// Set thumbnail size (smaller than actual page)
const viewport = page.getViewport({ scale: 0.2 });
canvas.width = viewport.width;
canvas.height = viewport.height;
// Render page to canvas
await page.render({
canvasContext: context,
viewport: viewport
}).promise;
// Convert to data URL
const dataUrl = canvas.toDataURL('image/jpeg', 0.7);
console.log("[PDF Pages] Sending thumbnail for page:", pageNumber, "size:", dataUrl.length);
// Send thumbnail to parent
window.parent.postMessage({
type: "pdfjs-viewer-thumbnail",
pageNumber,
dataUrl
}, "*");
} catch (error) {
console.error(`Error generating thumbnail for page ${pageNumber}:`, error);
}
}