This commit is contained in:
eric
2026-03-26 05:49:54 -05:00
parent cbe4585ff4
commit 082bcc8805
4 changed files with 224 additions and 6 deletions

View File

@@ -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"
]

155
electron/pnpm-lock.yaml generated
View File

@@ -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

View File

@@ -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)}`)

View File

@@ -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<void> {
const iconPath = resolveAppIconPath() ?? undefined
mainWindow = new BrowserWindow({
width: 1020,
height: 720,
@@ -398,6 +408,7 @@ async function createWindow(): Promise<void> {
backgroundColor: '#14161c',
title: '无人直播助手',
autoHideMenuBar: true,
...(iconPath ? { icon: iconPath } : {}),
webPreferences: {
preload: resolvePreload(),
contextIsolation: true,