diff --git a/icons/icon.png b/icons/icon.png index acb2a81..637bdbd 100644 Binary files a/icons/icon.png and b/icons/icon.png differ diff --git a/icons/icon.svg b/icons/icon.svg new file mode 100644 index 0000000..b9193b6 --- /dev/null +++ b/icons/icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/package.json b/package.json index df46e52..5dddfe0 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "lint": "eslint \"src/**/*.{ts,tsx}\"", "typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.node.json", "inspect:electron": "node scripts/playwright-inspect-electron.cjs", + "build:icon": "electron scripts/build-icon.mjs", "format": "prettier --write \"src/**/*.{ts,tsx,css}\" \"scripts/release-notes/**/*.mjs\"", "format:check": "prettier --check \"src/**/*.{ts,tsx,css}\" \"scripts/release-notes/**/*.mjs\"", "release:patch": "npm version patch -m \"chore(release): v%s\" && git push --follow-tags", diff --git a/scripts/build-icon.mjs b/scripts/build-icon.mjs new file mode 100644 index 0000000..df354db --- /dev/null +++ b/scripts/build-icon.mjs @@ -0,0 +1,73 @@ +// Renders icons/icon.svg -> icons/icon.png (1024x1024, RGBA) using the Electron +// binary that already ships with this project — no extra native dependency and +// no network access. electron-builder generates the installer .ico from this +// PNG, and the tray reuses it (see src/main/index.ts resolveTrayIcon). +// +// Run: npm run build:icon (after editing icons/icon.svg) +import { app, BrowserWindow } from "electron"; +import { readFileSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); +const SIZE = 1024; +const svgPath = join(root, "icons", "icon.svg"); +const outPath = join(root, "icons", "icon.png"); + +function fail(msg) { + console.error("[build-icon] " + msg); + process.exit(1); +} + +app.disableHardwareAcceleration(); +const giveUp = setTimeout(() => fail("timed out waiting for a rendered frame"), 20000); + +app.whenReady().then(async () => { + const svg = readFileSync(svgPath, "utf8"); + const html = + "" + + "" + + svg; + + const win = new BrowserWindow({ + width: SIZE, + height: SIZE, + show: false, + frame: false, + transparent: true, + backgroundColor: "#00000000", + useContentSize: true, + webPreferences: { offscreen: true }, + }); + + let captured = null; + win.webContents.on("paint", (_e, _dirty, image) => { + if (image && !image.isEmpty()) captured = image; + }); + + await win.loadURL("data:text/html;charset=utf-8," + encodeURIComponent(html)); + await new Promise((r) => setTimeout(r, 800)); // let the compositor settle + + let image = captured; + if (!image || image.isEmpty()) { + try { + image = await win.webContents.capturePage(); + } catch (e) { + return fail("offscreen paint empty and capturePage failed: " + e.message); + } + } + if (!image || image.isEmpty()) return fail("renderer produced an empty image"); + + const size = image.getSize(); + if (size.width !== SIZE || size.height !== SIZE) { + image = image.resize({ width: SIZE, height: SIZE, quality: "best" }); + } + + writeFileSync(outPath, image.toPNG()); + clearTimeout(giveUp); + console.log("[build-icon] wrote " + outPath + " (" + SIZE + "x" + SIZE + ")"); + win.destroy(); + app.quit(); + process.exit(0); +});