import { nativeImage } from 'electron/common' import type { Tray as ElectronTray } from 'electron/main' import { Menu, Tray, app } from 'electron/main' import { TRAY_DANGER_ICON_BASE64, TRAY_ICON_BASE64, TRAY_WINDOWS_ICON_BASE64 } from './brand-icons' type TrayIconImage = ReturnType // The tray PNGs are generated at 2x (see scripts/generate-brand-icons.cjs), so // they stay crisp on retina menu bars. const TRAY_ICON_SCALE_FACTOR = 2 let tray: ElectronTray | null = null let normalTrayIcon: TrayIconImage | null = null let dangerTrayIcon: TrayIconImage | null = null function createTrayIcon(tone: 'normal' | 'danger'): TrayIconImage { if (tone === 'danger') { if (dangerTrayIcon) { return dangerTrayIcon } const icon = nativeImage.createFromBuffer(Buffer.from(TRAY_DANGER_ICON_BASE64, 'base64'), { scaleFactor: TRAY_ICON_SCALE_FACTOR, }) icon.setTemplateImage(false) dangerTrayIcon = icon.isEmpty() ? createTrayIcon('normal') : icon return dangerTrayIcon } if (normalTrayIcon) { return normalTrayIcon } const buffer = Buffer.from( process.platform === 'win32' ? TRAY_WINDOWS_ICON_BASE64 : TRAY_ICON_BASE64, 'base64', ) const icon = nativeImage.createFromBuffer(buffer, { scaleFactor: TRAY_ICON_SCALE_FACTOR }) if (icon.isEmpty()) { return nativeImage.createEmpty() } icon.setTemplateImage(process.platform === 'darwin') normalTrayIcon = icon return icon } function formatIssueCount(count: number): string { return count > 99 ? '99+' : String(count) } // Windows-only: tray.displayBalloon shows a system toast next to the tray // icon. macOS and Linux ignore this call (Electron returns silently), so the // guard here is just to avoid the no-op log noise on non-Windows. export function showTrayBalloon(title: string, content: string): void { if (process.platform !== 'win32') { return } if (!tray || tray.isDestroyed()) { return } try { tray.displayBalloon({ title, content, icon: createTrayIcon('normal'), iconType: 'custom', }) } catch (error) { console.warn('[desktop-tray] displayBalloon failed', error) } } export function updateTrayIssueIndicator(issueCount: number): void { if (!tray || tray.isDestroyed()) { return } const safeCount = Math.max(0, Math.floor(issueCount)) const hasIssues = safeCount > 0 tray.setImage(createTrayIcon(hasIssues ? 'danger' : 'normal')) tray.setTitle(hasIssues ? formatIssueCount(safeCount) : '', { fontType: 'monospacedDigit' }) tray.setToolTip(hasIssues ? `省心推 · ${safeCount} 个账号健康问题` : '省心推') } export function initTray(onOpen: () => void): ElectronTray { if (tray) { return tray } tray = new Tray(createTrayIcon('normal')) updateTrayIssueIndicator(0) tray.setContextMenu( Menu.buildFromTemplate([ { label: '打开省心推', click: () => onOpen() }, { type: 'separator' }, { label: '退出省心推', click: () => app.quit() }, ]), ) tray.on('click', () => onOpen()) return tray }