From 3ab3f775941f12fa470f1b998309ac3769290ef5 Mon Sep 17 00:00:00 2001 From: Local Dev Date: Fri, 23 Oct 2026 23:00:42 +0300 Subject: [PATCH] Theseus 0.3.109: Save page as PDF or PNG; connector design doc hosted on theseus.x Right-click on a tab now shows a Save page as submenu with PDF and Image (PNG) items. PDF uses webContents.printToPDF (A4, prefers CSS page size, prints backgrounds); PNG uses webContents.capturePage of the visible viewport. Both open a save dialog with a filename derived from the tab title or hostname, write to the chosen path, and register the result in the downloads list. The Claude connector's design doc now ships at theseus.x/dev/claude-connector/DESIGN-claude-connector.md so readers on the site do not have to leave for the forge; the Developers page links the local copy first and the forge mirror alongside it. --- main.js | 46 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/main.js b/main.js index 5bf00b81..23b48e28 100644 --- a/main.js +++ b/main.js @@ -3995,6 +3995,48 @@ function addonIdForSender(sender) { return first || null; } catch { return null; } } +// Right-click "Save page as…" → PDF via webContents.printToPDF, and PNG via +// webContents.capturePage (visible viewport). Both ask for a save path with +// a filename derived from the tab's title or hostname, write the bytes, and +// register the file in the downloads list so the toolbar's downloads popup +// shows it alongside normal downloads. +async function savePageSavePath(wc, ext, fileLabel) { + const title = (wc.getTitle() || "").trim(); + const host = (() => { try { return new URL(wc.getURL()).hostname; } catch { return ""; } })(); + const base = (title || host || "page").replace(/[\\/:*?"<>|]/g, "_").replace(/\s+/g, " ").slice(0, 120); + const def = path.join(app.getPath("downloads"), `${base}.${ext}`); + const parent = BrowserWindow.fromWebContents(wc) || win || BrowserWindow.getFocusedWindow(); + const r = await dialog.showSaveDialog(parent, { + title: `Save page as ${fileLabel}`, + defaultPath: def, + filters: [{ name: fileLabel, extensions: [ext] }], + }); + if (r.canceled || !r.filePath) return null; + return r.filePath; +} +function registerSavedFile(filePath, mime) { + try { + const st = fs.statSync(filePath); + downloads.unshift({ id: nextDlId++, name: path.basename(filePath), path: filePath, size: st.size, mime: mime || "application/octet-stream", state: "completed", receivedBytes: st.size, totalBytes: st.size, startTime: Date.now() }); + try { emitDownloads?.(); } catch {} + } catch (e) { console.warn("registerSavedFile:", e?.message); } +} +async function savePageAsPdf(wc) { + if (!wc || wc.isDestroyed()) return; + const outPath = await savePageSavePath(wc, "pdf", "PDF"); + if (!outPath) return; + const buf = await wc.printToPDF({ printBackground: true, pageSize: "A4", margins: { marginType: "default" }, preferCSSPageSize: true }); + fs.writeFileSync(outPath, buf); + registerSavedFile(outPath, "application/pdf"); +} +async function savePageAsImage(wc) { + if (!wc || wc.isDestroyed()) return; + const outPath = await savePageSavePath(wc, "png", "PNG image"); + if (!outPath) return; + const img = await wc.capturePage(); + fs.writeFileSync(outPath, img.toPNG()); + registerSavedFile(outPath, "image/png"); +} // In-memory download list. Not persisted: closing the browser clears history // (the files are still on disk; only the list of "recent downloads" is dropped). const downloads = []; let nextDlId = 1; const dlItems = new Map(); // id -> DownloadItem @@ -5492,6 +5534,10 @@ function createTab(initial, opts = {}) { { label: "Back", enabled: wc.navigationHistory.canGoBack(), click: () => wc.navigationHistory.goBack() }, { label: "Forward", enabled: wc.navigationHistory.canGoForward(), click: () => wc.navigationHistory.goForward() }, { label: "Reload", click: () => wc.reload() }, + { label: "Save page as…", submenu: [ + { label: "PDF…", click: () => savePageAsPdf(wc).catch((e) => console.warn("save-pdf:", e?.message)) }, + { label: "Image (PNG)…", click: () => savePageAsImage(wc).catch((e) => console.warn("save-png:", e?.message)) }, + ]}, ); if (tab.webapp) { const inst = webapps.find(tab.webapp.key); diff --git a/package.json b/package.json index 541bdd57..2433c08f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "theseus-navigator", - "version": "0.3.108", + "version": "0.3.109", "license": "MPL-2.0", "description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.", "author": "Silent Mode",