diff --git a/main.js b/main.js index 0ed0b77..8eceb52 100644 --- a/main.js +++ b/main.js @@ -1610,10 +1610,41 @@ function initAddons() { } if (mode === "full") { // Page.captureScreenshot with captureBeyondViewport does the whole - // scrollable page in one shot, no setBounds gymnastics needed. - const r = await cdpCapture({ full: true }); - console.log(`[addons] [${addonId}] captureTab full ${r.width}x${r.height}`); - return { dataUrl: r.dataUrl, width: r.width, height: r.height, host, format }; + // scrollable page. Before we shoot, force the layout viewport to the + // window's full content width via Emulation.setDeviceMetricsOverride + // so an open sidebar (or any other on-screen chrome that narrowed + // the tab view) doesn't clip the capture — the shot always comes + // back at the page's natural full width, not the visible width. + const wasAttached = wc.debugger.isAttached(); + if (!wasAttached) { + try { wc.debugger.attach("1.3"); } + catch (e) { if (!/already attached/i.test(String(e?.message))) throw e; } + } + let overrode = false; + try { + const winW = (win?.getContentBounds()?.width) || 0; + const tabB = t.view.getBounds(); + const need = winW > tabB.width + 24 ? winW : 0; + if (need > 0) { + // dsf 0 = "let Chromium keep the real device scale factor". + // mobile false, deviceScaleFactor 0 keeps typography sane; + // height 0 tells CDP "use the current viewport height". + await wc.debugger.sendCommand("Emulation.setDeviceMetricsOverride", { + width: need, height: 0, deviceScaleFactor: 0, mobile: false, + }); + overrode = true; + // A frame or two so the reflow settles before we snapshot. + await new Promise((r) => setTimeout(r, 250)); + } + const r = await cdpCapture({ full: true }); + console.log(`[addons] [${addonId}] captureTab full ${r.width}x${r.height}${overrode ? ` (viewport widened to ${need}px)` : ""}`); + return { dataUrl: r.dataUrl, width: r.width, height: r.height, host, format }; + } finally { + if (overrode) { + try { await wc.debugger.sendCommand("Emulation.clearDeviceMetricsOverride"); } catch {} + } + if (!wasAttached) { try { wc.debugger.detach(); } catch {} } + } } if (mode === "region") { const src = String(opts?.overlaySource || ""); @@ -2177,6 +2208,34 @@ function loadErrorPage(t, id, { url, code, desc }) { if (id === activeId) pushNav(t.prov); emitTabs(); } +// Scrollbar theme injected into every webContents we own — tabs, chrome, +// sidebar, all the floating popovers, and every add-on panel host. Track +// picks up a subtle neutral grey (works on both dark and light surfaces +// without hardcoding either); thumb is the BCH primary #0AC18E so every +// scroll surface reads as Silent Mode's. `scrollbar-color` is the modern +// standard (Chromium ≥ 121); the ::-webkit- fallback gives us fine control +// over width, radius and hover state on older engines. `!important` on the +// track / thumb wins over per-page overrides so the branding stays visible +// even on sites that theme their own scrollbars — but we deliberately don't +// force `scrollbar-width` so a page that has hidden its scrollbars entirely +// keeps that behaviour. +const SCROLLBAR_CSS = ` + html { scrollbar-color: #0AC18E rgba(120,130,150,0.18); } + ::-webkit-scrollbar { width: 12px; height: 12px; background: rgba(120,130,150,0.18) !important; } + ::-webkit-scrollbar-track { background: rgba(120,130,150,0.18) !important; } + ::-webkit-scrollbar-thumb { background: #0AC18E !important; border-radius: 6px; + border: 2px solid transparent; background-clip: padding-box !important; } + ::-webkit-scrollbar-thumb:hover { background: #14e0a5 !important; background-clip: padding-box !important; } + ::-webkit-scrollbar-corner { background: transparent !important; } +`; +function styleScrollbars(wc) { + if (!wc) return; + const inject = () => { try { wc.insertCSS(SCROLLBAR_CSS); } catch {} }; + wc.on("dom-ready", inject); + // For a wc that's already past dom-ready when we attach (fixed views load + // fast during startup), fire once explicitly. + try { if (!wc.isLoading()) inject(); } catch {} +} function createTab(initial, opts = {}) { const id = ++tabSeq; // Non-settings tabs get home-preload so the built-in home page can round- @@ -2197,6 +2256,7 @@ function createTab(initial, opts = {}) { // mode users don't get a dark stub while a page paints. try { view.setBackgroundColor(nativeTheme.shouldUseDarkColors ? "#0b0e14" : "#ffffff"); } catch {} const wc = view.webContents; + styleScrollbars(wc); try { wc.setWebRTCIPHandlingPolicy(webrtcPolicy()); } catch {} try { wc.setBackgroundThrottling(settings.backgroundThrottle); } catch {} applyFingerprint(wc); @@ -2398,52 +2458,64 @@ function createWindow() { }); chrome = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "preload.js") } }); win.contentView.addChildView(chrome); + styleScrollbars(chrome.webContents); chrome.webContents.loadFile("chrome.html"); // Floating site-info overlay (hidden until the address-bar badge is clicked). popover = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "popover-preload.js") } }); try { popover.setBackgroundColor("#00000000"); } catch {} win.contentView.addChildView(popover); + styleScrollbars(popover.webContents); popover.webContents.loadFile("popover.html"); popover.setVisible(false); // Floating engine-picker overlay (custom dropdown with real favicons). enginePicker = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "engine-picker-preload.js") } }); try { enginePicker.setBackgroundColor("#00000000"); } catch {} win.contentView.addChildView(enginePicker); + styleScrollbars(enginePicker.webContents); enginePicker.webContents.loadFile("engine-picker.html"); enginePicker.setVisible(false); // Floating downloads panel — shows active + recent downloads. downloadsPop = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "downloads-preload.js") } }); try { downloadsPop.setBackgroundColor("#00000000"); } catch {} win.contentView.addChildView(downloadsPop); + styleScrollbars(downloadsPop.webContents); downloadsPop.webContents.loadFile("downloads.html"); downloadsPop.setVisible(false); // Floating address-bar suggestions dropdown. addressPicker = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "address-picker-preload.js") } }); try { addressPicker.setBackgroundColor("#00000000"); } catch {} win.contentView.addChildView(addressPicker); + styleScrollbars(addressPicker.webContents); addressPicker.webContents.loadFile("address-picker.html"); addressPicker.setVisible(false); // Floating password-fill picker. pwFillPop = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "pw-fill-preload.js") } }); try { pwFillPop.setBackgroundColor("#00000000"); } catch {} win.contentView.addChildView(pwFillPop); + styleScrollbars(pwFillPop.webContents); pwFillPop.webContents.loadFile("pw-fill.html"); pwFillPop.setVisible(false); // Link-hover status pill (bottom-left of window). linkStatus = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "link-status-preload.js") } }); try { linkStatus.setBackgroundColor("#00000000"); } catch {} win.contentView.addChildView(linkStatus); + styleScrollbars(linkStatus.webContents); linkStatus.webContents.loadFile("link-status.html"); linkStatus.setVisible(false); - // Add-on sidebar host. Doesn't loadFile until an add-on panel is opened. + // Add-on sidebar host. Doesn't loadFile until an add-on panel is opened — + // styleScrollbars hooks dom-ready, which fires per navigation, so every + // panel loaded into this view (Aegis, Screenshot, etc.) picks up the + // brand scrollbar the moment its DOM is ready. sidebar = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "sidebar-preload.js") } }); win.contentView.addChildView(sidebar); + styleScrollbars(sidebar.webContents); sidebar.setVisible(false); // Add-on approval overlay (approval-modal capability). Transparent view // over the tab area, loaded once, shown per request. approvalPop = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "approval-preload.js") } }); try { approvalPop.setBackgroundColor("#00000000"); } catch {} win.contentView.addChildView(approvalPop); + styleScrollbars(approvalPop.webContents); approvalPop.webContents.loadFile("approval.html"); approvalPop.setVisible(false); chrome.webContents.once("did-finish-load", () => { diff --git a/package.json b/package.json index c651163..241cbcf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "theseus-navigator", - "version": "0.3.37", + "version": "0.3.38", "description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.", "author": "Silent Mode", "main": "main.js", @@ -11,15 +11,22 @@ "dist": "electron-builder --win nsis portable" }, "dependencies": { + "@bch-wc2/interfaces": "^0.0.16", + "@bitauth/libauth": "^3.1.0-next.8", "@bitcoinerlab/secp256k1": "^1.2.0", "@noble/curves": "^2.0.1", "@noble/hashes": "^2.0.1", "@scure/bip32": "^2.0.1", + "@wizardconnect/core": "^0.2.4", + "@wizardconnect/wallet": "^0.2.3", "bip32": "^4.0.0", "bip39": "^3.1.0", "bitcoinjs-lib": "^6.1.7", "ecpair": "^2.1.0", + "eventemitter3": "^5.0.1", "fetch-socks": "^1.3.3", + "isomorphic-ws": "^5.0.0", + "lossless-json": "^4.0.2", "nostr-tools": "^2.10.4", "psl": "^1.15.0", "socks-proxy-agent": "^10.1.0",