diff --git a/apps/client/src/components/note_context.ts b/apps/client/src/components/note_context.ts index f8457aabd..b78cca3b3 100644 --- a/apps/client/src/components/note_context.ts +++ b/apps/client/src/components/note_context.ts @@ -31,6 +31,10 @@ export interface NoteContextDataMap { scrollToPage(page: number): void; requestThumbnail(page: number): void; }; + pdfAttachments: { + attachments: Array<{ filename: string; size: number }>; + downloadAttachment(filename: string): void; + }; } type ContextDataKey = keyof NoteContextDataMap; diff --git a/apps/client/src/widgets/sidebar/PdfAttachments.css b/apps/client/src/widgets/sidebar/PdfAttachments.css new file mode 100644 index 000000000..30c8fa573 --- /dev/null +++ b/apps/client/src/widgets/sidebar/PdfAttachments.css @@ -0,0 +1,57 @@ +.pdf-attachments-list { + width: 100%; +} + +.pdf-attachment-item { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + cursor: pointer; + border-bottom: 1px solid var(--main-border-color); + transition: background-color 0.2s; +} + +.pdf-attachment-item:hover { + background-color: var(--hover-item-background-color); +} + +.pdf-attachment-item:last-child { + border-bottom: none; +} + +.pdf-attachment-info { + flex: 1; + min-width: 0; +} + +.pdf-attachment-filename { + font-size: 13px; + font-weight: 500; + color: var(--main-text-color); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.pdf-attachment-size { + font-size: 11px; + color: var(--muted-text-color); + margin-top: 2px; +} + +.no-attachments { + padding: 16px; + text-align: center; + color: var(--muted-text-color); +} + +.pdf-attachment-item .bx { + flex-shrink: 0; + font-size: 18px; + color: var(--muted-text-color); +} + +.pdf-attachment-item:hover .bx { + color: var(--main-text-color); +} diff --git a/apps/client/src/widgets/sidebar/PdfAttachments.tsx b/apps/client/src/widgets/sidebar/PdfAttachments.tsx new file mode 100644 index 000000000..1627814af --- /dev/null +++ b/apps/client/src/widgets/sidebar/PdfAttachments.tsx @@ -0,0 +1,75 @@ +import "./PdfAttachments.css"; + +import { useActiveNoteContext, useGetContextData, useNoteProperty } from "../react/hooks"; +import Icon from "../react/Icon"; +import RightPanelWidget from "./RightPanelWidget"; + +interface AttachmentInfo { + filename: string; + size: number; +} + +export default function PdfAttachments() { + const { note } = useActiveNoteContext(); + const noteType = useNoteProperty(note, "type"); + const noteMime = useNoteProperty(note, "mime"); + + if (noteType !== "file" || noteMime !== "application/pdf") { + return null; + } + + return ( + + + + ); +} + +function PdfAttachmentsList() { + const attachmentsData = useGetContextData("pdfAttachments"); + + if (!attachmentsData || attachmentsData.attachments.length === 0) { + return
No attachments
; + } + + return ( +
+ {attachmentsData.attachments.map((attachment) => ( + + ))} +
+ ); +} + +function PdfAttachmentItem({ + attachment, + onDownload +}: { + attachment: AttachmentInfo; + onDownload: (filename: string) => void; +}) { + const sizeText = formatFileSize(attachment.size); + + return ( +
onDownload(attachment.filename)}> + +
+
{attachment.filename}
+
{sizeText}
+
+ +
+ ); +} + +function formatFileSize(bytes: number): string { + if (bytes === 0) return "0 B"; + const k = 1024; + const sizes = ["B", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${Math.round((bytes / Math.pow(k, i)) * 100) / 100 } ${ sizes[i]}`; +} diff --git a/apps/client/src/widgets/sidebar/RightPanelContainer.tsx b/apps/client/src/widgets/sidebar/RightPanelContainer.tsx index f22791762..8144972cb 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 PdfAttachments from "./PdfAttachments"; import PdfPages from "./PdfPages"; import RightPanelWidget from "./RightPanelWidget"; import TableOfContents from "./TableOfContents"; @@ -72,6 +73,10 @@ function useItems(rightPaneVisible: boolean, widgetsByParent: WidgetsByParent) { el: , enabled: isPdf, }, + { + el: , + enabled: isPdf, + }, { el: , enabled: noteType === "text" && highlightsList.length > 0, diff --git a/apps/client/src/widgets/type_widgets/file/Pdf.tsx b/apps/client/src/widgets/type_widgets/file/Pdf.tsx index c667414fd..bac8e3cf1 100644 --- a/apps/client/src/widgets/type_widgets/file/Pdf.tsx +++ b/apps/client/src/widgets/type_widgets/file/Pdf.tsx @@ -109,6 +109,18 @@ export default function PdfPreview({ note, blob, componentId, noteContext }: { } })); } + + if (event.data.type === "pdfjs-viewer-attachments") { + noteContext.setContextData("pdfAttachments", { + attachments: event.data.attachments, + downloadAttachment: (filename: string) => { + iframeRef.current?.contentWindow?.postMessage({ + type: "trilium-download-attachment", + filename + }, "*"); + } + }); + } } window.addEventListener("message", handleMessage); diff --git a/packages/pdfjs-viewer/src/attachments.ts b/packages/pdfjs-viewer/src/attachments.ts new file mode 100644 index 000000000..fefe554ee --- /dev/null +++ b/packages/pdfjs-viewer/src/attachments.ts @@ -0,0 +1,80 @@ +export async function setupPdfAttachments() { + const app = window.PDFViewerApplication; + + // Extract immediately since we're called after documentloaded + await extractAndSendAttachments(); + + // Listen for download requests + window.addEventListener("message", async (event) => { + if (event.data?.type === "trilium-download-attachment") { + const filename = event.data.filename; + await downloadAttachment(filename); + } + }); +} + +async function extractAndSendAttachments() { + const app = window.PDFViewerApplication; + + try { + const attachments = await app.pdfDocument.getAttachments(); + console.log("Got attachments:", attachments); + + if (!attachments) { + window.parent.postMessage({ + type: "pdfjs-viewer-attachments", + attachments: [] + }, "*"); + return; + } + + // Convert attachments object to array + const attachmentList = Object.entries(attachments).map(([filename, data]: [string, any]) => ({ + filename, + content: data.content, // Uint8Array + size: data.content?.length || 0 + })); + + // Send metadata only (not the full content) + window.parent.postMessage({ + type: "pdfjs-viewer-attachments", + attachments: attachmentList.map(att => ({ + filename: att.filename, + size: att.size + })) + }, "*"); + } catch (error) { + console.error("Error extracting attachments:", error); + window.parent.postMessage({ + type: "pdfjs-viewer-attachments", + attachments: [] + }, "*"); + } +} + +async function downloadAttachment(filename: string) { + const app = window.PDFViewerApplication; + + try { + const attachments = await app.pdfDocument.getAttachments(); + const attachment = attachments?.[filename]; + + if (!attachment) { + console.error("Attachment not found:", filename); + return; + } + + // Create blob and download + const blob = new Blob([attachment.content], { type: "application/octet-stream" }); + const url = URL.createObjectURL(blob); + + const a = document.createElement("a"); + a.href = url; + a.download = filename; + a.click(); + + URL.revokeObjectURL(url); + } catch (error) { + console.error("Error downloading attachment:", error); + } +} diff --git a/packages/pdfjs-viewer/src/custom.ts b/packages/pdfjs-viewer/src/custom.ts index 088ba771f..917cf5958 100644 --- a/packages/pdfjs-viewer/src/custom.ts +++ b/packages/pdfjs-viewer/src/custom.ts @@ -1,6 +1,7 @@ import interceptPersistence from "./persistence"; import { extractAndSendToc, setupScrollToHeading, setupActiveHeadingTracking } from "./toc"; import { setupPdfPages } from "./pages"; +import { setupPdfAttachments } from "./attachments"; const LOG_EVENT_BUS = false; @@ -23,6 +24,7 @@ async function main() { setupScrollToHeading(); setupActiveHeadingTracking(); setupPdfPages(); + setupPdfAttachments(); }); await app.initializedPromise; };