From 082bcc8805da30305c5994381b6d4637b30d338b Mon Sep 17 00:00:00 2001 From: eric Date: Thu, 26 Mar 2026 05:49:54 -0500 Subject: [PATCH] 's' --- electron/package.json | 14 ++- electron/pnpm-lock.yaml | 155 ++++++++++++++++++++++++++++ electron/scripts/generate-icons.mjs | 46 +++++++++ electron/src/main/index.ts | 15 ++- 4 files changed, 224 insertions(+), 6 deletions(-) create mode 100644 electron/scripts/generate-icons.mjs diff --git a/electron/package.json b/electron/package.json index 390872c..418a4e6 100644 --- a/electron/package.json +++ b/electron/package.json @@ -9,7 +9,8 @@ "dev": "electron-vite dev", "build": "electron-vite build", "preview": "electron-vite preview", - "dist": "pnpm run build && electron-builder --win --publish never" + "gen:icons": "node ./scripts/generate-icons.mjs", + "dist": "pnpm run build && pnpm run gen:icons && electron-builder --win --publish never" }, "dependencies": { "d3": "^7.9.0", @@ -28,9 +29,11 @@ "@types/react-dom": "^18.3.1", "@types/three": "^0.183.1", "@vitejs/plugin-react": "^4.3.4", + "@resvg/resvg-js": "^2.6.2", "electron": "^33.2.0", "electron-builder": "^25.1.8", "electron-vite": "^2.3.0", + "png-to-ico": "^2.1.0", "typescript": "~5.6.2", "vite": "^5.4.11" }, @@ -38,7 +41,8 @@ "onlyBuiltDependencies": [ "electron", "electron-builder", - "esbuild" + "esbuild", + "@resvg/resvg-js" ] }, "build": { @@ -50,7 +54,8 @@ }, "files": [ "out/**/*", - "package.json" + "package.json", + "build/**/*" ], "extraResources": [ { @@ -68,9 +73,10 @@ } ], "win": { + "icon": "build/icon.ico", "target": [ { - "target": "nsis", + "target": "portable", "arch": [ "x64" ] diff --git a/electron/pnpm-lock.yaml b/electron/pnpm-lock.yaml index 03de5ae..85c7dd5 100644 --- a/electron/pnpm-lock.yaml +++ b/electron/pnpm-lock.yaml @@ -30,6 +30,9 @@ importers: specifier: ^0.183.2 version: 0.183.2 devDependencies: + '@resvg/resvg-js': + specifier: ^2.6.2 + version: 2.6.2 '@types/d3': specifier: ^7.4.3 version: 7.4.3 @@ -60,6 +63,9 @@ importers: electron-vite: specifier: ^2.3.0 version: 2.3.0(vite@5.4.21(@types/node@22.19.15)) + png-to-ico: + specifier: ^2.1.0 + version: 2.1.8 typescript: specifier: ~5.6.2 version: 5.6.3 @@ -377,6 +383,82 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@resvg/resvg-js-android-arm-eabi@2.6.2': + resolution: {integrity: sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + + '@resvg/resvg-js-android-arm64@2.6.2': + resolution: {integrity: sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@resvg/resvg-js-darwin-arm64@2.6.2': + resolution: {integrity: sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@resvg/resvg-js-darwin-x64@2.6.2': + resolution: {integrity: sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@resvg/resvg-js-linux-arm-gnueabihf@2.6.2': + resolution: {integrity: sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@resvg/resvg-js-linux-arm64-gnu@2.6.2': + resolution: {integrity: sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@resvg/resvg-js-linux-arm64-musl@2.6.2': + resolution: {integrity: sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@resvg/resvg-js-linux-x64-gnu@2.6.2': + resolution: {integrity: sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@resvg/resvg-js-linux-x64-musl@2.6.2': + resolution: {integrity: sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@resvg/resvg-js-win32-arm64-msvc@2.6.2': + resolution: {integrity: sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@resvg/resvg-js-win32-ia32-msvc@2.6.2': + resolution: {integrity: sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@resvg/resvg-js-win32-x64-msvc@2.6.2': + resolution: {integrity: sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@resvg/resvg-js@2.6.2': + resolution: {integrity: sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==} + engines: {node: '>= 10'} + '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} @@ -649,6 +731,9 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@17.0.45': + resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} + '@types/node@20.19.37': resolution: {integrity: sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==} @@ -1869,10 +1954,19 @@ packages: resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} engines: {node: '>=10.4.0'} + png-to-ico@2.1.8: + resolution: {integrity: sha512-Nf+IIn/cZ/DIZVdGveJp86NG5uNib1ZXMiDd/8x32HCTeKSvgpyg6D/6tUBn1QO/zybzoMK0/mc3QRgAyXdv9w==} + engines: {node: '>=8'} + hasBin: true + pngjs@5.0.0: resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} engines: {node: '>=10.13.0'} + pngjs@6.0.0: + resolution: {integrity: sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==} + engines: {node: '>=12.13.0'} + postcss@8.5.8: resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} engines: {node: ^10 || ^12 || >=14} @@ -2618,6 +2712,57 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@resvg/resvg-js-android-arm-eabi@2.6.2': + optional: true + + '@resvg/resvg-js-android-arm64@2.6.2': + optional: true + + '@resvg/resvg-js-darwin-arm64@2.6.2': + optional: true + + '@resvg/resvg-js-darwin-x64@2.6.2': + optional: true + + '@resvg/resvg-js-linux-arm-gnueabihf@2.6.2': + optional: true + + '@resvg/resvg-js-linux-arm64-gnu@2.6.2': + optional: true + + '@resvg/resvg-js-linux-arm64-musl@2.6.2': + optional: true + + '@resvg/resvg-js-linux-x64-gnu@2.6.2': + optional: true + + '@resvg/resvg-js-linux-x64-musl@2.6.2': + optional: true + + '@resvg/resvg-js-win32-arm64-msvc@2.6.2': + optional: true + + '@resvg/resvg-js-win32-ia32-msvc@2.6.2': + optional: true + + '@resvg/resvg-js-win32-x64-msvc@2.6.2': + optional: true + + '@resvg/resvg-js@2.6.2': + optionalDependencies: + '@resvg/resvg-js-android-arm-eabi': 2.6.2 + '@resvg/resvg-js-android-arm64': 2.6.2 + '@resvg/resvg-js-darwin-arm64': 2.6.2 + '@resvg/resvg-js-darwin-x64': 2.6.2 + '@resvg/resvg-js-linux-arm-gnueabihf': 2.6.2 + '@resvg/resvg-js-linux-arm64-gnu': 2.6.2 + '@resvg/resvg-js-linux-arm64-musl': 2.6.2 + '@resvg/resvg-js-linux-x64-gnu': 2.6.2 + '@resvg/resvg-js-linux-x64-musl': 2.6.2 + '@resvg/resvg-js-win32-arm64-msvc': 2.6.2 + '@resvg/resvg-js-win32-ia32-msvc': 2.6.2 + '@resvg/resvg-js-win32-x64-msvc': 2.6.2 + '@rolldown/pluginutils@1.0.0-beta.27': {} '@rollup/rollup-android-arm-eabi@4.60.0': @@ -2870,6 +3015,8 @@ snapshots: '@types/ms@2.1.0': {} + '@types/node@17.0.45': {} + '@types/node@20.19.37': dependencies: undici-types: 6.21.0 @@ -4300,8 +4447,16 @@ snapshots: base64-js: 1.5.1 xmlbuilder: 15.1.1 + png-to-ico@2.1.8: + dependencies: + '@types/node': 17.0.45 + minimist: 1.2.8 + pngjs: 6.0.0 + pngjs@5.0.0: {} + pngjs@6.0.0: {} + postcss@8.5.8: dependencies: nanoid: 3.3.11 diff --git a/electron/scripts/generate-icons.mjs b/electron/scripts/generate-icons.mjs new file mode 100644 index 0000000..283872c --- /dev/null +++ b/electron/scripts/generate-icons.mjs @@ -0,0 +1,46 @@ +import fs from 'fs' +import path from 'path' +import { fileURLToPath } from 'url' +import pngToIco from 'png-to-ico' +import { Resvg } from '@resvg/resvg-js' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +const root = path.resolve(__dirname, '..') +const buildDir = path.join(root, 'build') +const srcSvg = path.join(buildDir, 'app-icon.svg') +const outPng = path.join(buildDir, 'icon.png') +const outIco = path.join(buildDir, 'icon.ico') + +if (!fs.existsSync(srcSvg)) { + console.error(`[gen:icons] missing ${srcSvg}`) + process.exit(1) +} + +fs.mkdirSync(buildDir, { recursive: true }) + +const svgBuf = fs.readFileSync(srcSvg) +const svg = svgBuf.toString('utf8') + +function renderSvgToPngBuffer(svgInputUtf8, size) { + const resvg = new Resvg(svgInputUtf8, { + fitTo: { mode: 'width', value: size }, + background: 'rgba(0,0,0,0)', + }) + const img = resvg.render() + return img.asPng() +} + +// 生成一张 256x256 的 PNG(也可供托盘/文档等使用) +fs.writeFileSync(outPng, renderSvgToPngBuffer(svg, 256)) + +// Windows .ico:包含多尺寸位图更稳(任务栏/快捷方式/高 DPI) +const sizes = [16, 24, 32, 48, 64, 128, 256] +const pngBuffers = sizes.map((s) => renderSvgToPngBuffer(svg, s)) + +const ico = await pngToIco(pngBuffers) +fs.writeFileSync(outIco, ico) + +console.log(`[gen:icons] wrote ${path.relative(root, outIco)} and ${path.relative(root, outPng)}`) + diff --git a/electron/src/main/index.ts b/electron/src/main/index.ts index 9e16176..3f7558e 100644 --- a/electron/src/main/index.ts +++ b/electron/src/main/index.ts @@ -51,6 +51,15 @@ let pendingNav: string | null = null let tray: Tray | null = null let isQuitting = false +function resolveAppIconPath(): string | null { + // 构建后 app.asar 内也会包含 build/icon.ico(见 package.json build.files) + const devCandidate = path.resolve(__dirname, '../../build/icon.ico') + const packagedCandidate = path.join(app.getAppPath(), 'build', 'icon.ico') + + const pick = (p: string): string | null => (p && fs.existsSync(p) ? p : null) + return pick(packagedCandidate) ?? pick(devCandidate) +} + function showMainWindow(): void { if (!mainWindow || mainWindow.isDestroyed()) return if (!mainWindow.isVisible()) mainWindow.show() @@ -60,8 +69,8 @@ function showMainWindow(): void { function ensureTray(): void { if (tray) return - // 项目内未找到托盘图标资源:先用空图标兜底(Windows 会显示默认占位/可点击区域) - const img = nativeImage.createEmpty() + const p = resolveAppIconPath() + const img = p ? nativeImage.createFromPath(p) : nativeImage.createEmpty() tray = new Tray(img) tray.setToolTip('无人直播助手') tray.on('click', () => showMainWindow()) @@ -389,6 +398,7 @@ function resolvePreload(): string { } async function createWindow(): Promise { + const iconPath = resolveAppIconPath() ?? undefined mainWindow = new BrowserWindow({ width: 1020, height: 720, @@ -398,6 +408,7 @@ async function createWindow(): Promise { backgroundColor: '#14161c', title: '无人直播助手', autoHideMenuBar: true, + ...(iconPath ? { icon: iconPath } : {}), webPreferences: { preload: resolvePreload(), contextIsolation: true,