Ship Theseus 0.3.13 7d88e4c4 (Ariadne toggle + 'p' record + Screenshot addon)
Setup 7d88e4c46b02448e40d6075d10f2c6688c6d60c5a9ec41b9cbcb7684f131d6e1 Portable 5a4bcc6abb21c23729d79dd600142df4f171cc3d6bf71716dae1c802ac10e48f Bundled since 0.3.12: 1514793 - Settings > Registries gets an on/off toggle for Ariadne's Thread (system-wide BCDN resolver for non-Theseus browsers). Query is silent Get-ScheduledTask; toggle spawns elevated PowerShell (UAC once per action). Three states: running / stopped / not-installed. b16f0a1 - New BCDN 'p' record type in Argus record-picker + Theseus serving. Reverse-proxies an upstream URL under a BCDN name, keeping the BCDN name in the address bar; uses upstream's own DNS + public CA + Host header (unlike 'ip' which pins IP + on-chain TLS fingerprint). Placed after 'ip' in the apex chain, suppressed under subdomain inheritance so a 'p' name doesn't silently proxy every subdomain. cfec253 - Argus registrar gains buildTldRegistrationTx + TLD_BEACON + normalizeTld exports for minting per-TLD certificates per the TLD- registry design. bbfc05c - Bundled Screenshot add-on: capture-tab capability + sidebar launcher for visible / full page / region modes; saves to Downloads. Follow-up task_b9608dc6 will rework this into a full-tab editor. Deployed. Verified LIVE 0.3.13.
This commit is contained in:
parent
10f01644c1
commit
57d71a0996
3 changed files with 160 additions and 3 deletions
|
|
@ -31,7 +31,13 @@ const KNOWN_CAPABILITIES = new Set([
|
|||
// URL matches one of the origin patterns.
|
||||
// approval-modal: api.approvalModal({...}) — user-facing consent dialog over
|
||||
// the active tab, resolved by main.
|
||||
"vault-derive", "page-inject", "approval-modal",
|
||||
// capture-tab: api.captureTab({mode, ...}) + api.saveCapture({dataUrl, filename})
|
||||
// — snapshot the active tab (visible viewport / full page /
|
||||
// user-drawn rectangle) and save the result through the app's
|
||||
// downloads pipeline. The add-on sees pixels of whatever the
|
||||
// current tab is showing, so this is the same trust bar as a
|
||||
// page-inject add-on that matches "*://*/*".
|
||||
"vault-derive", "page-inject", "approval-modal", "capture-tab",
|
||||
]);
|
||||
|
||||
// Chrome-style match pattern → predicate. "<scheme>://<host>/<path>" where
|
||||
|
|
@ -99,7 +105,7 @@ function validateManifest(raw, folderName) {
|
|||
// Loader singleton. `discoverAndActivate(opts)` returns a snapshot the rest
|
||||
// of the app queries via `getActive()` / `getInstalled()`.
|
||||
class AddonHost {
|
||||
constructor({ addonsDir, dataDir, isDisabled, logger, setSessionProxy, vaultDerive, approvalModal, emitToPanel, hostRequire, hostImport, openTab }) {
|
||||
constructor({ addonsDir, dataDir, isDisabled, logger, setSessionProxy, vaultDerive, approvalModal, emitToPanel, hostRequire, hostImport, openTab, captureTab, saveCapture }) {
|
||||
this.addonsDir = addonsDir;
|
||||
this.dataDir = dataDir;
|
||||
this.isDisabled = isDisabled || (() => false);
|
||||
|
|
@ -125,6 +131,10 @@ class AddonHost {
|
|||
// A `null` rule clears the proxy. Kept as a callback rather than requiring
|
||||
// the loader itself import electron.
|
||||
this._setSessionProxy = typeof setSessionProxy === "function" ? setSessionProxy : null;
|
||||
// capture-tab hooks — main captures/saves; the loader only enforces the
|
||||
// manifest gate.
|
||||
this._captureTab = typeof captureTab === "function" ? captureTab : null;
|
||||
this._saveCapture = typeof saveCapture === "function" ? saveCapture : null;
|
||||
}
|
||||
|
||||
ensureDirs() {
|
||||
|
|
@ -330,6 +340,35 @@ class AddonHost {
|
|||
if (!this._approvalModal) throw new Error(`approvalModal unavailable (host not wired)`);
|
||||
return this._approvalModal(opts || {}, manifest.id);
|
||||
},
|
||||
// capture-tab: snapshot the currently-active tab.
|
||||
// opts.mode "visible" | "full" | "region" (required)
|
||||
// opts.format "png" | "jpeg" (default "png")
|
||||
// opts.quality 1-100 (jpeg only, default 90)
|
||||
// opts.overlaySource string (region only — DOM
|
||||
// code the add-on wants injected while the user
|
||||
// drags a selection. Must resolve to `{x,y,w,h}`
|
||||
// in CSS pixels; return null/undefined to cancel.)
|
||||
// Resolves to `{ dataUrl, width, height, host, format }`.
|
||||
captureTab: async (opts) => {
|
||||
if (!manifest.capabilities.includes("capture-tab")) {
|
||||
throw new Error(`add-on "${manifest.id}" must declare the "capture-tab" capability in addon.json`);
|
||||
}
|
||||
if (!this._captureTab) throw new Error(`captureTab unavailable (host not wired)`);
|
||||
return this._captureTab(opts || {}, manifest.id);
|
||||
},
|
||||
// capture-tab: route an in-memory image into the app's downloads pipeline
|
||||
// so it lands in the user's Downloads folder AND shows up in the
|
||||
// download-chip list the same way any HTTP download would.
|
||||
// opts.dataUrl "data:image/png;base64,…" (required)
|
||||
// opts.filename filename shown in the chip (required)
|
||||
// Resolves to `{ savePath }`.
|
||||
saveCapture: async (opts) => {
|
||||
if (!manifest.capabilities.includes("capture-tab")) {
|
||||
throw new Error(`add-on "${manifest.id}" must declare the "capture-tab" capability in addon.json`);
|
||||
}
|
||||
if (!this._saveCapture) throw new Error(`saveCapture unavailable (host not wired)`);
|
||||
return this._saveCapture(opts || {}, manifest.id);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
118
main.js
118
main.js
|
|
@ -1384,6 +1384,124 @@ function initAddons() {
|
|||
hostRequire: (name) => require(name),
|
||||
hostImport: (name) => import(require("node:url").pathToFileURL(require.resolve(name)).href),
|
||||
openTab: (url) => { if (win) createTab(url); },
|
||||
// capture-tab: three modes.
|
||||
// visible — one WebContents.capturePage() of the current viewport.
|
||||
// full — temporarily grow the tab's WebContentsView to the page's
|
||||
// scrollHeight, capture, restore. Cheap and works for most
|
||||
// pages; fixed-position headers/footers will repeat because
|
||||
// they anchor to the viewport, which is a known trade-off
|
||||
// (documented in the panel). Alternative would be a scroll-
|
||||
// and-stitch pass; kept for a later revision.
|
||||
// region — run the caller-supplied overlay source in the tab, wait
|
||||
// for a rect (or null = cancel), then capturePage(rect).
|
||||
captureTab: async (opts, addonId) => {
|
||||
const t = activeTab();
|
||||
if (!t) throw new Error("no active tab");
|
||||
const wc = t.view.webContents;
|
||||
const host = t?.prov?.host || (() => { try { return new URL(wc.getURL()).host; } catch { return ""; } })();
|
||||
const mode = String(opts?.mode || "visible");
|
||||
const format = opts?.format === "jpeg" ? "jpeg" : "png";
|
||||
const quality = Math.max(1, Math.min(100, Number(opts?.quality) || 90));
|
||||
const encode = (img) => format === "jpeg"
|
||||
? `data:image/jpeg;base64,${img.toJPEG(quality).toString("base64")}`
|
||||
: img.toDataURL();
|
||||
if (mode === "visible") {
|
||||
const img = await wc.capturePage();
|
||||
const s = img.getSize();
|
||||
console.log(`[addons] [${addonId}] captureTab visible ${s.width}x${s.height}`);
|
||||
return { dataUrl: encode(img), width: s.width, height: s.height, host, format };
|
||||
}
|
||||
if (mode === "full") {
|
||||
const dims = await wc.executeJavaScript(
|
||||
`({w: Math.max(document.documentElement.scrollWidth, document.body ? document.body.scrollWidth : 0),`
|
||||
+ ` h: Math.max(document.documentElement.scrollHeight, document.body ? document.body.scrollHeight : 0),`
|
||||
+ ` dpr: window.devicePixelRatio || 1})`, true);
|
||||
const fullW = Math.max(1, Math.min(16384, Math.floor(dims.w)));
|
||||
const fullH = Math.max(1, Math.min(32768, Math.floor(dims.h)));
|
||||
const prevBounds = t.view.getBounds();
|
||||
try {
|
||||
t.view.setBounds({ x: prevBounds.x, y: prevBounds.y, width: fullW, height: fullH });
|
||||
// Let layout+paint catch up before capture. One requestAnimationFrame
|
||||
// isn't enough for lazy-loaded content; a short delay gets most pages.
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
const img = await wc.capturePage();
|
||||
const s = img.getSize();
|
||||
console.log(`[addons] [${addonId}] captureTab full ${s.width}x${s.height} (page ${fullW}x${fullH})`);
|
||||
return { dataUrl: encode(img), width: s.width, height: s.height, host, format };
|
||||
} finally {
|
||||
try { layout(); } catch {}
|
||||
}
|
||||
}
|
||||
if (mode === "region") {
|
||||
const src = String(opts?.overlaySource || "");
|
||||
if (!src) throw new Error("region capture needs opts.overlaySource");
|
||||
// Overlay script runs in the target tab's world. It's expected to
|
||||
// resolve (as the executeJavaScript result) with {x,y,w,h} in CSS
|
||||
// pixels, or null when the user hits Escape / right-clicks.
|
||||
const rect = await wc.executeJavaScript(src, true);
|
||||
if (!rect || typeof rect !== "object") {
|
||||
console.log(`[addons] [${addonId}] captureTab region cancelled`);
|
||||
return { dataUrl: "", width: 0, height: 0, host, format, cancelled: true };
|
||||
}
|
||||
const r = {
|
||||
x: Math.max(0, Math.floor(rect.x)),
|
||||
y: Math.max(0, Math.floor(rect.y)),
|
||||
width: Math.max(1, Math.floor(rect.w)),
|
||||
height: Math.max(1, Math.floor(rect.h)),
|
||||
};
|
||||
const img = await wc.capturePage(r);
|
||||
const s = img.getSize();
|
||||
console.log(`[addons] [${addonId}] captureTab region ${s.width}x${s.height} @ ${r.x},${r.y}`);
|
||||
return { dataUrl: encode(img), width: s.width, height: s.height, host, format };
|
||||
}
|
||||
throw new Error(`unknown capture mode: ${mode}`);
|
||||
},
|
||||
// saveCapture writes the bytes to Downloads and synthesizes a completed
|
||||
// download record so the chip shows the file with a Show-in-folder link,
|
||||
// just like an HTTP save. session.downloadURL(dataUrl) would go through
|
||||
// will-download, but data URLs come across with a synthetic filename that
|
||||
// Electron won't let us override in-flight without gymnastics — writing
|
||||
// directly is deterministic and produces the same user-facing artifact.
|
||||
saveCapture: async (opts, addonId) => {
|
||||
const dataUrl = String(opts?.dataUrl || "");
|
||||
const m = /^data:([^;,]+);base64,(.+)$/.exec(dataUrl);
|
||||
if (!m) throw new Error("saveCapture: dataUrl must be base64-encoded");
|
||||
const mime = m[1];
|
||||
const bytes = Buffer.from(m[2], "base64");
|
||||
const raw = String(opts?.filename || "screenshot.png");
|
||||
// Strip path separators — add-on-provided filename must not escape the
|
||||
// downloads folder.
|
||||
const safe = raw.replace(/[\\/:*?"<>|]+/g, "_").slice(0, 200) || "screenshot.png";
|
||||
const dlDir = app.getPath("downloads");
|
||||
let target = path.join(dlDir, safe);
|
||||
// Uniquify: append " (n)" before the extension if the name is taken.
|
||||
if (fs.existsSync(target)) {
|
||||
const ext = path.extname(safe);
|
||||
const stem = safe.slice(0, safe.length - ext.length);
|
||||
for (let i = 2; i < 10000; i++) {
|
||||
const cand = path.join(dlDir, `${stem} (${i})${ext}`);
|
||||
if (!fs.existsSync(cand)) { target = cand; break; }
|
||||
}
|
||||
}
|
||||
try { fs.writeFileSync(target, bytes); }
|
||||
catch (e) { throw new Error(`saveCapture: write failed: ${e?.message || e}`); }
|
||||
const id = nextDlId++;
|
||||
const rec = {
|
||||
id,
|
||||
filename: path.basename(target),
|
||||
url: `internal://addons/${addonId}/${path.basename(target)}`,
|
||||
mime,
|
||||
total: bytes.length,
|
||||
received: bytes.length,
|
||||
state: "completed",
|
||||
savePath: target,
|
||||
startedAt: Date.now(),
|
||||
};
|
||||
downloads.unshift(rec);
|
||||
emitDownloads();
|
||||
console.log(`[addons] [${addonId}] saveCapture wrote ${bytes.length} bytes → ${target}`);
|
||||
return { savePath: target };
|
||||
},
|
||||
});
|
||||
addonHost.discoverAndActivate();
|
||||
const snap = addonHost.snapshot();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "theseus-navigator",
|
||||
"version": "0.3.12",
|
||||
"version": "0.3.13",
|
||||
"description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.",
|
||||
"author": "Silent Mode",
|
||||
"main": "main.js",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue