diff --git a/bundled-addons/screenshot/addon.json b/bundled-addons/screenshot/addon.json index 247a4aa..f0089d2 100644 --- a/bundled-addons/screenshot/addon.json +++ b/bundled-addons/screenshot/addon.json @@ -1,11 +1,11 @@ { "id": "screenshot", "name": "Screenshot", - "version": "0.3.0", - "description": "Capture the current tab — visible viewport, full page, or a rectangle you draw. Preview lands in the sidebar; open in a new tab for the full editor (crop, annotate, redact, save).", + "version": "0.4.0", + "description": "Capture the current tab — visible viewport, full page, or a rectangle you draw. Preview + full editor (crop, annotate, redact, save) live inside the sidebar. Expand the sidebar to full window for a canvas-sized editor.", "author": "Silent Mode", "icon": "📸", "main": "index.js", - "capabilities": ["sidebar-panel", "capture-tab", "open-tab"], + "capabilities": ["sidebar-panel", "capture-tab"], "updateURL": "https://navigate.st/bns/theseus.x/extensions/screenshot/updates.json" } diff --git a/bundled-addons/screenshot/editor.html b/bundled-addons/screenshot/editor.html index 6b43400..bfa8732 100644 --- a/bundled-addons/screenshot/editor.html +++ b/bundled-addons/screenshot/editor.html @@ -7,6 +7,12 @@
+ + screenshot diff --git a/bundled-addons/screenshot/editor.js b/bundled-addons/screenshot/editor.js index fd48a37..021337f 100644 --- a/bundled-addons/screenshot/editor.js +++ b/bundled-addons/screenshot/editor.js @@ -588,24 +588,34 @@ $("save").addEventListener("click", save); $("copy").addEventListener("click", copy); $("discard").addEventListener("click", discard); -// Drop the working screenshot and close the editor tab. Used by the -// Discard button and the top-level Escape shortcut. window.close() on a -// tab opened via api.openTab lands as the tab's close request; Theseus's -// tab manager honors it just like a normal cross-origin window.close(). +// Back button — navigate the sidebar view back to panel.html. Same +// webContents, so it's just a location swap; no IPC needed. +const backBtn = $("back"); +if (backBtn) backBtn.addEventListener("click", () => { location.href = "panel.html"; }); + +// Maximize / restore — asks the sidebar host to widen its view to the full +// window and back. Icon reflects state via silentmode.sidebar.onMaxChange. +const maxBtn = $("toggle-max"); +if (maxBtn && window.silentmode?.sidebar) { + maxBtn.addEventListener("click", async () => { + try { await window.silentmode.sidebar.toggleMax(); } + catch (e) { console.warn("toggleMax failed:", e); } + }); + const paint = (isMax) => { + maxBtn.title = isMax ? "Restore sidebar width" : "Expand the sidebar to full window"; + maxBtn.innerHTML = isMax + ? '' + : ''; + }; + window.silentmode.sidebar.onMaxChange(paint); + window.silentmode.sidebar.isMax().then(paint).catch(() => {}); +} + +// Drop the working screenshot and go back to the sidebar panel. Used by +// the Discard button and the top-level Escape shortcut. async function discard() { - // Preferred path: ask main to close this tab. window.close() on a page - // Chromium didn't open via script is a no-op by default, and we don't - // want the editor stuck if the user has changed nothing to save. - try { - if (window.silentmode?.closeTab) { - const ok = await window.silentmode.closeTab(); - if (ok) return; - } - } catch { /* fall through */ } - // Fallbacks: window.close() (works when the tab was opened via JS), then - // a hard navigation so at minimum the stale screenshot is gone. - try { window.close(); } catch {} - location.replace("about:blank"); + try { if (window.silentmode?.storage) await window.silentmode.storage.set("__pending", null); } catch {} + location.href = "panel.html"; } init(); diff --git a/bundled-addons/screenshot/index.js b/bundled-addons/screenshot/index.js index 03805bf..70255ad 100644 --- a/bundled-addons/screenshot/index.js +++ b/bundled-addons/screenshot/index.js @@ -101,22 +101,22 @@ module.exports = { }; }); - // panel invokes "openInTab" once the user is happy with the preview. - // We rewrite __pending fresh (the editor consumes it on load) so the - // editor is always populated from the most-recent capture — even if a - // prior editor tab already drained the pending entry. - api.onMessage("openInTab", (payload) => { + // panel invokes "arm" right before it navigates itself to editor.html + // (in-sidebar navigation — SAME webContents, so the editor's storage- + // based load path Just Works). We rewrite __pending fresh here so + // whichever capture is currently previewed becomes the one the editor + // draws, even if a previous edit session already drained the entry. + api.onMessage("arm", (payload) => { const wantedName = payload && payload.name ? String(payload.name) : ""; let recent = api.storage.get("recent", []); if (!Array.isArray(recent)) recent = []; recent = pruneRecent(recent); api.storage.set("recent", recent); const hit = wantedName ? recent.find((r) => r.id === wantedName) : recent[0]; - if (!hit) throw new Error("no capture to open — take one first"); + if (!hit) throw new Error("no capture to edit — take one first"); const dataUrl = readAsDataUrl(hit.path); api.storage.set("__pending", { name: hit.name, dataUrl, at: Date.now() }); - api.openTab("editor.html", { query: { name: hit.name } }); - api.log(`openInTab → editor.html?name=${hit.name}`); + api.log(`arm → editor.html?name=${hit.name}`); return { ok: true, name: hit.name }; }); diff --git a/bundled-addons/screenshot/panel.html b/bundled-addons/screenshot/panel.html index b3bd844..7f2275c 100644 --- a/bundled-addons/screenshot/panel.html +++ b/bundled-addons/screenshot/panel.html @@ -20,10 +20,16 @@ display: flex; flex-direction: column; } header { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; border-bottom: 1px solid var(--line); - background: var(--panel); } - header .t { font-weight: 600; display: flex; gap: 8px; align-items: center; } + background: var(--panel); gap: 8px; } + header .t { font-weight: 600; display: flex; gap: 8px; align-items: center; min-width: 0; } header .t .em { font-size: 15px; } - header .m { color: var(--dim); font-size: 11.5px; min-height: 15px; } + header .m { color: var(--dim); font-size: 11.5px; min-height: 15px; flex: 1; text-align: right; } + header .max { + border: 1px solid var(--line); border-radius: 6px; background: var(--btn); + color: var(--ink); cursor: pointer; padding: 4px 6px; font: inherit; line-height: 0; + } + header .max:hover { background: var(--btn-h); } + header .max svg { width: 14px; height: 14px; display: block; } main { flex: 1; overflow-y: auto; padding: 12px 14px; display: flex; flex-direction: column; gap: 12px; } .modes { display: flex; gap: 6px; } button.mode { @@ -67,7 +73,7 @@ .status.ok { color: var(--acid); } .recent { display: none; } .recent.on { display: block; } - .recent .lbl { color: var(--dim); font-size: 11.5px; text-transform: uppercase; letter-spacing: .5px; margin: 4px 0 6px; } + .recent .lbl { color: var(--dim); font-size: 11.5px; text-transform: uppercase; letter-spacing: .5px; margin: 4px 0 6px; display: flex; justify-content: space-between; } .recent .strip { display: flex; gap: 6px; overflow-x: auto; padding-bottom: 4px; } .recent .tile { flex: 0 0 auto; width: 78px; height: 52px; border-radius: 4px; @@ -77,7 +83,7 @@ } .recent .tile img { width: 100%; height: 100%; object-fit: cover; background: #fff; } .recent .tile:hover { border-color: var(--acid); } - .recent .clear { background: none; border: 0; color: var(--dim); font: inherit; cursor: pointer; float: right; } + .recent .clear { background: none; border: 0; color: var(--dim); font: inherit; cursor: pointer; text-transform: none; letter-spacing: 0; } .recent .clear:hover { color: var(--err); } @@ -85,6 +91,11 @@
📸 Screenshot
+
@@ -111,13 +122,13 @@
- +
-
Recent +
Recent
@@ -131,8 +142,9 @@ const status = $("status"); const metaSize = $("meta-size"); const metaHost = $("meta-host"); - const btnOpenTab = $("btn-open-tab"); + const btnEdit = $("btn-edit"); const btnDiscard = $("btn-discard"); + const btnMax = $("btn-max"); const hdrStatus = $("hdr-status"); const recentBox = $("recent"); const recentStrip = $("recent-strip"); @@ -156,7 +168,7 @@ previewEmpty.hidden = false; metaSize.textContent = "—"; metaHost.textContent = ""; - btnOpenTab.disabled = true; + btnEdit.disabled = true; btnDiscard.disabled = true; return; } @@ -165,7 +177,7 @@ previewEmpty.hidden = true; metaSize.textContent = `${last.width}×${last.height} · ${fmt(last.bytes || 0)}`; metaHost.textContent = last.host || ""; - btnOpenTab.disabled = false; + btnEdit.disabled = false; btnDiscard.disabled = false; } @@ -179,24 +191,19 @@ } recentBox.classList.add("on"); recentStrip.innerHTML = list.map((r) => - `
- ${r.mode ? r.mode.charAt(0).toUpperCase() + r.mode.slice(1) : ""} -
`).join(""); + `
` + ).join(""); for (const tile of recentStrip.querySelectorAll(".tile")) { - // Lazy-load thumbnail on hover / first paint. const name = tile.dataset.name; try { const b = await window.silentmode.invoke("getBytes", { name }); - if (b && b.dataUrl) { - tile.innerHTML = ``; - } + if (b && b.dataUrl) tile.innerHTML = ``; } catch {} tile.addEventListener("click", async () => { try { const b = await window.silentmode.invoke("getBytes", { name }); if (!b) return; last = { name: b.name, dataUrl: b.dataUrl, width: 0, height: 0, host: "", bytes: b.bytes }; - // Fill in dims from the loaded image. const im = new Image(); im.onload = () => { last.width = im.naturalWidth; last.height = im.naturalHeight; renderPreview(); }; im.src = b.dataUrl; @@ -227,7 +234,7 @@ } else if (res && res.dataUrl) { last = res; renderPreview(); - setStatus("Captured. Preview above; open in editor to annotate.", "ok"); + setStatus("Captured. Click Edit to annotate.", "ok"); refreshRecent(); } else { setStatus("Nothing captured.", "err"); @@ -242,29 +249,38 @@ } } - async function doOpenTab() { + async function doEdit() { if (!last || !last.dataUrl || busy) return; busy = true; - btnOpenTab.disabled = true; - hdrStatus.textContent = "opening…"; + btnEdit.disabled = true; + hdrStatus.textContent = "opening editor…"; try { - await window.silentmode.invoke("openInTab", { name: last.name }); - setStatus("Opened in editor tab.", "ok"); + // Arm __pending with the currently-previewed capture, then navigate + // this sidebar view to editor.html — same webContents, same preload, + // so the editor keeps talking to the add-on through silentmode.*. + const armed = await window.silentmode.invoke("arm", { name: last.name }); + const name = (armed && armed.name) || last.name; + location.href = "editor.html?name=" + encodeURIComponent(name); } catch (e) { - console.warn("openInTab failed:", e); - setStatus("Open failed: " + (e && e.message || e), "err"); - } finally { - busy = false; - btnOpenTab.disabled = !last; + console.warn("arm failed:", e); + setStatus("Edit failed: " + (e && e.message || e), "err"); + btnEdit.disabled = !last; hdrStatus.textContent = ""; + busy = false; } } + async function toggleMax() { + try { await window.silentmode.sidebar.toggleMax(); } + catch (e) { console.warn("toggleMax failed:", e); } + } + for (const b of document.querySelectorAll("button.mode")) { b.addEventListener("click", () => doCapture(b.dataset.mode)); } - btnOpenTab.addEventListener("click", doOpenTab); + btnEdit.addEventListener("click", doEdit); btnDiscard.addEventListener("click", () => { last = null; renderPreview(); setStatus(""); }); + btnMax.addEventListener("click", toggleMax); clearBtn.addEventListener("click", async () => { if (!confirm("Delete every past capture?")) return; try { await window.silentmode.invoke("clearRecent", {}); await refreshRecent(); } diff --git a/main.js b/main.js index ed28e1a..0ed0b77 100644 --- a/main.js +++ b/main.js @@ -1299,6 +1299,17 @@ let sidebar, sidebarVisible = false, sidebarActivePanelId = null; // loadSettings() runs and persists any drag adjustment made by the user. const SIDEBAR_W_MIN = 200, SIDEBAR_W_MAX = 800, SIDEBAR_W_DEFAULT = 340; let sidebarW = SIDEBAR_W_DEFAULT; +// When a panel asks for "maximize" (screenshot editor wanting the full canvas +// area), we widen the sidebar to fill the window and remember the previous +// width so restore drops us back exactly. Non-persisted: closing/reopening +// Theseus always starts un-maximized. +let sidebarMaximized = false; +let sidebarPreMaxW = SIDEBAR_W_DEFAULT; +function sidebarMaxWidth() { + if (!win) return SIDEBAR_W_MAX; + const { width } = win.getContentBounds(); + return Math.max(SIDEBAR_W_MIN, width); +} // The add-on host is the single point of truth for what's installed and // active. Populated by initAddons() at app-ready time. let addonHost = null; @@ -2653,6 +2664,14 @@ ipcMain.handle("sidebar-toggle", () => { toggleSidebar(); return sidebarVisible; let _sidebarSaveTimer = null; ipcMain.handle("sidebar-drag", (_e, deltaPx) => { const d = Number(deltaPx) || 0; + // Drag-to-resize exits maximize mode — the user is asking for a specific + // width. We snap out of maximize first so the delta lands on the pre-max + // width rather than on the (huge) maximized value. + if (sidebarMaximized) { + sidebarMaximized = false; + sidebarW = Math.max(SIDEBAR_W_MIN, Math.min(SIDEBAR_W_MAX, sidebarPreMaxW || SIDEBAR_W_DEFAULT)); + try { sidebar?.webContents.send("sidebar-max-change", false); } catch {} + } const next = Math.max(SIDEBAR_W_MIN, Math.min(SIDEBAR_W_MAX, sidebarW + d)); if (next === sidebarW) return sidebarW; sidebarW = next; @@ -2664,6 +2683,29 @@ ipcMain.handle("sidebar-drag", (_e, deltaPx) => { }); ipcMain.handle("sidebar-open", (_e, panelId) => { setSidebar(true, panelId); return sidebarVisible; }); ipcMain.handle("sidebar-close", () => { setSidebar(false); return false; }); +// Panel-driven sidebar maximize: fills the window with the sidebar (tab +// area shrinks to zero-width), remembering the pre-max width so restore +// returns cleanly. Doesn't persist across sessions — a fresh launch +// always starts at the saved settings.sidebarWidth. Layout runs so tab +// views and other floating popovers reposition against the new bounds. +function setSidebarMaximized(next) { + const wanted = !!next; + if (wanted === sidebarMaximized) return sidebarMaximized; + if (wanted) { + sidebarPreMaxW = sidebarW; + sidebarW = sidebarMaxWidth(); + } else { + sidebarW = Math.max(SIDEBAR_W_MIN, Math.min(SIDEBAR_W_MAX, sidebarPreMaxW || SIDEBAR_W_DEFAULT)); + } + sidebarMaximized = wanted; + layout(); + try { sidebar?.webContents.send("sidebar-max-change", sidebarMaximized); } catch {} + return sidebarMaximized; +} +ipcMain.handle("sidebar-maximize", () => setSidebarMaximized(true)); +ipcMain.handle("sidebar-restore", () => setSidebarMaximized(false)); +ipcMain.handle("sidebar-toggle-max", () => setSidebarMaximized(!sidebarMaximized)); +ipcMain.handle("sidebar-is-max", () => sidebarMaximized); ipcMain.handle("sidebar-state", () => ({ visible: sidebarVisible, active: sidebarActivePanelId, diff --git a/sidebar-preload.js b/sidebar-preload.js index 7d2d42d..68c2ce6 100644 --- a/sidebar-preload.js +++ b/sidebar-preload.js @@ -18,6 +18,16 @@ contextBridge.exposeInMainWorld("silentmode", { invoke: (msg, payload) => ipcRenderer.invoke("addon-msg", String(msg), payload), // Events the add-on pushes via api.emit while this panel is open. on: (msg, cb) => ipcRenderer.on("addon-event", (_e, name, payload) => { if (name === msg) cb(payload); }), + // Sidebar sizing controls a panel may want (e.g. the screenshot editor + // wants a full-window canvas). Widen fills the window minus a thin strip + // for the tab area behind it; restore returns to the pre-widen width. + sidebar: { + maximize: () => ipcRenderer.invoke("sidebar-maximize"), + restore: () => ipcRenderer.invoke("sidebar-restore"), + toggleMax:() => ipcRenderer.invoke("sidebar-toggle-max"), + isMax: () => ipcRenderer.invoke("sidebar-is-max"), + onMaxChange: (cb) => ipcRenderer.on("sidebar-max-change", (_e, max) => cb(!!max)), + }, }); // Panel picker strip was here — a 32-px tab bar injected at the top of