Ship Theseus 0.2.2 972f6209 (Extensions rename + draggable sidebar + session-proxy)
Setup 972f6209639122f32f032d5f2f9fc5a4808e0d4a810f88ae38ec9a1275ac52ac
Portable a9771f054ec36b9aff6ddee958cecf7e84cdacd4d7932aa8385c445aab4d29be
User-visible rename: the Settings tab and its labels say "Extensions"
now instead of "Add-ons". Internal identifiers (disabledAddons, the
addons/ folder, IPC channels, capability strings) stay put — code
churn wasn't worth it, and users only see the user-facing text.
Draggable sidebar. sidebar-preload.js now injects a 5px grip strip
along the LEFT edge of every panel document. mousedown+mousemove
streams delta-x px to main via sidebar-drag IPC; main clamps to
[200, 800] and debounces a save to settings.sidebarWidth. Width is
restored on next launch. The default is still 340. Faint acid-green
highlight on hover so the affordance is discoverable.
New extension capability: session-proxy. An extension whose addon.json
declares "session-proxy" gets api.setSessionProxy(rules) which routes
to session.defaultSession.setProxy — the same primitive Tor already
uses under the hood. Rules can be a string ("socks5://host:port") or
an object matching Electron's setProxy shape; null clears. The
capability is opt-in: an extension without the declaration gets an
error if it tries to call setSessionProxy. This is the framework
surface a private 3-VPS relay extension would build on (extension
folder stays on the operator's disk only; nothing about it appears in
the public build).
Deployed: scp installers + manifest + tools/ + releases/ pages to
VPS, sia-upload of both trees, verified HEAD 200 and manifest 0.2.2.
This commit is contained in:
parent
1a6b631d98
commit
ee0548fec9
6 changed files with 117 additions and 15 deletions
|
|
@ -22,7 +22,7 @@ const path = require("node:path");
|
||||||
// teaching main.js and (typically) the chrome renderer about the new point.
|
// teaching main.js and (typically) the chrome renderer about the new point.
|
||||||
// Right now only sidebar panels are wired — future rev adds toolbar-chip,
|
// Right now only sidebar panels are wired — future rev adds toolbar-chip,
|
||||||
// proxy, page-inject, etc.
|
// proxy, page-inject, etc.
|
||||||
const KNOWN_CAPABILITIES = new Set(["sidebar-panel"]);
|
const KNOWN_CAPABILITIES = new Set(["sidebar-panel", "session-proxy"]);
|
||||||
|
|
||||||
// Manifest field guardrails. Reject anything shape-suspicious so a bad
|
// Manifest field guardrails. Reject anything shape-suspicious so a bad
|
||||||
// addon.json can't get past the loader gate.
|
// addon.json can't get past the loader gate.
|
||||||
|
|
@ -53,13 +53,19 @@ function validateManifest(raw, folderName) {
|
||||||
// Loader singleton. `discoverAndActivate(opts)` returns a snapshot the rest
|
// Loader singleton. `discoverAndActivate(opts)` returns a snapshot the rest
|
||||||
// of the app queries via `getActive()` / `getInstalled()`.
|
// of the app queries via `getActive()` / `getInstalled()`.
|
||||||
class AddonHost {
|
class AddonHost {
|
||||||
constructor({ addonsDir, dataDir, isDisabled, logger }) {
|
constructor({ addonsDir, dataDir, isDisabled, logger, setSessionProxy }) {
|
||||||
this.addonsDir = addonsDir;
|
this.addonsDir = addonsDir;
|
||||||
this.dataDir = dataDir;
|
this.dataDir = dataDir;
|
||||||
this.isDisabled = isDisabled || (() => false);
|
this.isDisabled = isDisabled || (() => false);
|
||||||
this.log = logger || ((...a) => console.log("[addons]", ...a));
|
this.log = logger || ((...a) => console.log("[addons]", ...a));
|
||||||
this._installed = []; // [{ manifest, folder, error? }]
|
this._installed = []; // [{ manifest, folder, error? }]
|
||||||
this._active = new Map(); // id -> { manifest, folder, exports, sidebarPanels: [...] }
|
this._active = new Map(); // id -> { manifest, folder, exports, sidebarPanels: [...] }
|
||||||
|
// Session-proxy hook — injected by main so add-ons can swap the default
|
||||||
|
// session's proxy rules (e.g. a "route everything through my VPS" add-on).
|
||||||
|
// Signature: (rules: string | { proxyRules, proxyBypassRules }) => Promise<void>
|
||||||
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureDirs() {
|
ensureDirs() {
|
||||||
|
|
@ -160,6 +166,23 @@ class AddonHost {
|
||||||
active.sidebarPanels.push({ panelId, title, icon, pageFile: abs, addonId: manifest.id });
|
active.sidebarPanels.push({ panelId, title, icon, pageFile: abs, addonId: manifest.id });
|
||||||
this.log(`[${manifest.id}] registered sidebar panel: ${panelId}`);
|
this.log(`[${manifest.id}] registered sidebar panel: ${panelId}`);
|
||||||
},
|
},
|
||||||
|
// Swap the default session's proxy. Rules follow Chromium's proxy
|
||||||
|
// format ("socks5://1.2.3.4:1080" for a single SOCKS server,
|
||||||
|
// "http=1.2.3.4:8080;https=5.6.7.8:8080" for scheme-split HTTP, etc.).
|
||||||
|
// Pass null to clear. Add-ons that opt into this capability are
|
||||||
|
// fully replacing the browser's outgoing network path — they're
|
||||||
|
// the trust boundary while active. Same-signature as the built-in
|
||||||
|
// Tor toggle uses under the hood.
|
||||||
|
setSessionProxy: async (rules) => {
|
||||||
|
if (!this._setSessionProxy) {
|
||||||
|
this.log(`[${manifest.id}] setSessionProxy unavailable (host not wired)`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!manifest.capabilities.includes("session-proxy")) {
|
||||||
|
throw new Error(`add-on "${manifest.id}" must declare the "session-proxy" capability in addon.json`);
|
||||||
|
}
|
||||||
|
await this._setSessionProxy(rules, manifest.id);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,7 @@
|
||||||
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 2 V10 M4.5 6.5 L8 10 L11.5 6.5"/><path d="M3 12.5 L3 13.5 L13 13.5 L13 12.5"/></svg>
|
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 2 V10 M4.5 6.5 L8 10 L11.5 6.5"/><path d="M3 12.5 L3 13.5 L13 13.5 L13 12.5"/></svg>
|
||||||
<span class="dlbadge" id="dlbadge" hidden>0</span>
|
<span class="dlbadge" id="dlbadge" hidden>0</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="ic" id="sidebarbtn" title="Toggle add-on sidebar" hidden>
|
<button class="ic" id="sidebarbtn" title="Toggle extension sidebar" hidden>
|
||||||
<svg viewBox="0 0 16 16" aria-hidden="true"><rect x="2" y="3" width="12" height="10" rx="1.5"/><path d="M10 3 L10 13"/></svg>
|
<svg viewBox="0 0 16 16" aria-hidden="true"><rect x="2" y="3" width="12" height="10" rx="1.5"/><path d="M10 3 L10 13"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<span class="upchip" id="upchip" hidden><button class="upcore" id="upDownload" title=""></button><button class="updismiss" id="upDismiss" title="Not now">✕</button></span>
|
<span class="upchip" id="upchip" hidden><button class="upcore" id="upDownload" title=""></button><button class="updismiss" id="upDismiss" title="Not now">✕</button></span>
|
||||||
|
|
|
||||||
43
main.js
43
main.js
|
|
@ -217,6 +217,9 @@ const SETTINGS_DEFAULTS = {
|
||||||
// Add-on framework: ids the user has explicitly turned off. Installed but
|
// Add-on framework: ids the user has explicitly turned off. Installed but
|
||||||
// disabled add-ons are still discovered — they just never activate.
|
// disabled add-ons are still discovered — they just never activate.
|
||||||
disabledAddons: [],
|
disabledAddons: [],
|
||||||
|
// Sidebar width in px. Adjusted by dragging the grip on the panel's left
|
||||||
|
// edge; persisted across launches. Clamped to [200, 800] on load.
|
||||||
|
sidebarWidth: 340,
|
||||||
};
|
};
|
||||||
// Applying the theme via nativeTheme.themeSource makes prefers-color-scheme update
|
// Applying the theme via nativeTheme.themeSource makes prefers-color-scheme update
|
||||||
// in every renderer (chrome, settings, popover, page views) with no per-view IPC.
|
// in every renderer (chrome, settings, popover, page views) with no per-view IPC.
|
||||||
|
|
@ -230,6 +233,10 @@ const settingsFile = () => path.join(app.getPath("userData"), "settings.json");
|
||||||
function loadSettings() {
|
function loadSettings() {
|
||||||
try { if (fs.existsSync(settingsFile())) settings = { ...SETTINGS_DEFAULTS, ...JSON.parse(fs.readFileSync(settingsFile(), "utf8")) }; }
|
try { if (fs.existsSync(settingsFile())) settings = { ...SETTINGS_DEFAULTS, ...JSON.parse(fs.readFileSync(settingsFile(), "utf8")) }; }
|
||||||
catch (e) { console.error("settings load failed:", e.message); }
|
catch (e) { console.error("settings load failed:", e.message); }
|
||||||
|
// Restore the persisted sidebar width so the first-open of a session
|
||||||
|
// uses whatever the user left it at last time.
|
||||||
|
const w = Number(settings.sidebarWidth) || SIDEBAR_W_DEFAULT;
|
||||||
|
sidebarW = Math.max(SIDEBAR_W_MIN, Math.min(SIDEBAR_W_MAX, w));
|
||||||
}
|
}
|
||||||
function saveSettings() {
|
function saveSettings() {
|
||||||
try { fs.writeFileSync(settingsFile(), JSON.stringify(settings, null, 2)); } catch (e) { console.error("settings save failed:", e.message); }
|
try { fs.writeFileSync(settingsFile(), JSON.stringify(settings, null, 2)); } catch (e) { console.error("settings save failed:", e.message); }
|
||||||
|
|
@ -1141,7 +1148,11 @@ let linkStatusW = 100, linkStatusH = 22;
|
||||||
// nothing until the user actively opens it, so the perf cost of an unused
|
// nothing until the user actively opens it, so the perf cost of an unused
|
||||||
// add-on is nil.
|
// add-on is nil.
|
||||||
let sidebar, sidebarVisible = false, sidebarActivePanelId = null;
|
let sidebar, sidebarVisible = false, sidebarActivePanelId = null;
|
||||||
const SIDEBAR_W = 340;
|
// Sidebar width is user-adjustable via a drag grip on the panel's left edge.
|
||||||
|
// The value below is the default; settings.sidebarWidth overrides it once
|
||||||
|
// 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;
|
||||||
// The add-on host is the single point of truth for what's installed and
|
// The add-on host is the single point of truth for what's installed and
|
||||||
// active. Populated by initAddons() at app-ready time.
|
// active. Populated by initAddons() at app-ready time.
|
||||||
let addonHost = null;
|
let addonHost = null;
|
||||||
|
|
@ -1175,6 +1186,20 @@ function initAddons() {
|
||||||
dataDir: addonsDataDir(),
|
dataDir: addonsDataDir(),
|
||||||
isDisabled: (id) => Array.isArray(settings.disabledAddons) && settings.disabledAddons.includes(id),
|
isDisabled: (id) => Array.isArray(settings.disabledAddons) && settings.disabledAddons.includes(id),
|
||||||
logger: (...a) => console.log("[addons]", ...a),
|
logger: (...a) => console.log("[addons]", ...a),
|
||||||
|
// Session-proxy capability. Add-ons that declare "session-proxy" in
|
||||||
|
// their manifest can call api.setSessionProxy(rules) to swap
|
||||||
|
// Chromium's outbound network path. Same primitive Tor uses.
|
||||||
|
setSessionProxy: async (rules, addonId) => {
|
||||||
|
const ses = session.defaultSession;
|
||||||
|
if (rules == null || rules === "") {
|
||||||
|
console.log(`[addons] [${addonId}] clearing session proxy`);
|
||||||
|
try { await ses.setProxy({ proxyRules: "" }); } catch (e) { console.warn("proxy clear failed:", e?.message); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const opts = typeof rules === "string" ? { proxyRules: rules } : rules;
|
||||||
|
console.log(`[addons] [${addonId}] setting session proxy:`, opts.proxyRules || JSON.stringify(opts));
|
||||||
|
try { await ses.setProxy(opts); } catch (e) { console.warn("proxy set failed:", e?.message); }
|
||||||
|
},
|
||||||
});
|
});
|
||||||
addonHost.discoverAndActivate();
|
addonHost.discoverAndActivate();
|
||||||
const snap = addonHost.snapshot();
|
const snap = addonHost.snapshot();
|
||||||
|
|
@ -1227,7 +1252,7 @@ function layout() {
|
||||||
const bodyH = Math.max(0, height - CHROME_H);
|
const bodyH = Math.max(0, height - CHROME_H);
|
||||||
// Sidebar (when visible) claims a fixed slice on the right; the tab views
|
// Sidebar (when visible) claims a fixed slice on the right; the tab views
|
||||||
// shrink to fit alongside it. When hidden, tabs get the full width.
|
// shrink to fit alongside it. When hidden, tabs get the full width.
|
||||||
const sideW = sidebarVisible ? SIDEBAR_W : 0;
|
const sideW = sidebarVisible ? sidebarW : 0;
|
||||||
const tabW = Math.max(0, width - sideW);
|
const tabW = Math.max(0, width - sideW);
|
||||||
for (const t of tabs) t.view.setBounds({ x: 0, y: CHROME_H, width: tabW, height: bodyH });
|
for (const t of tabs) t.view.setBounds({ x: 0, y: CHROME_H, width: tabW, height: bodyH });
|
||||||
if (sidebar) sidebar.setBounds({ x: tabW, y: CHROME_H, width: sideW, height: bodyH });
|
if (sidebar) sidebar.setBounds({ x: tabW, y: CHROME_H, width: sideW, height: bodyH });
|
||||||
|
|
@ -1931,6 +1956,20 @@ ipcMain.handle("go-home", () => loadHome(activeId));
|
||||||
// is the namespaced string the loader emits (`<addonId>:<panelId>`) — no
|
// is the namespaced string the loader emits (`<addonId>:<panelId>`) — no
|
||||||
// coercion, main matches it verbatim.
|
// coercion, main matches it verbatim.
|
||||||
ipcMain.handle("sidebar-toggle", () => { toggleSidebar(); return sidebarVisible; });
|
ipcMain.handle("sidebar-toggle", () => { toggleSidebar(); return sidebarVisible; });
|
||||||
|
// Drag events stream in from sidebar-preload while the user is holding the
|
||||||
|
// grip. Delta is px per mousemove; we clamp, layout, and debounce the save.
|
||||||
|
let _sidebarSaveTimer = null;
|
||||||
|
ipcMain.handle("sidebar-drag", (_e, deltaPx) => {
|
||||||
|
const d = Number(deltaPx) || 0;
|
||||||
|
const next = Math.max(SIDEBAR_W_MIN, Math.min(SIDEBAR_W_MAX, sidebarW + d));
|
||||||
|
if (next === sidebarW) return sidebarW;
|
||||||
|
sidebarW = next;
|
||||||
|
layout();
|
||||||
|
settings.sidebarWidth = sidebarW;
|
||||||
|
clearTimeout(_sidebarSaveTimer);
|
||||||
|
_sidebarSaveTimer = setTimeout(saveSettings, 400);
|
||||||
|
return sidebarW;
|
||||||
|
});
|
||||||
ipcMain.handle("sidebar-open", (_e, panelId) => { setSidebar(true, panelId); return sidebarVisible; });
|
ipcMain.handle("sidebar-open", (_e, panelId) => { setSidebar(true, panelId); return sidebarVisible; });
|
||||||
ipcMain.handle("sidebar-close", () => { setSidebar(false); return false; });
|
ipcMain.handle("sidebar-close", () => { setSidebar(false); return false; });
|
||||||
ipcMain.handle("sidebar-state", () => ({
|
ipcMain.handle("sidebar-state", () => ({
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "theseus-navigator",
|
"name": "theseus-navigator",
|
||||||
"version": "0.2.1",
|
"version": "0.2.2",
|
||||||
"description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.",
|
"description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.",
|
||||||
"author": "Silent Mode",
|
"author": "Silent Mode",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,7 @@
|
||||||
<a data-sec="naming">Registries</a>
|
<a data-sec="naming">Registries</a>
|
||||||
<a data-sec="performance">Performance</a>
|
<a data-sec="performance">Performance</a>
|
||||||
<a data-sec="privacy">Privacy</a>
|
<a data-sec="privacy">Privacy</a>
|
||||||
<a data-sec="addons">Add-ons</a>
|
<a data-sec="addons">Extensions</a>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<!-- GENERAL -->
|
<!-- GENERAL -->
|
||||||
|
|
@ -500,19 +500,19 @@
|
||||||
|
|
||||||
<!-- ADD-ONS -->
|
<!-- ADD-ONS -->
|
||||||
<section id="addons" hidden>
|
<section id="addons" hidden>
|
||||||
<h1>Add-ons</h1>
|
<h1>Extensions</h1>
|
||||||
<p class="lede">Small modules that add capabilities to Theseus. Add-ons live as folders under
|
<p class="lede">Small modules that add capabilities to Theseus. Extensions live as folders under
|
||||||
<code style="background:transparent;border:none;padding:0" id="addonsPathHint">%APPDATA%\Theseus Navigator\addons\</code>. Drop a folder in, restart, it's live.
|
<code style="background:transparent;border:none;padding:0" id="addonsPathHint">%APPDATA%\Theseus Navigator\addons\</code>. Drop a folder in, restart, it's live.
|
||||||
Bundled reference add-ons (like the Notepad) are copied there on first run — you can edit or remove them
|
Bundled reference extensions (like the Notepad) are copied there on first run — you can edit or remove them
|
||||||
without losing anything the browser needs.</p>
|
without losing anything the browser needs.</p>
|
||||||
<div class="row" style="justify-content:flex-end;gap:8px">
|
<div class="row" style="justify-content:flex-end;gap:8px">
|
||||||
<button id="addonsReload" class="btn" type="button">Reload</button>
|
<button id="addonsReload" class="btn" type="button">Reload</button>
|
||||||
<button id="addonsOpenDir" class="btn" type="button">Open add-ons folder</button>
|
<button id="addonsOpenDir" class="btn" type="button">Open extensions folder</button>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="sub" style="border-top:0;padding-top:0;margin-top:1.5rem">Installed</h2>
|
<h2 class="sub" style="border-top:0;padding-top:0;margin-top:1.5rem">Installed</h2>
|
||||||
<div id="addonsList"><div class="d" style="color:var(--dim)">Loading…</div></div>
|
<div id="addonsList"><div class="d" style="color:var(--dim)">Loading…</div></div>
|
||||||
<div class="note">Add-ons run with full app access — treat installing one like installing an unsigned executable.
|
<div class="note">Extensions run with full app access — treat installing one like installing an unsigned executable.
|
||||||
Only load add-ons whose source you trust.</div>
|
Only load extensions whose source you trust.</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -985,7 +985,7 @@
|
||||||
function renderAddons(snap) {
|
function renderAddons(snap) {
|
||||||
const items = (snap && snap.installed) || [];
|
const items = (snap && snap.installed) || [];
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
addonsList.innerHTML = '<div class="d" style="color:var(--dim)">No add-ons installed. Drop a folder into the add-ons directory to install one.</div>';
|
addonsList.innerHTML = '<div class="d" style="color:var(--dim)">No extensions installed. Drop a folder into the extensions directory to install one.</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
addonsList.innerHTML = items.map((a) => {
|
addonsList.innerHTML = items.map((a) => {
|
||||||
|
|
@ -1009,7 +1009,7 @@
|
||||||
function escapeAttr(s) { return escapeHtml(s); }
|
function escapeAttr(s) { return escapeHtml(s); }
|
||||||
async function loadAddons() {
|
async function loadAddons() {
|
||||||
try { renderAddons(await C.listAddons()); }
|
try { renderAddons(await C.listAddons()); }
|
||||||
catch (e) { addonsList.textContent = "Failed to load add-ons: " + (e?.message || e); }
|
catch (e) { addonsList.textContent = "Failed to load extensions: " + (e?.message || e); }
|
||||||
}
|
}
|
||||||
document.getElementById("addonsReload").addEventListener("click", async () => {
|
document.getElementById("addonsReload").addEventListener("click", async () => {
|
||||||
await C.reloadAddons(); loadAddons();
|
await C.reloadAddons(); loadAddons();
|
||||||
|
|
|
||||||
|
|
@ -14,3 +14,43 @@ contextBridge.exposeInMainWorld("silentmode", {
|
||||||
// suspend expensive work when hidden.
|
// suspend expensive work when hidden.
|
||||||
onVisibility: (cb) => ipcRenderer.on("sidebar-visibility", (_e, visible) => cb(!!visible)),
|
onVisibility: (cb) => ipcRenderer.on("sidebar-visibility", (_e, visible) => cb(!!visible)),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Sidebar resize grip. Injected into every panel automatically so panel
|
||||||
|
// authors don't have to reinvent it. A thin strip along the LEFT edge
|
||||||
|
// (the boundary between the tab area and the sidebar) accepts mousedown
|
||||||
|
// and streams drag deltas to main until mouseup. Main clamps the width
|
||||||
|
// to [200, 800] and persists it in settings.sidebarWidth.
|
||||||
|
window.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const grip = document.createElement("div");
|
||||||
|
grip.setAttribute("aria-label", "Resize sidebar");
|
||||||
|
grip.style.cssText = [
|
||||||
|
"position:fixed", "left:0", "top:0", "bottom:0",
|
||||||
|
"width:5px", "cursor:col-resize", "z-index:2147483647",
|
||||||
|
"background:transparent",
|
||||||
|
].join(";");
|
||||||
|
// A faint highlight on hover so the affordance is visible.
|
||||||
|
grip.addEventListener("mouseenter", () => { grip.style.background = "rgba(214,255,61,.20)"; });
|
||||||
|
grip.addEventListener("mouseleave", () => { if (!dragging) grip.style.background = "transparent"; });
|
||||||
|
document.body.appendChild(grip);
|
||||||
|
let dragging = false;
|
||||||
|
grip.addEventListener("mousedown", (e) => {
|
||||||
|
if (e.button !== 0) return;
|
||||||
|
e.preventDefault();
|
||||||
|
dragging = true;
|
||||||
|
document.body.style.userSelect = "none";
|
||||||
|
grip.style.background = "rgba(214,255,61,.35)";
|
||||||
|
});
|
||||||
|
window.addEventListener("mousemove", (e) => {
|
||||||
|
if (!dragging) return;
|
||||||
|
// Moving cursor LEFT = grow sidebar width. movementX is negative left.
|
||||||
|
if (e.movementX !== 0) ipcRenderer.invoke("sidebar-drag", -e.movementX);
|
||||||
|
});
|
||||||
|
const stop = () => {
|
||||||
|
if (!dragging) return;
|
||||||
|
dragging = false;
|
||||||
|
document.body.style.userSelect = "";
|
||||||
|
grip.style.background = "transparent";
|
||||||
|
};
|
||||||
|
window.addEventListener("mouseup", stop);
|
||||||
|
window.addEventListener("mouseleave", stop);
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue