bdad050a8b
Deployment Config CI / Deployment Config (push) Successful in 12s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 30s
Desktop Client Build / Resolve Build Metadata (push) Successful in 26s
Desktop Client Build / Build Desktop Client (push) Successful in 22m8s
125 lines
3.6 KiB
TypeScript
125 lines
3.6 KiB
TypeScript
import { nativeImage } from 'electron/common'
|
|
import type { BrowserWindow as ElectronBrowserWindow } from 'electron/main'
|
|
import { app } from 'electron/main'
|
|
|
|
import {
|
|
APP_ICON_DANGER_BASE64,
|
|
APP_ICON_NORMAL_BASE64,
|
|
WINDOWS_OVERLAY_ICON_BASE64,
|
|
} from './brand-icons'
|
|
import { createRuntimeSnapshot } from './runtime-snapshot'
|
|
import { updateTrayIssueIndicator } from './tray'
|
|
|
|
type RuntimeSnapshot = ReturnType<typeof createRuntimeSnapshot>
|
|
type AppIconTone = 'normal' | 'danger'
|
|
type AppIndicatorImage = ReturnType<typeof nativeImage.createFromBuffer>
|
|
|
|
const HEALTH_ISSUE_AUTH_STATES = new Set(['expired', 'revoked', 'challenge_required'])
|
|
const ISSUE_INDICATOR_REFRESH_MS = 15_000
|
|
|
|
let refreshTimer: ReturnType<typeof setInterval> | null = null
|
|
const appIconCache = new Map<AppIconTone, AppIndicatorImage>()
|
|
let overlayIcon: AppIndicatorImage | null = null
|
|
|
|
function compactCount(count: number): string {
|
|
return count > 99 ? '99+' : String(count)
|
|
}
|
|
|
|
function normalizeIssueCount(count: number): number {
|
|
if (!Number.isFinite(count)) {
|
|
return 0
|
|
}
|
|
return Math.max(0, Math.floor(count))
|
|
}
|
|
|
|
function iconFromBase64(base64: string): AppIndicatorImage {
|
|
const icon = nativeImage.createFromBuffer(Buffer.from(base64, 'base64'))
|
|
return icon.isEmpty() ? nativeImage.createEmpty() : icon
|
|
}
|
|
|
|
export function createDesktopHealthIcon(tone: AppIconTone): AppIndicatorImage {
|
|
const cached = appIconCache.get(tone)
|
|
if (cached) {
|
|
return cached
|
|
}
|
|
|
|
const icon = iconFromBase64(tone === 'danger' ? APP_ICON_DANGER_BASE64 : APP_ICON_NORMAL_BASE64)
|
|
appIconCache.set(tone, icon)
|
|
return icon
|
|
}
|
|
|
|
function createWindowsOverlayIcon(): AppIndicatorImage {
|
|
if (overlayIcon) {
|
|
return overlayIcon
|
|
}
|
|
overlayIcon = iconFromBase64(WINDOWS_OVERLAY_ICON_BASE64)
|
|
return overlayIcon
|
|
}
|
|
|
|
function healthIssueCount(snapshot: RuntimeSnapshot): number {
|
|
return snapshot.accounts.filter((account) => HEALTH_ISSUE_AUTH_STATES.has(account.authState))
|
|
.length
|
|
}
|
|
|
|
function applyPlatformAppIndicator(issueCount: number, window: ElectronBrowserWindow | null): void {
|
|
const safeCount = normalizeIssueCount(issueCount)
|
|
const hasIssues = safeCount > 0
|
|
const icon = createDesktopHealthIcon(hasIssues ? 'danger' : 'normal')
|
|
|
|
updateTrayIssueIndicator(safeCount)
|
|
|
|
if (process.platform === 'darwin') {
|
|
app.dock?.setIcon(icon)
|
|
app.dock?.setBadge(hasIssues ? compactCount(safeCount) : '')
|
|
app.setBadgeCount(safeCount)
|
|
return
|
|
}
|
|
|
|
if (process.platform === 'linux') {
|
|
app.setBadgeCount(safeCount)
|
|
}
|
|
|
|
if (!window || window.isDestroyed()) {
|
|
return
|
|
}
|
|
|
|
window.setIcon(icon)
|
|
|
|
if (process.platform === 'win32') {
|
|
window.setOverlayIcon(
|
|
hasIssues ? createWindowsOverlayIcon() : null,
|
|
hasIssues ? `${safeCount} 个账号健康问题` : '',
|
|
)
|
|
}
|
|
}
|
|
|
|
export function applyDesktopHealthIndicatorFromSnapshot(
|
|
snapshot: RuntimeSnapshot,
|
|
window: ElectronBrowserWindow | null,
|
|
): void {
|
|
applyPlatformAppIndicator(healthIssueCount(snapshot), window)
|
|
}
|
|
|
|
export function syncDesktopHealthIndicator(window: ElectronBrowserWindow | null): void {
|
|
try {
|
|
applyDesktopHealthIndicatorFromSnapshot(createRuntimeSnapshot(), window)
|
|
} catch (error) {
|
|
console.warn('[desktop-health-indicator] sync failed', {
|
|
message: error instanceof Error ? error.message : String(error),
|
|
})
|
|
}
|
|
}
|
|
|
|
export function startDesktopHealthIndicator(getWindow: () => ElectronBrowserWindow | null): void {
|
|
if (refreshTimer) {
|
|
clearInterval(refreshTimer)
|
|
refreshTimer = null
|
|
}
|
|
|
|
syncDesktopHealthIndicator(getWindow())
|
|
refreshTimer = setInterval(() => {
|
|
syncDesktopHealthIndicator(getWindow())
|
|
}, ISSUE_INDICATOR_REFRESH_MS)
|
|
refreshTimer.unref?.()
|
|
}
|