From b297436f264d2cf2ffed57c813e0a4071ccd864c Mon Sep 17 00:00:00 2001 From: talksik Date: Sun, 12 Apr 2026 14:12:36 -0700 Subject: [PATCH] support locally downloading attachments --- js/src/electron.d.ts | 3 +++ .../attachments/attachment-lightbox.tsx | 11 +--------- .../particles/particle-attachments.tsx | 14 ++---------- js/src/main.ts | 22 +++++++++++++++++++ js/src/preload.ts | 5 +++++ 5 files changed, 33 insertions(+), 22 deletions(-) diff --git a/js/src/electron.d.ts b/js/src/electron.d.ts index 16a096a..4c71340 100644 --- a/js/src/electron.d.ts +++ b/js/src/electron.d.ts @@ -44,6 +44,9 @@ declare global { fetchMetadata: (url: string) => Promise; openExternal: (url: string) => Promise; }; + electronAttachment: { + download: (url: string, filename?: string) => void; + }; electronApp: { setDockBadge: (count: number) => void; }; diff --git a/js/src/features/attachments/attachment-lightbox.tsx b/js/src/features/attachments/attachment-lightbox.tsx index 95a3780..98f75e4 100644 --- a/js/src/features/attachments/attachment-lightbox.tsx +++ b/js/src/features/attachments/attachment-lightbox.tsx @@ -49,15 +49,6 @@ function formatSize(bytes?: number): string | null { return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } -function downloadFromUrl(url: string, filename: string) { - const a = document.createElement("a"); - a.href = url; - a.download = filename; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); -} - export function AttachmentLightbox({ items, openIndex, @@ -104,7 +95,7 @@ export function AttachmentLightbox({ const handleDownload = () => { if (!current || !url || current.source.kind !== "remote") return; - downloadFromUrl(url, current.filename); + window.electronAttachment.download(url, current.filename); }; const handleRemove = () => { diff --git a/js/src/features/particles/particle-attachments.tsx b/js/src/features/particles/particle-attachments.tsx index 365ee3a..2555fc5 100644 --- a/js/src/features/particles/particle-attachments.tsx +++ b/js/src/features/particles/particle-attachments.tsx @@ -65,12 +65,7 @@ function ImageAttachment({ const handleDownload = (e: React.MouseEvent) => { e.stopPropagation(); - const a = document.createElement("a"); - a.href = url; - a.download = particle.properties.filename; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); + window.electronAttachment.download(url, particle.properties.filename); }; return ( @@ -118,12 +113,7 @@ function FileAttachment({ const handleDownload = (e: React.MouseEvent) => { e.stopPropagation(); if (!url) return; - const a = document.createElement("a"); - a.href = url; - a.download = particle.properties.filename; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); + window.electronAttachment.download(url, particle.properties.filename); }; return ( diff --git a/js/src/main.ts b/js/src/main.ts index 3db9d71..f1ff738 100644 --- a/js/src/main.ts +++ b/js/src/main.ts @@ -454,6 +454,28 @@ ipcMain.handle('link:open-external', async (_event, url: string) => { await shell.openExternal(url); }); +// --- Attachment download --- +// Triggers a native download with save-as dialog. Cross-origin safe — unlike +// the web `` hack, which is ignored for cross-origin URLs. +ipcMain.on( + 'attachment:download', + (event, payload: { url: string; filename?: string }) => { + const win = BrowserWindow.fromWebContents(event.sender); + if (!win) return; + const { url, filename } = payload ?? {}; + if (typeof url !== 'string') return; + if (!url.startsWith('http://') && !url.startsWith('https://')) return; + + const dlSession = win.webContents.session; + const onWillDownload = (_e: Electron.Event, item: Electron.DownloadItem) => { + if (filename) item.setSaveDialogOptions({ defaultPath: filename }); + dlSession.removeListener('will-download', onWillDownload); + }; + dlSession.on('will-download', onWillDownload); + win.webContents.downloadURL(url); + }, +); + // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. diff --git a/js/src/preload.ts b/js/src/preload.ts index 2e592d5..244d2d4 100644 --- a/js/src/preload.ts +++ b/js/src/preload.ts @@ -66,6 +66,11 @@ contextBridge.exposeInMainWorld('electronLink', { openExternal: (url: string) => ipcRenderer.invoke('link:open-external', url), }); +contextBridge.exposeInMainWorld('electronAttachment', { + download: (url: string, filename?: string) => + ipcRenderer.send('attachment:download', { url, filename }), +}); + contextBridge.exposeInMainWorld('electronApp', { setDockBadge: (count: number) => ipcRenderer.send('app:set-dock-badge', count), });