fix(theseus/screenshot): 0.2.4 — deliver capture via addon storage, not a cross-origin file://

Blank editor + broken buttons root cause: index.js was writing the
capture to <userData>/addons-data/screenshot-scratch/<name>.png and
passing "?src=file://<that path>" to editor.html. The editor lives at
file:///<userData>/addons/screenshot/editor.html — different directory
tree under file://. Chromium's file:// origin policy treats those as
different origins and quietly refuses the <img> load, so init()'s
loadImage() rejects, the canvas never gets an image, and every tool
after that operates on a still-empty 300×150 default canvas — the
tools appear to work but produce no visible output because the base
image never landed. The sidebar version we replaced set
`previewImg.src = dataUrl` (a base64 data URL) directly, which has no
origin and just worked; the tab version regressed by adding the file
hop.

Fix keeps the scratch file for the recent-captures ring but hands
the raw capture through the add-on's per-add-on kv store
(`__pending` key). Same store, same origin scoping, no
cross-directory read: index.js writes via api.storage.set from main;
editor.js reads via window.silentmode.storage.get through the tab
preload (packaged since 0.3.27). Fallback path retained for
"openRecent" callers still passing ?src=… — those will need their
own fix in a follow-up.

Bumped to 0.2.4 and signed for the OTA endpoint — first real
independent add-on ship: no Theseus release needed to fix this,
0.3.27 installs pick up 0.2.4 via the boot-time signed-update poll.
This commit is contained in:
Local Dev 2026-09-08 12:57:46 +02:00
parent 1d0e25c4e0
commit 2e42783dfe
3 changed files with 47 additions and 9 deletions

View file

@ -1,7 +1,7 @@
{
"id": "screenshot",
"name": "Screenshot",
"version": "0.2.3",
"version": "0.2.4",
"description": "Capture the current tab — visible viewport, entire scrollable page, or a rectangle you draw. Pick a mode from a toolbar dropdown; the capture opens in a full-tab editor (crop, annotate, redact, save).",
"author": "Silent Mode",
"icon": "📸",

View file

@ -112,10 +112,38 @@ function loadImage(url) {
}
async function init() {
if (!srcUrl) { toast("Missing ?src=", true); return; }
// Preferred source: a pending capture handed to us through addon storage
// (the toolbar-menu capture path). A file:// src won't load — Chromium
// treats the editor at file:///…/addons/screenshot/editor.html as a
// different origin from the scratch PNG at file:///…/addons-data/…,
// so <img> quietly errors. Storage-based delivery sidesteps the origin
// entirely.
let dataUrl = null;
let toolFromPending = "";
let pendingName = baseName;
try {
if (window.silentmode?.storage) {
const pending = await window.silentmode.storage.get("__pending", null);
if (pending && pending.dataUrl) {
dataUrl = pending.dataUrl;
toolFromPending = pending.tool || "";
if (pending.name) { pendingName = pending.name; nameEl.textContent = pending.name; document.title = pending.name + " — editor"; }
// Clear so a later editor open (e.g. "recent captures") doesn't
// accidentally reload the same capture.
await window.silentmode.storage.set("__pending", null);
}
}
} catch (e) { console.warn("editor: storage read failed:", e); }
// Fallback path — retained so the "openRecent" flow (which currently
// still passes ?src=file://) keeps working after we teach it to use
// storage too.
if (!dataUrl) {
if (!srcUrl) { toast("No capture to edit", true); return; }
dataUrl = srcUrl;
}
let img;
try { img = await loadImage(srcUrl); }
catch (e) { toast(e.message, true); return; }
try { img = await loadImage(dataUrl); }
catch (e) { toast("Couldn't load capture: " + e.message, true); return; }
sizeCanvases(img.naturalWidth, img.naturalHeight);
cctx.drawImage(img, 0, 0);
undo = [];
@ -123,7 +151,8 @@ async function init() {
pushSnapshot(); // baseline so the very first edit is undoable
updateUndoButtons();
fitBoard();
if (initialTool) setTool(initialTool);
const t = initialTool || toolFromPending;
if (t) setTool(t);
}
// Scale the stage to fit within the board when the image is bigger than

View file

@ -83,11 +83,20 @@ module.exports = {
recent.unshift({ id: name, name, path: written.path, url: fileUrl(written.path), bytes: written.bytes, at: Date.now(), mode: id });
recent = pruneRecent(recent);
api.storage.set("recent", recent);
// Hand the raw data URL to the editor tab through addon storage. We
// used to pass `?src=file://…` and let the editor load the scratch
// file, but Chromium's file:// origin policy blocks <img> from
// cross-directory file loads: editor.html lives under
// <userData>/addons/screenshot/ and scratch PNGs land under
// <userData>/addons-data/screenshot-scratch/, which is a sibling of
// the addon dir, and the load quietly fails — the editor initializes
// but the image never draws, so every subsequent tool (draw, save,
// discard) operates on a still-empty canvas. Storage isn't
// origin-scoped, so the editor can pull the data URL out and paint
// it locally.
api.storage.set("__pending", { name, dataUrl: cap.dataUrl, tool: id === "region" ? "crop" : "", at: Date.now() });
api.log(`captured ${id}${name} (${written.bytes} bytes)`);
// Region mode = land in the crop tool immediately so the user can trim
// further if the freehand rect was rough.
const tool = id === "region" ? "crop" : "";
api.openTab("editor.html", { query: { src: fileUrl(written.path), name, tool } });
api.openTab("editor.html", { query: { name } });
return { ok: true, name };
});