feat(right_pane): display attachments

This commit is contained in:
Elian Doran 2025-12-29 22:56:06 +02:00
parent c1d6b3121a
commit 43a749b6a7
No known key found for this signature in database
7 changed files with 235 additions and 0 deletions

View File

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

View File

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

View File

@ -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 (
<RightPanelWidget id="pdf-attachments" title="Attachments">
<PdfAttachmentsList key={note?.noteId} />
</RightPanelWidget>
);
}
function PdfAttachmentsList() {
const attachmentsData = useGetContextData("pdfAttachments");
if (!attachmentsData || attachmentsData.attachments.length === 0) {
return <div className="no-attachments">No attachments</div>;
}
return (
<div className="pdf-attachments-list">
{attachmentsData.attachments.map((attachment) => (
<PdfAttachmentItem
key={attachment.filename}
attachment={attachment}
onDownload={attachmentsData.downloadAttachment}
/>
))}
</div>
);
}
function PdfAttachmentItem({
attachment,
onDownload
}: {
attachment: AttachmentInfo;
onDownload: (filename: string) => void;
}) {
const sizeText = formatFileSize(attachment.size);
return (
<div className="pdf-attachment-item" onClick={() => onDownload(attachment.filename)}>
<Icon icon="bx bx-paperclip" />
<div className="pdf-attachment-info">
<div className="pdf-attachment-filename">{attachment.filename}</div>
<div className="pdf-attachment-size">{sizeText}</div>
</div>
<Icon icon="bx bx-download" />
</div>
);
}
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]}`;
}

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 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: <PdfPages />,
enabled: isPdf,
},
{
el: <PdfAttachments />,
enabled: isPdf,
},
{
el: <HighlightsList />,
enabled: noteType === "text" && highlightsList.length > 0,

View File

@ -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);

View File

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

View File

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