Ship Theseus 0.3.2 09331b2f (tab context menu + branded installer)
Setup 09331b2fd9ccf136e2183b7cd85354cfd56e2ed50260b7aadeed63c7ea450251
Portable 21752d0fc85fb39ec1e65192920461e9ae395a22d9a68abd27f12e638d0fdd07
Right-click a tab: floating context menu with Reload, Duplicate, Group
(submenu: None / Red / Orange / Yellow / Green / Cyan / Blue / Purple),
Add to Bookmarks, Mute (also Unmute; 🔇 shows next to the title when
muted), Close. Menus close on outside click or Escape.
Group state is per-tab. A grouped tab shows a colored dot before the
title and a matching 2-px accent stripe on the top edge, so a cluster
of same-group tabs reads visually. Palette is drawn from existing
provenance colors (err/warn/acid/srv/sia/blue).
Backend IPCs are all tab-scoped (not "active tab"): tab-reload,
tab-duplicate, tab-mute (toggle or explicit boolean), tab-group,
tab-bookmark. emitTabs payload gains muted, group, and url so the
menu can read current state.
Installer wizard branding: 164×314 sidebar BMP with the compass mark
centered + "Theseus / NAVIGATOR" wordmark under it, plus a 150×57
top-strip header with a mini compass on the right. Sharp can't write
BMP directly (only png/webp/etc), so nsis/make-icons.mjs renders raw
RGB via sharp and wraps it in a hand-rolled 24-bit uncompressed BMP
header. Uninstaller reuses the same sidebar.
Silent-install fix: nsis/installer.nsh's AriadnePageCreate now checks
IfSilent BEFORE touching nsDialogs::Create. In /S mode the flag is
zeroed and the function returns cleanly, so the installer no longer
hangs waiting for a page it will never draw. This is why 0.3.2 needed
two builds — the first hung on /S install; the fixed hash is the one
that ships.
Deployed: scp + sia-upload of both trees. Verified VPS hash matches
local 09331b2f. Fresh /S install to D:\Program Files\Theseus Navigator\
placed 0.3.2 with the correct HKCU Uninstall registry entry.
This commit is contained in:
parent
19ffde3bfa
commit
5ea4515085
6 changed files with 247 additions and 5 deletions
101
chrome.html
101
chrome.html
|
|
@ -33,6 +33,24 @@
|
|||
.tab .spin { width: 10px; height: 10px; flex: none; border: 1.5px solid #ffffff2e; border-top-color: #d6ff3d; border-radius: 50%; animation: spin .7s linear infinite; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
.tab .x { opacity: .5; cursor: pointer; padding: 0 2px; border-radius: 4px; }
|
||||
.tab .mute { font-size: 10px; opacity: .8; margin-right: 2px; }
|
||||
/* Tab group visual: a colored dot before the title, plus a matching top
|
||||
accent stripe on the tab itself so a whole group reads as one cluster
|
||||
even when tabs are next to each other. */
|
||||
.tab .gdot { width: 8px; height: 8px; flex: none; border-radius: 50%; }
|
||||
.tab.grp { box-shadow: inset 0 2px 0 var(--gc, transparent); }
|
||||
.tab.g-red, .gdot.g-red { --gc: #f6768a; } .gdot.g-red { background: #f6768a; }
|
||||
.tab.g-orange, .gdot.g-orange { --gc: #ffa96a; } .gdot.g-orange { background: #ffa96a; }
|
||||
.tab.g-yellow, .gdot.g-yellow { --gc: #ffd44f; } .gdot.g-yellow { background: #ffd44f; }
|
||||
.tab.g-green, .gdot.g-green { --gc: #4fd1a5; } .gdot.g-green { background: #4fd1a5; }
|
||||
.tab.g-cyan, .gdot.g-cyan { --gc: #4fd1e5; } .gdot.g-cyan { background: #4fd1e5; }
|
||||
.tab.g-blue, .gdot.g-blue { --gc: #4b7bec; } .gdot.g-blue { background: #4b7bec; }
|
||||
.tab.g-purple, .gdot.g-purple { --gc: #b39ddb; } .gdot.g-purple { background: #b39ddb; }
|
||||
/* Submenu popover for the tab context menu (Group → color). */
|
||||
.ctxmenu .mi.sub { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||
.ctxmenu .mi.sub::after { content: "▸"; color: var(--dim); font-size: 11px; }
|
||||
.ctxmenu.sub2 { min-width: 140px; }
|
||||
.ctxmenu .swatch { width: 12px; height: 12px; border-radius: 50%; display: inline-block; margin-right: 8px; vertical-align: -1px; border: 1px solid rgba(255,255,255,.10); }
|
||||
.tab .x:hover { opacity: 1; background: var(--line2); }
|
||||
.newtab { padding: 4px 10px; cursor: pointer; color: var(--dim); border-radius: 6px; font-size: 16px; }
|
||||
.newtab:hover { background: var(--hover); color: var(--ink); }
|
||||
|
|
@ -561,13 +579,24 @@
|
|||
const icon = t.loading
|
||||
? '<span class="spin"></span>'
|
||||
: (t.favicon ? `<img class="fav" src="${String(t.favicon).replace(/"/g,""")}" onerror="this.remove()">` : '');
|
||||
return `<div class="tab ${t.active ? "active" : ""}" data-id="${t.id}" draggable="true">${icon}<span class="t">${(t.title||"New Tab").replace(/</g,"<")}</span><span class="x" data-close="${t.id}">✕</span></div>`;
|
||||
const groupDot = t.group ? `<span class="gdot g-${t.group}" title="Group: ${t.group}"></span>` : "";
|
||||
const mute = t.muted ? `<span class="mute" title="Muted">🔇</span>` : "";
|
||||
return `<div class="tab ${t.active ? "active" : ""}${t.group ? " grp g-" + t.group : ""}" data-id="${t.id}" draggable="true">${groupDot}${icon}<span class="t">${(t.title||"New Tab").replace(/</g,"<")}</span>${mute}<span class="x" data-close="${t.id}">✕</span></div>`;
|
||||
}).join("") +
|
||||
`<span class="newtab" id="newtab">+</span>`;
|
||||
box.querySelectorAll(".tab").forEach((el) => el.onclick = (e) => {
|
||||
if (e.target.dataset.close) T.closeTab(Number(e.target.dataset.close));
|
||||
else T.switchTab(Number(el.dataset.id));
|
||||
});
|
||||
// Right-click a tab → floating menu (Reload / Duplicate / Group / Add to
|
||||
// Bookmarks / Mute / Close). Menu closes on any other click.
|
||||
box.querySelectorAll(".tab").forEach((el) => el.addEventListener("contextmenu", (e) => {
|
||||
e.preventDefault();
|
||||
const id = Number(el.dataset.id);
|
||||
const t = d.tabs.find((x) => x.id === id);
|
||||
if (!t) return;
|
||||
openTabContextMenu(e.clientX, e.clientY, t);
|
||||
}));
|
||||
// Drag-reorder — HTML5 drag events. Drop-side chosen by whether the pointer
|
||||
// is on the left or right half of the target tab (matches Chrome UX).
|
||||
let dragId = null;
|
||||
|
|
@ -603,6 +632,76 @@
|
|||
$("newtab").onclick = () => T.newTab();
|
||||
});
|
||||
|
||||
// ---- Tab context menu (right-click a tab) ----
|
||||
const TAB_GROUP_COLORS = [
|
||||
{ id: "red", label: "Red" }, { id: "orange", label: "Orange" },
|
||||
{ id: "yellow", label: "Yellow" }, { id: "green", label: "Green" },
|
||||
{ id: "cyan", label: "Cyan" }, { id: "blue", label: "Blue" },
|
||||
{ id: "purple", label: "Purple" },
|
||||
];
|
||||
function closeAllMenus() { document.querySelectorAll(".ctxmenu").forEach((m) => m.remove()); }
|
||||
function openTabContextMenu(x, y, t) {
|
||||
closeAllMenus();
|
||||
const m = document.createElement("div");
|
||||
m.className = "ctxmenu";
|
||||
m.style.left = x + "px"; m.style.top = y + "px";
|
||||
// Build items
|
||||
const item = (label, cls, fn) => {
|
||||
const el = document.createElement("div");
|
||||
el.className = "mi" + (cls ? " " + cls : "");
|
||||
el.innerHTML = label;
|
||||
el.onclick = (ev) => { ev.stopPropagation(); if (fn) fn(); closeAllMenus(); };
|
||||
return el;
|
||||
};
|
||||
m.appendChild(item("Reload", "", () => T.tabReload(t.id)));
|
||||
m.appendChild(item("Duplicate", "", () => T.tabDuplicate(t.id)));
|
||||
// Group submenu — hover/click opens a second menu next to the first.
|
||||
const gp = item("Group " + (t.group ? '<span class="swatch g-' + t.group + '"></span>' : ""), "sub", null);
|
||||
gp.onclick = (ev) => {
|
||||
ev.stopPropagation();
|
||||
const rect = gp.getBoundingClientRect();
|
||||
openGroupSubmenu(rect.right + 2, rect.top, t);
|
||||
};
|
||||
m.appendChild(gp);
|
||||
// Add to Bookmarks (disabled if the tab has no url — e.g. Home).
|
||||
const bmDisabled = !t.url;
|
||||
m.appendChild(item("Add to Bookmarks", bmDisabled ? "off" : "", bmDisabled ? null : () => T.tabBookmark(t.id)));
|
||||
m.appendChild(item(t.muted ? "Unmute" : "Mute", "", () => T.tabMute(t.id)));
|
||||
const sep = document.createElement("div"); sep.className = "sep"; m.appendChild(sep);
|
||||
m.appendChild(item("Close", "danger", () => T.closeTab(t.id)));
|
||||
document.body.appendChild(m);
|
||||
// Clamp inside viewport
|
||||
const r = m.getBoundingClientRect();
|
||||
if (r.right > window.innerWidth) m.style.left = Math.max(4, window.innerWidth - r.width - 4) + "px";
|
||||
if (r.bottom > window.innerHeight) m.style.top = Math.max(4, window.innerHeight - r.height - 4) + "px";
|
||||
}
|
||||
function openGroupSubmenu(x, y, t) {
|
||||
document.querySelectorAll(".ctxmenu.sub2").forEach((m) => m.remove());
|
||||
const m = document.createElement("div");
|
||||
m.className = "ctxmenu sub2";
|
||||
m.style.left = x + "px"; m.style.top = y + "px";
|
||||
const row = (html, fn) => {
|
||||
const el = document.createElement("div");
|
||||
el.className = "mi";
|
||||
el.innerHTML = html;
|
||||
el.onclick = (ev) => { ev.stopPropagation(); fn(); closeAllMenus(); };
|
||||
m.appendChild(el);
|
||||
};
|
||||
row('<span class="swatch" style="background:transparent;border:1px dashed var(--line2)"></span>None' + (t.group ? "" : " ✓"), () => T.tabGroup(t.id, null));
|
||||
for (const c of TAB_GROUP_COLORS) {
|
||||
row('<span class="swatch g-' + c.id + '"></span>' + c.label + (t.group === c.id ? " ✓" : ""), () => T.tabGroup(t.id, c.id));
|
||||
}
|
||||
document.body.appendChild(m);
|
||||
const r = m.getBoundingClientRect();
|
||||
if (r.right > window.innerWidth) m.style.left = Math.max(4, x - r.width - 6) + "px";
|
||||
if (r.bottom > window.innerHeight) m.style.top = Math.max(4, window.innerHeight - r.height - 4) + "px";
|
||||
}
|
||||
document.addEventListener("click", (e) => {
|
||||
// Close any open menus on outside click.
|
||||
if (!e.target.closest(".ctxmenu")) closeAllMenus();
|
||||
});
|
||||
document.addEventListener("keydown", (e) => { if (e.key === "Escape") closeAllMenus(); });
|
||||
|
||||
// ---- provenance ----
|
||||
T.onNav((d) => {
|
||||
setBadge(d); setReg(d);
|
||||
|
|
|
|||
35
main.js
35
main.js
|
|
@ -1668,7 +1668,7 @@ function emitTabs() {
|
|||
const t = activeTab();
|
||||
const wc = t?.view.webContents;
|
||||
chrome?.webContents.send("tabs", {
|
||||
tabs: tabs.map((x) => ({ id: x.id, title: x.title || "New Tab", active: x.id === activeId, loading: !!x.loading, favicon: x.favicon || null })),
|
||||
tabs: tabs.map((x) => ({ id: x.id, title: x.title || "New Tab", active: x.id === activeId, loading: !!x.loading, favicon: x.favicon || null, muted: !!x.muted, group: x.group || null, url: x.url || "" })),
|
||||
url: t?.url || "",
|
||||
loading: !!t?.loading,
|
||||
canBack: wc ? wc.navigationHistory.canGoBack() : false,
|
||||
|
|
@ -1759,7 +1759,7 @@ function createTab(initial, opts = {}) {
|
|||
try { wc.setWebRTCIPHandlingPolicy(webrtcPolicy()); } catch {}
|
||||
try { wc.setBackgroundThrottling(settings.backgroundThrottle); } catch {}
|
||||
applyFingerprint(wc);
|
||||
const tab = { id, view, title: opts.settings ? "Settings" : "New Tab", url: "", favicon: null, prov: null, settings: !!opts.settings };
|
||||
const tab = { id, view, title: opts.settings ? "Settings" : "New Tab", url: "", favicon: null, prov: null, settings: !!opts.settings, muted: false, group: null };
|
||||
tabs.push(tab);
|
||||
win.contentView.addChildView(view);
|
||||
wc.on("page-title-updated", (_e, title) => {
|
||||
|
|
@ -2108,6 +2108,37 @@ ipcMain.handle("search", (_e, q) => navigateTab(activeId, SEARCH(q)));
|
|||
ipcMain.handle("new-tab", () => createTab());
|
||||
ipcMain.handle("close-tab", (_e, id) => closeTab(id));
|
||||
ipcMain.handle("switch-tab", (_e, id) => setActive(id));
|
||||
// Tab context menu backing IPCs. All scoped to a specific tab id so the
|
||||
// active tab doesn't have to be the one the user right-clicked.
|
||||
ipcMain.handle("tab-reload", (_e, id) => { const t = tabById(id); if (t) try { t.view.webContents.reload(); } catch {} });
|
||||
ipcMain.handle("tab-duplicate", (_e, id) => {
|
||||
const t = tabById(id); if (!t) return;
|
||||
const target = t.url || "";
|
||||
if (target) createTab(target); else createTab();
|
||||
});
|
||||
ipcMain.handle("tab-mute", (_e, id, on) => {
|
||||
const t = tabById(id); if (!t) return false;
|
||||
const want = typeof on === "boolean" ? on : !t.muted;
|
||||
try { t.view.webContents.setAudioMuted(want); t.muted = want; emitTabs(); return want; }
|
||||
catch { return t.muted; }
|
||||
});
|
||||
ipcMain.handle("tab-group", (_e, id, color) => {
|
||||
const t = tabById(id); if (!t) return null;
|
||||
// color: null | "red" | "orange" | "yellow" | "green" | "cyan" | "blue" | "purple"
|
||||
const allowed = new Set(["red","orange","yellow","green","cyan","blue","purple"]);
|
||||
t.group = allowed.has(color) ? color : null;
|
||||
emitTabs();
|
||||
return t.group;
|
||||
});
|
||||
ipcMain.handle("tab-bookmark", (_e, id) => {
|
||||
const t = tabById(id); if (!t) return false;
|
||||
const url = t.url; const title = t.title || url;
|
||||
if (!url) return false;
|
||||
if (bookmarks.some((b) => b.url === url)) return true; // already saved
|
||||
bookmarks.unshift({ url, title, addedAt: Date.now() });
|
||||
saveBookmarks(); emitBookmarks();
|
||||
return true;
|
||||
});
|
||||
ipcMain.handle("move-tab", (_e, id, targetId, place) => {
|
||||
const src = tabs.findIndex((t) => t.id === id);
|
||||
const dst = tabs.findIndex((t) => t.id === targetId);
|
||||
|
|
|
|||
|
|
@ -25,6 +25,16 @@ Var InstallAriadneFlag
|
|||
Page custom AriadnePageCreate AriadnePageLeave
|
||||
|
||||
Function AriadnePageCreate
|
||||
; Silent install (/S) — no user, no page. Default to "don't touch Ariadne";
|
||||
; the whole point of /S is unattended, and existing Ariadne stays as-is.
|
||||
; nsDialogs::Create in silent mode returns a dialog handle we can't display,
|
||||
; so we short-circuit BEFORE calling any nsDialogs API.
|
||||
IfSilent ariadne_page_silent ariadne_page_check
|
||||
ariadne_page_silent:
|
||||
StrCpy $InstallAriadneFlag "0"
|
||||
Return
|
||||
|
||||
ariadne_page_check:
|
||||
; Skip the page entirely if Ariadne's Thread is already on this machine —
|
||||
; nothing to offer. Registry lookup uses HKLM 64-bit view because Ariadne's
|
||||
; Inno script sets ArchitecturesInstallIn64BitMode=x64compatible.
|
||||
|
|
|
|||
|
|
@ -11,6 +11,46 @@ import { fileURLToPath } from "node:url";
|
|||
import sharp from "sharp";
|
||||
import pngToIco from "png-to-ico";
|
||||
|
||||
// Minimal 24-bit uncompressed BMP writer. Sharp can produce RGB raw pixels
|
||||
// but not BMP output; NSIS's MUI wizard branding needs BMP.
|
||||
function writeBmp24(outPath, width, height, rgb) {
|
||||
const rowBytes = width * 3;
|
||||
const rowPad = (4 - (rowBytes % 4)) % 4;
|
||||
const stride = rowBytes + rowPad;
|
||||
const pixelBytes = stride * height;
|
||||
const fileSize = 14 + 40 + pixelBytes;
|
||||
const buf = Buffer.alloc(fileSize);
|
||||
// BITMAPFILEHEADER
|
||||
buf.write("BM", 0);
|
||||
buf.writeUInt32LE(fileSize, 2);
|
||||
buf.writeUInt32LE(0, 6);
|
||||
buf.writeUInt32LE(54, 10);
|
||||
// BITMAPINFOHEADER
|
||||
buf.writeUInt32LE(40, 14);
|
||||
buf.writeInt32LE(width, 18);
|
||||
buf.writeInt32LE(height, 22);
|
||||
buf.writeUInt16LE(1, 26);
|
||||
buf.writeUInt16LE(24, 28);
|
||||
buf.writeUInt32LE(0, 30);
|
||||
buf.writeUInt32LE(pixelBytes, 34);
|
||||
buf.writeInt32LE(2835, 38); // 72 DPI
|
||||
buf.writeInt32LE(2835, 42);
|
||||
buf.writeUInt32LE(0, 46);
|
||||
buf.writeUInt32LE(0, 50);
|
||||
// Pixel rows bottom-up, BGR order.
|
||||
for (let y = 0; y < height; y++) {
|
||||
const srcRow = (height - 1 - y) * width * 3;
|
||||
const dstRow = 54 + y * stride;
|
||||
for (let x = 0; x < width; x++) {
|
||||
buf[dstRow + x * 3] = rgb[srcRow + x * 3 + 2]; // B
|
||||
buf[dstRow + x * 3 + 1] = rgb[srcRow + x * 3 + 1]; // G
|
||||
buf[dstRow + x * 3 + 2] = rgb[srcRow + x * 3]; // R
|
||||
}
|
||||
// trailing padding bytes are already 0-filled by alloc()
|
||||
}
|
||||
fs.writeFileSync(outPath, buf);
|
||||
}
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
// SVG source lives in the theseus.x standalone site so the browser icon
|
||||
// and the site brand stay in lockstep.
|
||||
|
|
@ -18,6 +58,9 @@ const SVG = path.join(__dirname, "..", "..", "site-theseus-x", "assets", "favico
|
|||
// Icons drop into build/, which is gitignored (regenerated artifacts).
|
||||
// electron-builder picks them up from there via package.json.
|
||||
const OUT_DIR = path.join(__dirname, "..", "build");
|
||||
const INSTALLER_SIDEBAR = path.join(OUT_DIR, "installerSidebar.bmp");
|
||||
const UNINSTALLER_SIDEBAR = path.join(OUT_DIR, "uninstallerSidebar.bmp");
|
||||
const INSTALLER_HEADER = path.join(OUT_DIR, "installerHeader.bmp");
|
||||
const PNG_512 = path.join(OUT_DIR, "icon.png");
|
||||
const ICO = path.join(OUT_DIR, "icon.ico");
|
||||
const TMP_DIR = path.join(OUT_DIR, ".icon-tmp");
|
||||
|
|
@ -46,3 +89,54 @@ console.log("wrote", ICO, `(${ICO_SIZES.length} sizes)`);
|
|||
// Clean up the temp PNGs.
|
||||
for (const p of paths) fs.unlinkSync(p);
|
||||
fs.rmdirSync(TMP_DIR);
|
||||
|
||||
// ---- NSIS wizard branding ----------------------------------------------
|
||||
// MUI2's Welcome/Finish sidebar is 164×314 BMP (no alpha). Compose the
|
||||
// compass mark centered on the Silent Mode dark background so the wizard
|
||||
// stops shouting "default Electron installer" at the user.
|
||||
async function makeSidebar(outFile, W, H, markSize) {
|
||||
const bg = { r: 11, g: 14, b: 20, alpha: 1 };
|
||||
const canvas = sharp({
|
||||
create: { width: W, height: H, channels: 3, background: bg },
|
||||
});
|
||||
const markX = Math.floor((W - markSize) / 2);
|
||||
const markY = Math.floor((H - markSize) / 2) - Math.floor(H * 0.08); // slight nudge up
|
||||
const markPng = await sharp(SVG).resize(markSize, markSize).png().toBuffer();
|
||||
// "Theseus Navigator" wordmark rendered as SVG text so we don't need a font file.
|
||||
const wordmarkSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="${W - 20}" height="60">
|
||||
<text x="50%" y="24" text-anchor="middle" fill="#e7eaf1"
|
||||
font-family="Segoe UI, Arial, sans-serif" font-weight="600" font-size="18">Theseus</text>
|
||||
<text x="50%" y="48" text-anchor="middle" fill="#d6ff3d"
|
||||
font-family="Segoe UI, Arial, sans-serif" font-weight="600" font-size="14" letter-spacing="1">NAVIGATOR</text>
|
||||
</svg>`;
|
||||
const wordmarkPng = await sharp(Buffer.from(wordmarkSvg)).png().toBuffer();
|
||||
await canvas
|
||||
.composite([
|
||||
{ input: markPng, top: markY, left: markX },
|
||||
{ input: wordmarkPng, top: markY + markSize + 14, left: 10 },
|
||||
])
|
||||
.toColorspace("srgb")
|
||||
.raw({ depth: "uchar" })
|
||||
.toBuffer({ resolveWithObject: true })
|
||||
.then(({ data, info }) => writeBmp24(outFile, info.width, info.height, data));
|
||||
console.log("wrote", outFile);
|
||||
}
|
||||
// NSIS MUI2 sidebar: 164x314. Uninstaller reuses the same asset.
|
||||
await makeSidebar(INSTALLER_SIDEBAR, 164, 314, 96);
|
||||
fs.copyFileSync(INSTALLER_SIDEBAR, UNINSTALLER_SIDEBAR);
|
||||
console.log("wrote", UNINSTALLER_SIDEBAR);
|
||||
// NSIS header (top strip, 150x57) — small compass on dark, right-aligned so
|
||||
// it doesn't compete with the page title on the left.
|
||||
async function makeHeader(outFile) {
|
||||
const W = 150, H = 57;
|
||||
const bg = { r: 11, g: 14, b: 20, alpha: 1 };
|
||||
const mark = await sharp(SVG).resize(40, 40).png().toBuffer();
|
||||
await sharp({ create: { width: W, height: H, channels: 3, background: bg } })
|
||||
.composite([{ input: mark, top: 8, left: W - 48 }])
|
||||
.toColorspace("srgb")
|
||||
.raw({ depth: "uchar" })
|
||||
.toBuffer({ resolveWithObject: true })
|
||||
.then(({ data, info }) => writeBmp24(outFile, info.width, info.height, data));
|
||||
console.log("wrote", outFile);
|
||||
}
|
||||
await makeHeader(INSTALLER_HEADER);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "theseus-navigator",
|
||||
"version": "0.3.1",
|
||||
"version": "0.3.2",
|
||||
"description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.",
|
||||
"author": "Silent Mode",
|
||||
"main": "main.js",
|
||||
|
|
@ -118,7 +118,10 @@
|
|||
"include": "nsis/installer.nsh",
|
||||
"installerIcon": "build/icon.ico",
|
||||
"uninstallerIcon": "build/icon.ico",
|
||||
"installerHeaderIcon": "build/icon.ico"
|
||||
"installerHeaderIcon": "build/icon.ico",
|
||||
"installerSidebar": "build/installerSidebar.bmp",
|
||||
"uninstallerSidebar": "build/uninstallerSidebar.bmp",
|
||||
"installerHeader": "build/installerHeader.bmp"
|
||||
},
|
||||
"portable": {
|
||||
"artifactName": "TheseusNavigator-${version}-portable.${ext}"
|
||||
|
|
|
|||
|
|
@ -41,6 +41,11 @@ contextBridge.exposeInMainWorld("theseus", {
|
|||
onNav: (cb) => ipcRenderer.on("nav", (_e, d) => cb(d)),
|
||||
onTor: (cb) => ipcRenderer.on("tor", (_e, d) => cb(d)),
|
||||
onTabs: (cb) => ipcRenderer.on("tabs", (_e, d) => cb(d)),
|
||||
tabReload: (id) => ipcRenderer.invoke("tab-reload", id),
|
||||
tabDuplicate: (id) => ipcRenderer.invoke("tab-duplicate", id),
|
||||
tabMute: (id, on) => ipcRenderer.invoke("tab-mute", id, on),
|
||||
tabGroup: (id, color) => ipcRenderer.invoke("tab-group", id, color),
|
||||
tabBookmark: (id) => ipcRenderer.invoke("tab-bookmark", id),
|
||||
onAddressPicked: (cb) => ipcRenderer.on("address-picked", (_e, url) => cb(url)),
|
||||
onBcnrOffer: (cb) => ipcRenderer.on("bcnr-offer", (_e, d) => cb(d)),
|
||||
// Collision-mode (BCNR ↔ ICANN) live switcher for the active tab
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue