💄 style: add windows control and tray (#7665)
* add windows support * add windows close support * improve * fix * FIX * improve builder * improve windows icon
|
Before Width: | Height: | Size: 66 KiB |
BIN
apps/desktop/build/icon-beta.ico
Normal file
|
After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 72 KiB |
@@ -79,10 +79,10 @@ const config = {
|
||||
},
|
||||
npmRebuild: true,
|
||||
nsis: {
|
||||
allowToChangeInstallationDirectory: true,
|
||||
artifactName: '${productName}-${version}-setup.${ext}',
|
||||
createDesktopShortcut: 'always',
|
||||
// allowToChangeInstallationDirectory: true,
|
||||
// oneClick: false,
|
||||
oneClick: false,
|
||||
shortcutName: '${productName}',
|
||||
uninstallDisplayName: '${productName}',
|
||||
},
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
color: #f5f5f5;
|
||||
background-color: #121212;
|
||||
}
|
||||
.error-message {
|
||||
color: #f5f5f5;
|
||||
|
||||
BIN
apps/desktop/resources/tray-icon.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
@@ -2,6 +2,7 @@ import { InterceptRouteParams } from '@lobechat/electron-client-ipc';
|
||||
import { extractSubPath, findMatchingRoute } from '~common/routes';
|
||||
|
||||
import { AppBrowsersIdentifiers, BrowsersIdentifiers } from '@/appBrowsers';
|
||||
import { IpcClientEventSender } from '@/types/ipcClientEvent';
|
||||
|
||||
import { ControllerModule, ipcClientEvent, shortcut } from './index';
|
||||
|
||||
@@ -26,6 +27,21 @@ export default class BrowserWindowsCtr extends ControllerModule {
|
||||
}
|
||||
}
|
||||
|
||||
@ipcClientEvent('closeWindow')
|
||||
closeWindow(data: undefined, sender: IpcClientEventSender) {
|
||||
this.app.browserManager.closeWindow(sender.identifier);
|
||||
}
|
||||
|
||||
@ipcClientEvent('minimizeWindow')
|
||||
minimizeWindow(data: undefined, sender: IpcClientEventSender) {
|
||||
this.app.browserManager.minimizeWindow(sender.identifier);
|
||||
}
|
||||
|
||||
@ipcClientEvent('maximizeWindow')
|
||||
maximizeWindow(data: undefined, sender: IpcClientEventSender) {
|
||||
this.app.browserManager.maximizeWindow(sender.identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle route interception requests
|
||||
* Responsible for handling route interception requests from the renderer process
|
||||
|
||||
109
apps/desktop/src/main/controllers/TrayMenuCtr.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import {
|
||||
ShowTrayNotificationParams,
|
||||
UpdateTrayIconParams,
|
||||
UpdateTrayTooltipParams,
|
||||
} from '@lobechat/electron-client-ipc';
|
||||
|
||||
import { createLogger } from '@/utils/logger';
|
||||
|
||||
import { ControllerModule, ipcClientEvent, shortcut } from './index';
|
||||
|
||||
// 创建日志记录器
|
||||
const logger = createLogger('controllers:TrayMenuCtr');
|
||||
|
||||
export default class TrayMenuCtr extends ControllerModule {
|
||||
/**
|
||||
* 使用快捷键切换窗口可见性
|
||||
*/
|
||||
@shortcut('toggleMainWindow')
|
||||
async toggleMainWindow() {
|
||||
logger.debug('通过快捷键切换主窗口可见性');
|
||||
const mainWindow = this.app.browserManager.getMainWindow();
|
||||
mainWindow.toggleVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示托盘气泡通知
|
||||
* @param options 气泡选项
|
||||
* @returns 操作结果
|
||||
*/
|
||||
@ipcClientEvent('showTrayNotification')
|
||||
async showNotification(options: ShowTrayNotificationParams) {
|
||||
logger.debug('显示托盘气泡通知');
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
const mainTray = this.app.trayManager.getMainTray();
|
||||
|
||||
if (mainTray) {
|
||||
mainTray.displayBalloon({
|
||||
content: options.content,
|
||||
iconType: options.iconType || 'info',
|
||||
title: options.title,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
error: '托盘通知仅在 Windows 平台支持',
|
||||
success: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新托盘图标
|
||||
* @param options 图标选项
|
||||
* @returns 操作结果
|
||||
*/
|
||||
@ipcClientEvent('updateTrayIcon')
|
||||
async updateTrayIcon(options: UpdateTrayIconParams) {
|
||||
logger.debug('更新托盘图标');
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
const mainTray = this.app.trayManager.getMainTray();
|
||||
|
||||
if (mainTray && options.iconPath) {
|
||||
try {
|
||||
mainTray.updateIcon(options.iconPath);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
logger.error('更新托盘图标失败:', error);
|
||||
return {
|
||||
error: String(error),
|
||||
success: false
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
error: '托盘功能仅在 Windows 平台支持',
|
||||
success: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新托盘提示文本
|
||||
* @param options 提示文本选项
|
||||
* @returns 操作结果
|
||||
*/
|
||||
@ipcClientEvent('updateTrayTooltip')
|
||||
async updateTrayTooltip(options: UpdateTrayTooltipParams) {
|
||||
logger.debug('更新托盘提示文本');
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
const mainTray = this.app.trayManager.getMainTray();
|
||||
|
||||
if (mainTray && options.tooltip) {
|
||||
mainTray.updateTooltip(options.tooltip);
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
error: '托盘功能仅在 Windows 平台支持',
|
||||
success: false
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { buildDir, nextStandaloneDir } from '@/const/dir';
|
||||
import { isDev } from '@/const/env';
|
||||
import { IControlModule } from '@/controllers';
|
||||
import { IServiceModule } from '@/services';
|
||||
import { IpcClientEventSender } from '@/types/ipcClientEvent';
|
||||
import { createLogger } from '@/utils/logger';
|
||||
import { CustomRequestHandler, createHandler } from '@/utils/next-electron-rsc';
|
||||
|
||||
@@ -18,6 +19,7 @@ import { IoCContainer } from './IoCContainer';
|
||||
import MenuManager from './MenuManager';
|
||||
import { ShortcutManager } from './ShortcutManager';
|
||||
import { StoreManager } from './StoreManager';
|
||||
import TrayManager from './TrayManager';
|
||||
import { UpdaterManager } from './UpdaterManager';
|
||||
|
||||
const logger = createLogger('core:App');
|
||||
@@ -38,6 +40,7 @@ export class App {
|
||||
storeManager: StoreManager;
|
||||
updaterManager: UpdaterManager;
|
||||
shortcutManager: ShortcutManager;
|
||||
trayManager: TrayManager;
|
||||
|
||||
/**
|
||||
* whether app is in quiting
|
||||
@@ -92,6 +95,7 @@ export class App {
|
||||
this.menuManager = new MenuManager(this);
|
||||
this.updaterManager = new UpdaterManager(this);
|
||||
this.shortcutManager = new ShortcutManager(this);
|
||||
this.trayManager = new TrayManager(this);
|
||||
|
||||
// register the schema to interceptor url
|
||||
// it should register before app ready
|
||||
@@ -130,6 +134,11 @@ export class App {
|
||||
|
||||
this.browserManager.initializeBrowsers();
|
||||
|
||||
// Initialize tray manager
|
||||
if (process.platform === 'win32') {
|
||||
this.trayManager.initializeTrays();
|
||||
}
|
||||
|
||||
// Initialize updater manager
|
||||
await this.updaterManager.initialize();
|
||||
|
||||
@@ -340,9 +349,13 @@ export class App {
|
||||
this.ipcClientEventMap.forEach((eventInfo, key) => {
|
||||
const { controller, methodName } = eventInfo;
|
||||
|
||||
ipcMain.handle(key, async (e, ...data) => {
|
||||
ipcMain.handle(key, async (e, data) => {
|
||||
// 从 WebContents 获取对应的 BrowserWindow id
|
||||
const senderIdentifier = this.browserManager.getIdentifierByWebContents(e.sender);
|
||||
try {
|
||||
return await controller[methodName](...data);
|
||||
return await controller[methodName](data, {
|
||||
identifier: senderIdentifier,
|
||||
} as IpcClientEventSender);
|
||||
} catch (error) {
|
||||
logger.error(`Error handling IPC event ${key}:`, error);
|
||||
return { error: error.message };
|
||||
@@ -370,7 +383,13 @@ export class App {
|
||||
|
||||
// 新增 before-quit 处理函数
|
||||
private handleBeforeQuit = () => {
|
||||
this.isQuiting = true; // 首先设置标志
|
||||
logger.info('Application is preparing to quit');
|
||||
this.isQuiting = true;
|
||||
|
||||
// 销毁托盘
|
||||
if (process.platform === 'win32') {
|
||||
this.trayManager.destroyAll();
|
||||
}
|
||||
|
||||
// 执行清理操作
|
||||
this.unregisterAllRequestHandlers();
|
||||
|
||||
@@ -184,12 +184,11 @@ export default class Browser {
|
||||
|
||||
const browserWindow = new BrowserWindow({
|
||||
...res,
|
||||
|
||||
height: savedState?.height || height,
|
||||
|
||||
// Always create hidden first
|
||||
show: false,
|
||||
|
||||
// Always create hidden first
|
||||
title,
|
||||
|
||||
transparent: true,
|
||||
@@ -199,11 +198,7 @@ export default class Browser {
|
||||
// https://www.electronjs.org/docs/tutorial/context-isolation
|
||||
contextIsolation: true,
|
||||
preload: join(preloadDir, 'index.js'),
|
||||
// devTools: isDev,
|
||||
},
|
||||
// Use saved state if available, otherwise use options. Do not set x/y
|
||||
// x: savedState?.x, // Don't restore x
|
||||
// y: savedState?.y, // Don't restore y
|
||||
width: savedState?.width || width,
|
||||
});
|
||||
|
||||
@@ -215,6 +210,7 @@ export default class Browser {
|
||||
session: browserWindow.webContents.session,
|
||||
});
|
||||
|
||||
console.log('platform:',process.platform);
|
||||
// Windows 11 can use this new API
|
||||
if (process.platform === 'win32' && browserWindow.setBackgroundMaterial) {
|
||||
logger.debug(`[${this.identifier}] Setting window background material for Windows 11`);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { MainBroadcastEventKey, MainBroadcastParams } from '@lobechat/electron-client-ipc';
|
||||
import { WebContents } from 'electron';
|
||||
|
||||
import { createLogger } from '@/utils/logger';
|
||||
|
||||
@@ -15,6 +16,8 @@ export default class BrowserManager {
|
||||
|
||||
browsers: Map<AppBrowsersIdentifiers, Browser> = new Map();
|
||||
|
||||
private webContentsMap = new Map<WebContents, AppBrowsersIdentifiers>();
|
||||
|
||||
constructor(app: App) {
|
||||
logger.debug('Initializing BrowserManager');
|
||||
this.app = app;
|
||||
@@ -147,8 +150,40 @@ export default class BrowserManager {
|
||||
logger.debug(`Creating new browser: ${options.identifier}`);
|
||||
browser = new Browser(options, this.app);
|
||||
|
||||
this.browsers.set(options.identifier as AppBrowsersIdentifiers, browser);
|
||||
const identifier = options.identifier as AppBrowsersIdentifiers;
|
||||
this.browsers.set(identifier, browser);
|
||||
|
||||
// 记录 WebContents 和 identifier 的映射
|
||||
this.webContentsMap.set(browser.browserWindow.webContents, identifier);
|
||||
|
||||
// 当窗口关闭时清理映射
|
||||
browser.browserWindow.on('closed', () => {
|
||||
this.webContentsMap.delete(browser.browserWindow.webContents);
|
||||
});
|
||||
|
||||
return browser;
|
||||
}
|
||||
|
||||
closeWindow(identifier: string) {
|
||||
const browser = this.browsers.get(identifier as AppBrowsersIdentifiers);
|
||||
browser?.close();
|
||||
}
|
||||
|
||||
minimizeWindow(identifier: string) {
|
||||
const browser = this.browsers.get(identifier as AppBrowsersIdentifiers);
|
||||
browser?.browserWindow.minimize();
|
||||
}
|
||||
|
||||
maximizeWindow(identifier: string) {
|
||||
const browser = this.browsers.get(identifier as AppBrowsersIdentifiers);
|
||||
if (browser.browserWindow.isMaximized()) {
|
||||
browser?.browserWindow.unmaximize();
|
||||
} else {
|
||||
browser?.browserWindow.maximize();
|
||||
}
|
||||
}
|
||||
|
||||
getIdentifierByWebContents(webContents: WebContents): AppBrowsersIdentifiers | null {
|
||||
return this.webContentsMap.get(webContents) || null;
|
||||
}
|
||||
}
|
||||
|
||||
231
apps/desktop/src/main/core/Tray.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
import { MainBroadcastEventKey, MainBroadcastParams } from '@lobechat/electron-client-ipc';
|
||||
import {
|
||||
DisplayBalloonOptions,
|
||||
Tray as ElectronTray,
|
||||
Menu,
|
||||
MenuItemConstructorOptions,
|
||||
app,
|
||||
nativeImage,
|
||||
} from 'electron';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import { resourcesDir } from '@/const/dir';
|
||||
import { createLogger } from '@/utils/logger';
|
||||
|
||||
import type { App } from './App';
|
||||
|
||||
// 创建日志记录器
|
||||
const logger = createLogger('core:Tray');
|
||||
|
||||
export interface TrayOptions {
|
||||
/**
|
||||
* 托盘图标路径(相对于资源目录)
|
||||
*/
|
||||
iconPath: string;
|
||||
|
||||
/**
|
||||
* 托盘标识符
|
||||
*/
|
||||
identifier: string;
|
||||
|
||||
/**
|
||||
* 托盘提示文本
|
||||
*/
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
export default class Tray {
|
||||
private app: App;
|
||||
|
||||
/**
|
||||
* 内部 Electron 托盘
|
||||
*/
|
||||
private _tray?: ElectronTray;
|
||||
|
||||
/**
|
||||
* 标识符
|
||||
*/
|
||||
identifier: string;
|
||||
|
||||
/**
|
||||
* 创建时的选项
|
||||
*/
|
||||
options: TrayOptions;
|
||||
|
||||
/**
|
||||
* 获取托盘实例
|
||||
*/
|
||||
get tray() {
|
||||
return this.retrieveOrInitialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造托盘对象
|
||||
* @param options 托盘选项
|
||||
* @param application 应用实例
|
||||
*/
|
||||
constructor(options: TrayOptions, application: App) {
|
||||
logger.debug(`创建托盘实例: ${options.identifier}`);
|
||||
logger.debug(`托盘选项: ${JSON.stringify(options)}`);
|
||||
this.app = application;
|
||||
this.identifier = options.identifier;
|
||||
this.options = options;
|
||||
|
||||
// 初始化
|
||||
this.retrieveOrInitialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化托盘
|
||||
*/
|
||||
retrieveOrInitialize() {
|
||||
// 如果托盘已存在且未被销毁,则返回
|
||||
if (this._tray) {
|
||||
logger.debug(`[${this.identifier}] 返回现有托盘实例`);
|
||||
return this._tray;
|
||||
}
|
||||
|
||||
const { iconPath, tooltip } = this.options;
|
||||
|
||||
// 加载托盘图标
|
||||
logger.info(`创建新的托盘实例: ${this.identifier}`);
|
||||
const iconFile = join(resourcesDir, iconPath);
|
||||
logger.debug(`[${this.identifier}] 加载图标: ${iconFile}`);
|
||||
|
||||
try {
|
||||
const icon = nativeImage.createFromPath(iconFile);
|
||||
this._tray = new ElectronTray(icon);
|
||||
|
||||
// 设置工具提示
|
||||
if (tooltip) {
|
||||
logger.debug(`[${this.identifier}] 设置提示文本: ${tooltip}`);
|
||||
this._tray.setToolTip(tooltip);
|
||||
}
|
||||
|
||||
// 设置默认上下文菜单
|
||||
this.setContextMenu();
|
||||
|
||||
// 设置点击事件
|
||||
this._tray.on('click', () => {
|
||||
logger.debug(`[${this.identifier}] 托盘被点击`);
|
||||
this.onClick();
|
||||
});
|
||||
|
||||
logger.debug(`[${this.identifier}] 托盘实例创建完成`);
|
||||
return this._tray;
|
||||
} catch (error) {
|
||||
logger.error(`[${this.identifier}] 创建托盘失败:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置托盘上下文菜单
|
||||
* @param template 菜单模板,如果未提供则使用默认模板
|
||||
*/
|
||||
setContextMenu(template?: MenuItemConstructorOptions[]) {
|
||||
logger.debug(`[${this.identifier}] 设置托盘上下文菜单`);
|
||||
|
||||
// 如果未提供模板,使用默认菜单
|
||||
const defaultTemplate: MenuItemConstructorOptions[] = template || [
|
||||
{
|
||||
click: () => {
|
||||
logger.debug(`[${this.identifier}] 菜单项 "显示主窗口" 被点击`);
|
||||
this.app.browserManager.showMainWindow();
|
||||
},
|
||||
label: '显示主窗口',
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
click: () => {
|
||||
logger.debug(`[${this.identifier}] 菜单项 "退出" 被点击`);
|
||||
app.quit();
|
||||
},
|
||||
label: '退出',
|
||||
},
|
||||
];
|
||||
|
||||
const contextMenu = Menu.buildFromTemplate(defaultTemplate);
|
||||
this._tray?.setContextMenu(contextMenu);
|
||||
logger.debug(`[${this.identifier}] 托盘上下文菜单已设置`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理托盘点击事件
|
||||
*/
|
||||
onClick() {
|
||||
logger.debug(`[${this.identifier}] 处理托盘点击事件`);
|
||||
const mainWindow = this.app.browserManager.getMainWindow();
|
||||
|
||||
if (mainWindow) {
|
||||
if (mainWindow.browserWindow.isVisible() && mainWindow.browserWindow.isFocused()) {
|
||||
logger.debug(`[${this.identifier}] 主窗口已可见且聚焦,现在隐藏它`);
|
||||
mainWindow.hide();
|
||||
} else {
|
||||
logger.debug(`[${this.identifier}] 显示并聚焦主窗口`);
|
||||
mainWindow.show();
|
||||
mainWindow.browserWindow.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新托盘图标
|
||||
* @param iconPath 新图标路径(相对于资源目录)
|
||||
*/
|
||||
updateIcon(iconPath: string) {
|
||||
logger.debug(`[${this.identifier}] 更新图标: ${iconPath}`);
|
||||
try {
|
||||
const iconFile = join(resourcesDir, iconPath);
|
||||
const icon = nativeImage.createFromPath(iconFile);
|
||||
this._tray?.setImage(icon);
|
||||
this.options.iconPath = iconPath;
|
||||
logger.debug(`[${this.identifier}] 图标已更新`);
|
||||
} catch (error) {
|
||||
logger.error(`[${this.identifier}] 更新图标失败:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新提示文本
|
||||
* @param tooltip 新提示文本
|
||||
*/
|
||||
updateTooltip(tooltip: string) {
|
||||
logger.debug(`[${this.identifier}] 更新提示文本: ${tooltip}`);
|
||||
this._tray?.setToolTip(tooltip);
|
||||
this.options.tooltip = tooltip;
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示气泡通知(仅在 Windows 上支持)
|
||||
* @param options 气泡选项
|
||||
*/
|
||||
displayBalloon(options: DisplayBalloonOptions) {
|
||||
if (process.platform === 'win32' && this._tray) {
|
||||
logger.debug(`[${this.identifier}] 显示气泡通知: ${JSON.stringify(options)}`);
|
||||
this._tray.displayBalloon(options);
|
||||
} else {
|
||||
logger.debug(`[${this.identifier}] 气泡通知仅在 Windows 上支持`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播事件
|
||||
*/
|
||||
broadcast = <T extends MainBroadcastEventKey>(channel: T, data?: MainBroadcastParams<T>) => {
|
||||
logger.debug(`向托盘 ${this.identifier} 广播, 频道: ${channel}`);
|
||||
// 可以通过 App 实例的 browserManager 将消息转发到主窗口
|
||||
this.app.browserManager.getMainWindow()?.broadcast(channel, data);
|
||||
};
|
||||
|
||||
/**
|
||||
* 销毁托盘实例
|
||||
*/
|
||||
destroy() {
|
||||
logger.debug(`销毁托盘实例: ${this.identifier}`);
|
||||
if (this._tray) {
|
||||
this._tray.destroy();
|
||||
this._tray = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
131
apps/desktop/src/main/core/TrayManager.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { MainBroadcastEventKey, MainBroadcastParams } from '@lobechat/electron-client-ipc';
|
||||
|
||||
import { name } from '@/../../package.json';
|
||||
import { createLogger } from '@/utils/logger';
|
||||
|
||||
import type { App } from './App';
|
||||
import Tray, { TrayOptions } from './Tray';
|
||||
|
||||
// 创建日志记录器
|
||||
const logger = createLogger('core:TrayManager');
|
||||
|
||||
/**
|
||||
* 托盘标识符类型
|
||||
*/
|
||||
export type TrayIdentifiers = 'main';
|
||||
|
||||
export default class TrayManager {
|
||||
app: App;
|
||||
|
||||
/**
|
||||
* 存储所有托盘实例
|
||||
*/
|
||||
trays: Map<TrayIdentifiers, Tray> = new Map();
|
||||
|
||||
/**
|
||||
* 构造方法
|
||||
* @param app 应用实例
|
||||
*/
|
||||
constructor(app: App) {
|
||||
logger.debug('初始化 TrayManager');
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化所有托盘
|
||||
*/
|
||||
initializeTrays() {
|
||||
logger.debug('初始化应用托盘');
|
||||
|
||||
// 初始化主托盘
|
||||
this.initializeMainTray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取主托盘
|
||||
*/
|
||||
getMainTray() {
|
||||
return this.retrieveByIdentifier('main');
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化主托盘
|
||||
*/
|
||||
initializeMainTray() {
|
||||
logger.debug('初始化主托盘');
|
||||
return this.retrieveOrInitialize({
|
||||
iconPath: 'tray-icon.png',
|
||||
identifier: 'main', // 使用应用图标,需要确保资源目录中有此文件
|
||||
tooltip: name, // 可以使用 app.getName() 或本地化字符串
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过标识符获取托盘实例
|
||||
* @param identifier 托盘标识符
|
||||
*/
|
||||
retrieveByIdentifier(identifier: TrayIdentifiers) {
|
||||
logger.debug(`通过标识符获取托盘: ${identifier}`);
|
||||
return this.trays.get(identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* 向所有托盘广播消息
|
||||
* @param event 事件名称
|
||||
* @param data 事件数据
|
||||
*/
|
||||
broadcastToAllTrays = <T extends MainBroadcastEventKey>(
|
||||
event: T,
|
||||
data: MainBroadcastParams<T>,
|
||||
) => {
|
||||
logger.debug(`向所有托盘广播事件 ${event}`);
|
||||
this.trays.forEach((tray) => {
|
||||
tray.broadcast(event, data);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 向指定托盘广播消息
|
||||
* @param identifier 托盘标识符
|
||||
* @param event 事件名称
|
||||
* @param data 事件数据
|
||||
*/
|
||||
broadcastToTray = <T extends MainBroadcastEventKey>(
|
||||
identifier: TrayIdentifiers,
|
||||
event: T,
|
||||
data: MainBroadcastParams<T>,
|
||||
) => {
|
||||
logger.debug(`向托盘 ${identifier} 广播事件 ${event}`);
|
||||
this.trays.get(identifier)?.broadcast(event, data);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取或创建托盘实例
|
||||
* @param options 托盘选项
|
||||
*/
|
||||
private retrieveOrInitialize(options: TrayOptions) {
|
||||
let tray = this.trays.get(options.identifier as TrayIdentifiers);
|
||||
if (tray) {
|
||||
logger.debug(`获取现有托盘: ${options.identifier}`);
|
||||
return tray;
|
||||
}
|
||||
|
||||
logger.debug(`创建新托盘: ${options.identifier}`);
|
||||
tray = new Tray(options, this.app);
|
||||
|
||||
this.trays.set(options.identifier as TrayIdentifiers, tray);
|
||||
|
||||
return tray;
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁所有托盘
|
||||
*/
|
||||
destroyAll() {
|
||||
logger.debug('销毁所有托盘');
|
||||
this.trays.forEach((tray) => {
|
||||
tray.destroy();
|
||||
});
|
||||
this.trays.clear();
|
||||
}
|
||||
}
|
||||
3
apps/desktop/src/main/types/ipcClientEvent.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface IpcClientEventSender {
|
||||
identifier: string;
|
||||
}
|
||||