refactor: update tray service imports for consistency and clarity
This commit is contained in:
286
electron/service/tray.ts
Normal file
286
electron/service/tray.ts
Normal file
@@ -0,0 +1,286 @@
|
||||
import { app, BrowserWindow, Menu, Tray, nativeImage } from 'electron';
|
||||
import { join } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { appUpdater } from '@electron/service/updater';
|
||||
import configManager from '@electron/service/config-service';
|
||||
import logManager from '@electron/service/logger';
|
||||
import { windowManager } from '@electron/service/window-service';
|
||||
import { createTranslator } from '@electron/utils';
|
||||
import { CONFIG_KEYS, MAIN_WIN_SIZE, WINDOW_NAMES } from '@runtime/lib/constants';
|
||||
|
||||
declare const MAIN_WINDOW_VITE_DEV_SERVER_URL: string | undefined;
|
||||
|
||||
type GatewayStatus = 'connected' | 'disconnected' | 'reconnecting';
|
||||
|
||||
let tray: Tray | null = null;
|
||||
let mainWindowRef: BrowserWindow | null = null;
|
||||
let gatewayStatus: GatewayStatus = 'disconnected';
|
||||
let removeLanguageListener: (() => void) | null = null;
|
||||
let quitHookBound = false;
|
||||
let t: ReturnType<typeof createTranslator> = createTranslator();
|
||||
|
||||
function getIconsDir(): string {
|
||||
if (app.isPackaged) {
|
||||
return join(process.resourcesPath, 'resources', 'icons');
|
||||
}
|
||||
|
||||
return join(app.getAppPath(), 'resources', 'icons');
|
||||
}
|
||||
|
||||
function resolveTrayIcon() {
|
||||
const iconsDir = getIconsDir();
|
||||
const iconPath = process.platform === 'win32'
|
||||
? join(iconsDir, 'icon.ico')
|
||||
: process.platform === 'darwin'
|
||||
? join(iconsDir, 'icon.png')
|
||||
: join(iconsDir, '32x32.png');
|
||||
|
||||
let icon = nativeImage.createFromPath(iconPath);
|
||||
if (icon.isEmpty()) {
|
||||
icon = nativeImage.createFromPath(join(iconsDir, 'icon.png'));
|
||||
}
|
||||
|
||||
if (process.platform === 'darwin' && !icon.isEmpty()) {
|
||||
icon.setTemplateImage(true);
|
||||
}
|
||||
|
||||
return icon;
|
||||
}
|
||||
|
||||
function getGatewayStatusLabelKey(status: GatewayStatus): string {
|
||||
switch (status) {
|
||||
case 'connected':
|
||||
return 'tray.status.running';
|
||||
case 'reconnecting':
|
||||
return 'tray.status.restarting';
|
||||
case 'disconnected':
|
||||
default:
|
||||
return 'tray.status.stopped';
|
||||
}
|
||||
}
|
||||
|
||||
function rememberMainWindow(window: BrowserWindow | null | undefined): BrowserWindow | null {
|
||||
if (!window || window.isDestroyed()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
mainWindowRef = window;
|
||||
return window;
|
||||
}
|
||||
|
||||
function buildMainWindowUrl(route: string): string {
|
||||
const normalizedHash = route.startsWith('#') ? route.slice(1) : route;
|
||||
|
||||
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
|
||||
const url = new URL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
|
||||
url.pathname = '/';
|
||||
url.hash = normalizedHash;
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
const url = pathToFileURL(join(app.getAppPath(), 'dist', 'index.html'));
|
||||
url.hash = normalizedHash;
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function resolveMainWindow(): BrowserWindow | null {
|
||||
const trackedWindow = rememberMainWindow(mainWindowRef);
|
||||
if (trackedWindow) {
|
||||
return trackedWindow;
|
||||
}
|
||||
|
||||
const currentWindow = rememberMainWindow(windowManager.get(WINDOW_NAMES.MAIN));
|
||||
if (currentWindow) {
|
||||
return currentWindow;
|
||||
}
|
||||
|
||||
return rememberMainWindow(windowManager.create(WINDOW_NAMES.MAIN, MAIN_WIN_SIZE));
|
||||
}
|
||||
|
||||
function showMainWindow(): BrowserWindow | null {
|
||||
const mainWindow = resolveMainWindow();
|
||||
if (!mainWindow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (mainWindow.isMinimized()) {
|
||||
mainWindow.restore();
|
||||
}
|
||||
|
||||
if (!mainWindow.isVisible()) {
|
||||
mainWindow.show();
|
||||
}
|
||||
|
||||
mainWindow.focus();
|
||||
return mainWindow;
|
||||
}
|
||||
|
||||
function navigateMainWindow(route: string): void {
|
||||
const mainWindow = showMainWindow();
|
||||
if (!mainWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hashRoute = route.startsWith('#') ? route : `#${route}`;
|
||||
const applyRoute = () => {
|
||||
if (mainWindow.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
void mainWindow.webContents.executeJavaScript(
|
||||
`window.location.hash = ${JSON.stringify(hashRoute)};`,
|
||||
true,
|
||||
).catch((error) => {
|
||||
logManager.warn(`Tray navigation fallback reload for ${hashRoute}`, error);
|
||||
void mainWindow.loadURL(buildMainWindowUrl(hashRoute));
|
||||
});
|
||||
};
|
||||
|
||||
if (mainWindow.webContents.isLoadingMainFrame()) {
|
||||
mainWindow.webContents.once('did-finish-load', applyRoute);
|
||||
return;
|
||||
}
|
||||
|
||||
applyRoute();
|
||||
}
|
||||
|
||||
function toggleMainWindow(): void {
|
||||
const mainWindow = resolveMainWindow();
|
||||
if (!mainWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mainWindow.isVisible()) {
|
||||
mainWindow.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
showMainWindow();
|
||||
}
|
||||
|
||||
function buildContextMenu() {
|
||||
const gatewayStatusLabel = t(getGatewayStatusLabelKey(gatewayStatus)) ?? gatewayStatus;
|
||||
|
||||
return Menu.buildFromTemplate([
|
||||
{
|
||||
label: t('tray.showWindow') ?? 'Show Window',
|
||||
click: () => {
|
||||
showMainWindow();
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: t('tray.gatewayStatus') ?? 'Gateway Status',
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
label: gatewayStatusLabel,
|
||||
type: 'checkbox',
|
||||
checked: gatewayStatus === 'connected',
|
||||
enabled: false,
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: t('tray.quickActions') ?? 'Quick Actions',
|
||||
submenu: [
|
||||
{
|
||||
label: t('tray.openChat') ?? 'Open Chat',
|
||||
click: () => {
|
||||
navigateMainWindow('/home');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t('tray.openSettings') ?? 'Open Settings',
|
||||
click: () => {
|
||||
navigateMainWindow('/setting?view=general');
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: t('tray.checkForUpdates') ?? 'Check for Updates...',
|
||||
click: () => {
|
||||
void appUpdater.checkForUpdates();
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: t('tray.quit') ?? 'Quit ZN-AI',
|
||||
click: () => {
|
||||
app.quit();
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
function refreshTray(): void {
|
||||
if (!tray) {
|
||||
tray = new Tray(resolveTrayIcon());
|
||||
}
|
||||
|
||||
const tooltip = [t('tray.tooltip') ?? 'ZN-AI', t(getGatewayStatusLabelKey(gatewayStatus)) ?? gatewayStatus]
|
||||
.filter(Boolean)
|
||||
.join(' - ');
|
||||
|
||||
tray.setToolTip(tooltip);
|
||||
tray.setContextMenu(buildContextMenu());
|
||||
|
||||
tray.removeAllListeners('click');
|
||||
tray.removeAllListeners('double-click');
|
||||
tray.on('click', toggleMainWindow);
|
||||
tray.on('double-click', () => {
|
||||
showMainWindow();
|
||||
});
|
||||
}
|
||||
|
||||
function ensureLanguageListener(): void {
|
||||
if (removeLanguageListener) {
|
||||
return;
|
||||
}
|
||||
|
||||
removeLanguageListener = configManager.onConfigChange((config) => {
|
||||
if (!config[CONFIG_KEYS.LANGUAGE]) {
|
||||
return;
|
||||
}
|
||||
|
||||
t = createTranslator();
|
||||
if (tray) {
|
||||
refreshTray();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function createTray(mainWindow: BrowserWindow): Tray {
|
||||
rememberMainWindow(mainWindow);
|
||||
ensureLanguageListener();
|
||||
refreshTray();
|
||||
|
||||
if (!quitHookBound) {
|
||||
quitHookBound = true;
|
||||
app.once('before-quit', () => {
|
||||
destroyTray();
|
||||
quitHookBound = false;
|
||||
});
|
||||
}
|
||||
|
||||
return tray as Tray;
|
||||
}
|
||||
|
||||
export function updateTrayStatus(status: GatewayStatus): void {
|
||||
gatewayStatus = status;
|
||||
if (tray) {
|
||||
refreshTray();
|
||||
}
|
||||
}
|
||||
|
||||
export function destroyTray(): void {
|
||||
tray?.destroy();
|
||||
tray = null;
|
||||
mainWindowRef = null;
|
||||
|
||||
if (removeLanguageListener) {
|
||||
removeLanguageListener();
|
||||
removeLanguageListener = null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user