feat: update app icon generation for improved Windows and macOS support
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 30s
Desktop Client Build / Resolve Build Metadata (push) Successful in 24s
Desktop Client Build / Build Desktop Client (push) Successful in 23m0s

This commit is contained in:
Xu Liang
2026-05-31 23:47:09 +08:00
parent e07a87224d
commit 8b3a5c3442
3 changed files with 41 additions and 11 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 43 KiB

@@ -32,6 +32,12 @@ const OUT_TS = path.join(APP_DIR, 'src', 'main', 'brand-icons.ts')
// Brand palette — keep in sync with the renderer (#1677ff is Ant Design blue).
const BRAND_BLUE = '#1677ff'
const DANGER_RED = '#ff4d4f'
const MAC_APP_ICON_PLATE = 824
const MAC_APP_ICON_LOGO_RATIO = 0.62
const APP_PNG_PLATE = 896
const APP_PNG_LOGO_RATIO = 0.72
const WINDOWS_ICO_PLATE = 1024
const WINDOWS_ICO_LOGO_RATIO = 1
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'brand-icons-'))
const cleanup = () => fs.rmSync(tmpDir, { recursive: true, force: true })
@@ -212,19 +218,36 @@ function forceLogoColor(source, fill) {
}
/** A square app icon: logo centered on a rounded-rect (squircle-ish) bg. */
function squareIconSvg(bg, logoSource = logo, boundsSource = logoBounds) {
function squareIconSvg(bg, logoSource = logo, boundsSource = logoBounds, options = {}) {
const CANVAS = 1024
// macOS icon grid: 824x824 art area inside a 1024 canvas (100px margin).
const PLATE = 824
const PLATE = options.plate ?? MAC_APP_ICON_PLATE
const LOGO_RATIO = options.logoRatio ?? MAC_APP_ICON_LOGO_RATIO
const radius = options.radius ?? Math.round(PLATE * 0.225)
const MARGIN = (CANVAS - PLATE) / 2
const target = PLATE * 0.62
const target = PLATE * LOGO_RATIO
const { scale, tx, ty } = fitLogo(boundsSource, target, target, MARGIN + PLATE / 2, MARGIN + PLATE / 2)
return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${CANVAS}" height="${CANVAS}" viewBox="0 0 ${CANVAS} ${CANVAS}">
<rect x="${MARGIN}" y="${MARGIN}" width="${PLATE}" height="${PLATE}" rx="185" ry="185" fill="${bg}"/>
<rect x="${MARGIN}" y="${MARGIN}" width="${PLATE}" height="${PLATE}" rx="${radius}" ry="${radius}" fill="${bg}"/>
<g transform="translate(${tx.toFixed(2)},${ty.toFixed(2)}) scale(${scale.toFixed(6)})">${logoSource.inner}</g>
</svg>`
}
function windowsIcoIconSvg(bg, logoSource = windowsTrayLogo, boundsSource = windowsTrayLogoBounds) {
// Windows .ico taskbar layers are tiny in practice; this denser composition
// keeps the foreground mark legible at 24-32px without changing icon.png.
return squareIconSvg(bg, logoSource, boundsSource, {
plate: WINDOWS_ICO_PLATE,
logoRatio: WINDOWS_ICO_LOGO_RATIO,
})
}
function appPngIconSvg(bg, logoSource = windowsTrayLogo, boundsSource = windowsTrayLogoBounds) {
return squareIconSvg(bg, logoSource, boundsSource, {
plate: APP_PNG_PLATE,
logoRatio: APP_PNG_LOGO_RATIO,
})
}
/**
* Tray glyph: the logo centered (with padding) inside a 32px square canvas.
* tray.ts loads it with scaleFactor 2, so it shows at ~16pt on retina menu
@@ -284,12 +307,19 @@ function pngsToIco(pngBuffers) {
}
function buildAppIcons() {
// 1024 master for the normal app icon.
const masterPng = svgToPng(squareIconSvg('#ffffff', windowsTrayLogo, windowsTrayLogoBounds), 'app-normal', ['-w', '1024', '-h', '1024'])
fs.mkdirSync(BUILD_DIR, { recursive: true })
fs.copyFileSync(masterPng, path.join(BUILD_DIR, 'icon.png'))
// 1024 master for the normal app icon.
const appPng = svgToPng(appPngIconSvg('#ffffff'), 'app-normal', ['-w', '1024', '-h', '1024'])
fs.copyFileSync(appPng, path.join(BUILD_DIR, 'icon.png'))
// .icns via an .iconset downscaled with sips.
const macMasterPng = svgToPng(squareIconSvg('#ffffff', windowsTrayLogo, windowsTrayLogoBounds), 'app-normal-mac', [
'-w',
'1024',
'-h',
'1024',
])
const iconset = path.join(tmpDir, 'icon.iconset')
fs.mkdirSync(iconset, { recursive: true })
const variants = [
@@ -306,7 +336,7 @@ function buildAppIcons() {
]
for (const [size, file] of variants) {
const dest = path.join(iconset, file)
execFileSync('sips', ['-z', String(size), String(size), masterPng, '--out', dest], {
execFileSync('sips', ['-z', String(size), String(size), macMasterPng, '--out', dest], {
stdio: 'ignore',
})
}
@@ -315,7 +345,7 @@ function buildAppIcons() {
// .ico with every size Windows asks for (taskbar/Explorer/Alt-Tab/tiles).
const icoSizes = [16, 24, 32, 48, 64, 128, 256]
const icoPngs = icoSizes.map((s) =>
fs.readFileSync(svgToPng(squareIconSvg('#ffffff', windowsTrayLogo, windowsTrayLogoBounds), `app-ico-${s}`, ['-w', String(s), '-h', String(s)])),
fs.readFileSync(svgToPng(windowsIcoIconSvg('#ffffff'), `app-ico-${s}`, ['-w', String(s), '-h', String(s)])),
)
fs.writeFileSync(path.join(BUILD_DIR, 'icon.ico'), pngsToIco(icoPngs))
}
@@ -356,7 +386,7 @@ function buildMainProcessIcons() {
'// Tray icon shown when accounts have health issues (rendered @2x, not a template).',
`export const TRAY_DANGER_ICON_BASE64 =\n '${trayDanger}'`,
'',
'// Dock / window icon — logo on the brand-blue plate.',
'// Dock / window icon — logo on the white plate, tuned for Windows taskbar visibility.',
`export const APP_ICON_NORMAL_BASE64 =\n '${appNormal}'`,
'',
'// Dock / window icon — logo on a danger plate when issues exist.',