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;
};