feat(desktop-client): surface account health issues in tray and app icons
Periodically polls runtime snapshot for accounts in expired/revoked/challenge_required states and reflects the count via tray badge, macOS dock badge, and Windows overlay icon so users see authentication problems without opening the app. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,11 @@ import { nativeImage } from "electron/common";
|
||||
import { Menu, Tray, app } from "electron/main";
|
||||
import type { Tray as ElectronTray } from "electron/main";
|
||||
|
||||
type TrayIconImage = ReturnType<typeof nativeImage.createFromBuffer>;
|
||||
|
||||
let tray: ElectronTray | null = null;
|
||||
let normalTrayIcon: TrayIconImage | null = null;
|
||||
let dangerTrayIcon: TrayIconImage | null = null;
|
||||
|
||||
const TRAY_ICON_BASE64 =
|
||||
"iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAAnUlEQVR4nN2UwQmAMAxF360Dee8I" +
|
||||
@@ -10,23 +14,59 @@ const TRAY_ICON_BASE64 =
|
||||
"VvzQufX8EchAEmQ5s2LZjUCThwMTg8E3IymG07PSzotFqoqUHcJZ3akWSTctOYST0cT3hMOiCGte" +
|
||||
"2HcjakCIHGmiltDW+eNrU7t/dNF/txaahq+VoZ+VygAAAABJRU5ErkJggg==";
|
||||
|
||||
function createTrayIcon() {
|
||||
const TRAY_DANGER_ICON_BASE64 =
|
||||
"iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAABmJLR0QA/wD/AP+gvaeTAAACSUlEQVQ4jd2Uz04TURTGf2dmkG5spwU3mOgKXoCFGlwM0qZpDYEFvoLEPyuN6F4TTYxLWPAKaqILxKZtmMSQ6CvAygUuhNIpLpowt3NcaMt0GkASN/qtbs6533e+e++5B/41yEnJRqmUlsPDMiLTAmMACt9Q3dBz5z6MrK8fnElYJyeHmrncI1FdAjLHcAMReZEJw1fi++ZU4abnudj2W0RunHSaGOoYs5D1/SAetJJOcZw3ZxAFmMFxXqvnOccKN7PZh8BMgriLyFNVnVXVWeAZsJvYk2/Z9oN4oHcVjVIpbYXhV8CN5T9Ztj2fqVT246RWsZiLOp33wPVYODDt9qULm5s/+hyLMTcTot9leHiuK9oqFMZbhcI4QKZS2bdV54G92H53KJUqD16Fqpc43qq7ttYECPL5lUh1K1LdCvL5ZYB0vd4Q1dU+hkhP48jx7z49qqNfuk4V7vTicLfrPLLtzwnOxUHHfxk9YRXZiSdE5ApAplrdFliJ2VrOVKvbAFanczXB6WkcOY4iP1F0sVUs5gDcWu2eJTJhiUxk6/X7AAeeN6oiiwnORq9Id3FMu23aqnPper0RZx943mjHcd4BU7Fw07Tbl7vt1vel9/P5JwLPEy72RHW1+1CW6jVVvQ2M9F0DLLm12ssBxwDqeU7gOB8Z/H2noeYaU4oPo76uEN83GLMA1P5YUqSKMbeSE26g3bK+H7jGlET1MRAk8zE0BZbcMCwnJxucMuh3p6bOD6VS5ciypiWKxn4ZlB1gI2y317sP9X/gJ6o+6LM9+rGLAAAAAElFTkSuQmCC";
|
||||
|
||||
function createTrayIcon(tone: "normal" | "danger"): TrayIconImage {
|
||||
if (tone === "danger") {
|
||||
if (dangerTrayIcon) {
|
||||
return dangerTrayIcon;
|
||||
}
|
||||
|
||||
const icon = nativeImage.createFromBuffer(Buffer.from(TRAY_DANGER_ICON_BASE64, "base64"));
|
||||
icon.setTemplateImage(false);
|
||||
dangerTrayIcon = icon.isEmpty() ? createTrayIcon("normal") : icon;
|
||||
return dangerTrayIcon;
|
||||
}
|
||||
|
||||
if (normalTrayIcon) {
|
||||
return normalTrayIcon;
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(TRAY_ICON_BASE64, "base64");
|
||||
const icon = nativeImage.createFromBuffer(buffer);
|
||||
if (icon.isEmpty()) {
|
||||
return nativeImage.createEmpty();
|
||||
}
|
||||
icon.setTemplateImage(true);
|
||||
normalTrayIcon = icon;
|
||||
return icon;
|
||||
}
|
||||
|
||||
function formatIssueCount(count: number): string {
|
||||
return count > 99 ? "99+" : String(count);
|
||||
}
|
||||
|
||||
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 ? `GEO Rankly Desktop · ${safeCount} 个账号健康问题` : "GEO Rankly Desktop");
|
||||
}
|
||||
|
||||
export function initTray(onOpen: () => void): ElectronTray {
|
||||
if (tray) {
|
||||
return tray;
|
||||
}
|
||||
|
||||
tray = new Tray(createTrayIcon());
|
||||
tray.setToolTip("GEO Rankly Desktop");
|
||||
tray = new Tray(createTrayIcon("normal"));
|
||||
updateTrayIssueIndicator(0);
|
||||
tray.setContextMenu(
|
||||
Menu.buildFromTemplate([
|
||||
{ label: "Open", click: () => onOpen() },
|
||||
|
||||
Reference in New Issue
Block a user