diff --git a/apps/client/src/components/note_context.ts b/apps/client/src/components/note_context.ts index c9261f38e..76de82b01 100644 --- a/apps/client/src/components/note_context.ts +++ b/apps/client/src/components/note_context.ts @@ -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; diff --git a/apps/client/src/widgets/sidebar/PdfPages.css b/apps/client/src/widgets/sidebar/PdfPages.css new file mode 100644 index 000000000..8c4d6970e --- /dev/null +++ b/apps/client/src/widgets/sidebar/PdfPages.css @@ -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); +} diff --git a/apps/client/src/widgets/sidebar/PdfPages.tsx b/apps/client/src/widgets/sidebar/PdfPages.tsx new file mode 100644 index 000000000..fe2018288 --- /dev/null +++ b/apps/client/src/widgets/sidebar/PdfPages.tsx @@ -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 ( + + + + ); +} + +function PdfPagesList() { + const pagesData = useGetContextData("pdfPages"); + const [thumbnails, setThumbnails] = useState>(new Map()); + const requestedThumbnails = useRef>(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
No pages available
; + } + + const pages = Array.from({ length: pagesData.totalPages }, (_, i) => i + 1); + + return ( +
+ {pages.map(pageNumber => ( + pagesData.scrollToPage(pageNumber)} + /> + ))} +
+ ); +} + +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 ( +
+
{pageNumber}
+
+ {thumbnail ? ( + {`Page + ) : ( +
Loading...
+ )} +
+
+ ); +} diff --git a/apps/client/src/widgets/sidebar/RightPanelContainer.tsx b/apps/client/src/widgets/sidebar/RightPanelContainer.tsx index 689360d5e..f22791762 100644 --- a/apps/client/src/widgets/sidebar/RightPanelContainer.tsx +++ b/apps/client/src/widgets/sidebar/RightPanelContainer.tsx @@ -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("highlightsList"); + const isPdf = noteType === "file" && noteMime === "application/pdf"; if (!rightPaneVisible) return []; const definitions: RightPanelWidgetDefinition[] = [ { el: , - enabled: (noteType === "text" - || noteType === "doc" - || (noteType === "file" && noteMime === "application/pdf")), + enabled: (noteType === "text" || noteType === "doc" || isPdf), + }, + { + el: , + enabled: isPdf, }, { el: , diff --git a/apps/client/src/widgets/type_widgets/file/Pdf.tsx b/apps/client/src/widgets/type_widgets/file/Pdf.tsx index 05b005b13..76de95554 100644 --- a/apps/client/src/widgets/type_widgets/file/Pdf.tsx +++ b/apps/client/src/widgets/type_widgets/file/Pdf.tsx @@ -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); diff --git a/packages/pdfjs-viewer/src/custom.ts b/packages/pdfjs-viewer/src/custom.ts index 3b0a6c001..088ba771f 100644 --- a/packages/pdfjs-viewer/src/custom.ts +++ b/packages/pdfjs-viewer/src/custom.ts @@ -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; }; diff --git a/packages/pdfjs-viewer/src/pages.ts b/packages/pdfjs-viewer/src/pages.ts new file mode 100644 index 000000000..079820796 --- /dev/null +++ b/packages/pdfjs-viewer/src/pages.ts @@ -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); + } +}