104 lines
3.0 KiB
TypeScript
104 lines
3.0 KiB
TypeScript
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<typeof nativeImage.createFromBuffer>
|
|
|
|
// 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
|
|
}
|