Files
geo/apps/desktop-client/src/main/app-issue-indicator.ts
T

125 lines
3.6 KiB
TypeScript
Raw Normal View History

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?.()
}