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.
This commit is contained in:
Local Dev 2026-10-23 23:00:42 +03:00
parent a639f7a80d
commit 3ab3f77594
2 changed files with 47 additions and 1 deletions

46
main.js
View file

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

View file

@ -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",