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.
142 lines
6.2 KiB
JavaScript
142 lines
6.2 KiB
JavaScript
// One-shot script: render site-theseus-x/assets/favicon.svg to the icon
|
||
// files electron-builder wants — a 512x512 PNG (main icon) and a multi-
|
||
// resolution .ico (Windows taskbar / installer / shortcut icon).
|
||
//
|
||
// Not in the runtime app bundle; run manually when the brand changes:
|
||
// node nsis/make-icons.mjs
|
||
// Outputs build/icon.png and build/icon.ico (both gitignored regenerated).
|
||
import fs from "node:fs";
|
||
import path from "node:path";
|
||
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.
|
||
const SVG = path.join(__dirname, "..", "..", "site-theseus-x", "assets", "favicon.svg");
|
||
// 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");
|
||
const ICO_SIZES = [16, 24, 32, 48, 64, 128, 256];
|
||
|
||
if (!fs.existsSync(SVG)) throw new Error("SVG not found at " + SVG);
|
||
console.log("source:", SVG);
|
||
|
||
// Main 512x512 PNG for macOS / linux / electron-builder auto-derived Windows.
|
||
await sharp(SVG).resize(512, 512).png().toFile(PNG_512);
|
||
console.log("wrote", PNG_512);
|
||
|
||
// Write individual sized PNGs to a temp dir, then feed their paths to
|
||
// png-to-ico so it preserves each as a distinct image inside the .ico.
|
||
fs.mkdirSync(TMP_DIR, { recursive: true });
|
||
const paths = [];
|
||
for (const size of ICO_SIZES) {
|
||
const p = path.join(TMP_DIR, `icon-${size}.png`);
|
||
await sharp(SVG).resize(size, size).png().toFile(p);
|
||
paths.push(p);
|
||
}
|
||
const ico = await pngToIco(paths);
|
||
fs.writeFileSync(ICO, ico);
|
||
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);
|