mirror of
https://github.com/zadam/trilium.git
synced 2026-03-06 18:56:09 +01:00
feat(client/right_pane): display pages
This commit is contained in:
parent
77ad6950e8
commit
bcf72f4624
@ -25,6 +25,12 @@ export type GetTextEditorCallback = (editor: CKTextEditor) => void;
|
|||||||
|
|
||||||
export interface NoteContextDataMap {
|
export interface NoteContextDataMap {
|
||||||
toc: HeadingContext;
|
toc: HeadingContext;
|
||||||
|
pdfPages: {
|
||||||
|
totalPages: number;
|
||||||
|
currentPage: number;
|
||||||
|
scrollToPage(page: number): void;
|
||||||
|
requestThumbnail(page: number): void;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContextDataKey = keyof NoteContextDataMap;
|
type ContextDataKey = keyof NoteContextDataMap;
|
||||||
|
|||||||
56
apps/client/src/widgets/sidebar/PdfPages.css
Normal file
56
apps/client/src/widgets/sidebar/PdfPages.css
Normal 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);
|
||||||
|
}
|
||||||
109
apps/client/src/widgets/sidebar/PdfPages.tsx
Normal file
109
apps/client/src/widgets/sidebar/PdfPages.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -15,6 +15,7 @@ import { useActiveNoteContext, useLegacyWidget, useNoteProperty, useTriliumEvent
|
|||||||
import Icon from "../react/Icon";
|
import Icon from "../react/Icon";
|
||||||
import LegacyRightPanelWidget from "../right_panel_widget";
|
import LegacyRightPanelWidget from "../right_panel_widget";
|
||||||
import HighlightsList from "./HighlightsList";
|
import HighlightsList from "./HighlightsList";
|
||||||
|
import PdfPages from "./PdfPages";
|
||||||
import RightPanelWidget from "./RightPanelWidget";
|
import RightPanelWidget from "./RightPanelWidget";
|
||||||
import TableOfContents from "./TableOfContents";
|
import TableOfContents from "./TableOfContents";
|
||||||
|
|
||||||
@ -59,14 +60,17 @@ function useItems(rightPaneVisible: boolean, widgetsByParent: WidgetsByParent) {
|
|||||||
const noteType = useNoteProperty(note, "type");
|
const noteType = useNoteProperty(note, "type");
|
||||||
const noteMime = useNoteProperty(note, "mime");
|
const noteMime = useNoteProperty(note, "mime");
|
||||||
const [ highlightsList ] = useTriliumOptionJson<string[]>("highlightsList");
|
const [ highlightsList ] = useTriliumOptionJson<string[]>("highlightsList");
|
||||||
|
const isPdf = noteType === "file" && noteMime === "application/pdf";
|
||||||
|
|
||||||
if (!rightPaneVisible) return [];
|
if (!rightPaneVisible) return [];
|
||||||
const definitions: RightPanelWidgetDefinition[] = [
|
const definitions: RightPanelWidgetDefinition[] = [
|
||||||
{
|
{
|
||||||
el: <TableOfContents />,
|
el: <TableOfContents />,
|
||||||
enabled: (noteType === "text"
|
enabled: (noteType === "text" || noteType === "doc" || isPdf),
|
||||||
|| noteType === "doc"
|
},
|
||||||
|| (noteType === "file" && noteMime === "application/pdf")),
|
{
|
||||||
|
el: <PdfPages />,
|
||||||
|
enabled: isPdf,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
el: <HighlightsList />,
|
el: <HighlightsList />,
|
||||||
|
|||||||
@ -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);
|
window.addEventListener("message", handleMessage);
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import interceptPersistence from "./persistence";
|
import interceptPersistence from "./persistence";
|
||||||
import { extractAndSendToc, setupScrollToHeading, setupActiveHeadingTracking } from "./toc";
|
import { extractAndSendToc, setupScrollToHeading, setupActiveHeadingTracking } from "./toc";
|
||||||
|
import { setupPdfPages } from "./pages";
|
||||||
|
|
||||||
const LOG_EVENT_BUS = false;
|
const LOG_EVENT_BUS = false;
|
||||||
|
|
||||||
@ -21,6 +22,7 @@ async function main() {
|
|||||||
extractAndSendToc();
|
extractAndSendToc();
|
||||||
setupScrollToHeading();
|
setupScrollToHeading();
|
||||||
setupActiveHeadingTracking();
|
setupActiveHeadingTracking();
|
||||||
|
setupPdfPages();
|
||||||
});
|
});
|
||||||
await app.initializedPromise;
|
await app.initializedPromise;
|
||||||
};
|
};
|
||||||
|
|||||||
93
packages/pdfjs-viewer/src/pages.ts
Normal file
93
packages/pdfjs-viewer/src/pages.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user