💄 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;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { MenuDispatchEvents } from './menu';
|
||||
import { RemoteServerBroadcastEvents, RemoteServerDispatchEvents } from './remoteServer';
|
||||
import { ShortcutDispatchEvents } from './shortcut';
|
||||
import { SystemDispatchEvents } from './system';
|
||||
import { TrayDispatchEvents } from './tray';
|
||||
import { AutoUpdateBroadcastEvents, AutoUpdateDispatchEvents } from './update';
|
||||
import { UploadFilesDispatchEvents } from './upload';
|
||||
import { WindowsDispatchEvents } from './windows';
|
||||
@@ -19,7 +20,8 @@ export interface ClientDispatchEvents
|
||||
AutoUpdateDispatchEvents,
|
||||
ShortcutDispatchEvents,
|
||||
RemoteServerDispatchEvents,
|
||||
UploadFilesDispatchEvents {}
|
||||
UploadFilesDispatchEvents,
|
||||
TrayDispatchEvents {}
|
||||
|
||||
export type ClientDispatchEventKey = keyof ClientDispatchEvents;
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@ import { ElectronAppState } from '../types';
|
||||
|
||||
export interface SystemDispatchEvents {
|
||||
checkSystemAccessibility: () => boolean | undefined;
|
||||
closeWindow: () => void;
|
||||
getDesktopAppState: () => ElectronAppState;
|
||||
maximizeWindow: () => void;
|
||||
minimizeWindow: () => void;
|
||||
openExternalLink: (url: string) => void;
|
||||
/**
|
||||
* 更新应用语言设置
|
||||
|
||||
31
packages/electron-client-ipc/src/events/tray.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import {
|
||||
ShowTrayNotificationParams,
|
||||
UpdateTrayIconParams,
|
||||
UpdateTrayTooltipParams,
|
||||
} from '../types';
|
||||
|
||||
export interface TrayDispatchEvents {
|
||||
/**
|
||||
* 显示托盘通知
|
||||
* @param params 通知参数
|
||||
* @returns 操作结果
|
||||
*/
|
||||
showTrayNotification: (params: ShowTrayNotificationParams) => {
|
||||
error?: string;
|
||||
success: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新托盘图标
|
||||
* @param params 图标参数
|
||||
* @returns 操作结果
|
||||
*/
|
||||
updateTrayIcon: (params: UpdateTrayIconParams) => { error?: string; success: boolean };
|
||||
|
||||
/**
|
||||
* 更新托盘提示文本
|
||||
* @param params 提示文本参数
|
||||
* @returns 操作结果
|
||||
*/
|
||||
updateTrayTooltip: (params: UpdateTrayTooltipParams) => { error?: string; success: boolean };
|
||||
}
|
||||
@@ -5,5 +5,6 @@ export * from './proxyTRPCRequest';
|
||||
export * from './route';
|
||||
export * from './shortcut';
|
||||
export * from './system';
|
||||
export * from './tray';
|
||||
export * from './update';
|
||||
export * from './upload';
|
||||
|
||||
39
packages/electron-client-ipc/src/types/tray.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 显示托盘通知的参数
|
||||
*/
|
||||
export interface ShowTrayNotificationParams {
|
||||
/**
|
||||
* 通知内容
|
||||
*/
|
||||
content: string;
|
||||
|
||||
/**
|
||||
* 图标类型
|
||||
*/
|
||||
iconType?: 'info' | 'warning' | 'error' | 'none';
|
||||
|
||||
/**
|
||||
* 通知标题
|
||||
*/
|
||||
title: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新托盘图标的参数
|
||||
*/
|
||||
export interface UpdateTrayIconParams {
|
||||
/**
|
||||
* 图标路径(相对于资源目录)
|
||||
*/
|
||||
iconPath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新托盘提示文本的参数
|
||||
*/
|
||||
export interface UpdateTrayTooltipParams {
|
||||
/**
|
||||
* 提示文本
|
||||
*/
|
||||
tooltip: string;
|
||||
}
|
||||
85
src/features/ElectronTitlebar/WinControl/index.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { Icon } from '@lobehub/ui';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { Minus, Square, XIcon } from 'lucide-react';
|
||||
|
||||
import { electronSystemService } from '@/services/electron/system';
|
||||
|
||||
import { TITLE_BAR_HEIGHT } from '../const';
|
||||
|
||||
const useStyles = createStyles(({ css, cx, token }) => {
|
||||
const icon = css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64px;
|
||||
min-height: ${TITLE_BAR_HEIGHT}px;
|
||||
transition: all ease-in-out 100ms;
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
color: ${token.colorTextSecondary};
|
||||
|
||||
&:hover {
|
||||
background: ${token.colorFillTertiary};
|
||||
}
|
||||
&:active {
|
||||
background: ${token.colorFillSecondary};
|
||||
}
|
||||
`;
|
||||
return {
|
||||
close: cx(
|
||||
icon,
|
||||
css`
|
||||
&:hover {
|
||||
color: ${token.colorTextLightSolid};
|
||||
/* win11 的色值,亮暗色均不变 */
|
||||
background: #d33328;
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: ${token.colorTextLightSolid};
|
||||
/* win11 的色值 */
|
||||
background: #8b2b25;
|
||||
}
|
||||
`,
|
||||
),
|
||||
container: css`
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
`,
|
||||
icon,
|
||||
};
|
||||
});
|
||||
|
||||
const WinControl = () => {
|
||||
const { styles } = useStyles();
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div
|
||||
className={styles.icon}
|
||||
onClick={() => {
|
||||
electronSystemService.minimizeWindow();
|
||||
}}
|
||||
>
|
||||
<Icon icon={Minus} style={{ fontSize: 18 }} />
|
||||
</div>
|
||||
<div
|
||||
className={styles.icon}
|
||||
onClick={() => {
|
||||
electronSystemService.maximizeWindow();
|
||||
}}
|
||||
>
|
||||
<Icon icon={Square} />
|
||||
</div>
|
||||
<div
|
||||
className={styles.close}
|
||||
onClick={() => {
|
||||
electronSystemService.closeWindow();
|
||||
}}
|
||||
>
|
||||
<Icon icon={XIcon} style={{ fontSize: 18 }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WinControl;
|
||||
1
src/features/ElectronTitlebar/const.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const TITLE_BAR_HEIGHT = 36;
|
||||
@@ -1,14 +1,18 @@
|
||||
import { Divider } from 'antd';
|
||||
import { memo } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { useElectronStore } from '@/store/electron';
|
||||
import { electronStylish } from '@/styles/electron';
|
||||
import { isMacOS } from '@/utils/platform';
|
||||
|
||||
import Connection from './Connection';
|
||||
import { UpdateModal } from './UpdateModal';
|
||||
import { UpdateNotification } from './UpdateNotification';
|
||||
import WinControl from './WinControl';
|
||||
import { TITLE_BAR_HEIGHT } from './const';
|
||||
|
||||
export const TITLE_BAR_HEIGHT = 36;
|
||||
const isMac = isMacOS();
|
||||
|
||||
const TitleBar = memo(() => {
|
||||
const initElectronAppState = useElectronStore((s) => s.useInitElectronAppState);
|
||||
@@ -22,16 +26,24 @@ const TitleBar = memo(() => {
|
||||
height={TITLE_BAR_HEIGHT}
|
||||
horizontal
|
||||
justify={'space-between'}
|
||||
paddingInline={12}
|
||||
paddingInline={isMac ? 12 : '12px 0'}
|
||||
style={{ minHeight: TITLE_BAR_HEIGHT }}
|
||||
width={'100%'}
|
||||
>
|
||||
<div />
|
||||
<div>{/* TODO */}</div>
|
||||
|
||||
<Flexbox className={electronStylish.nodrag} gap={8} horizontal>
|
||||
<UpdateNotification />
|
||||
<Connection />
|
||||
<Flexbox align={'center'} gap={4} horizontal>
|
||||
<Flexbox className={electronStylish.nodrag} gap={8} horizontal>
|
||||
<UpdateNotification />
|
||||
<Connection />
|
||||
</Flexbox>
|
||||
{!isMac && (
|
||||
<>
|
||||
<Divider type={'vertical'} />
|
||||
<WinControl />
|
||||
</>
|
||||
)}
|
||||
</Flexbox>
|
||||
<UpdateModal />
|
||||
</Flexbox>
|
||||
@@ -39,3 +51,5 @@ const TitleBar = memo(() => {
|
||||
});
|
||||
|
||||
export default TitleBar;
|
||||
|
||||
export { TITLE_BAR_HEIGHT } from './const';
|
||||
|
||||
@@ -14,6 +14,18 @@ class ElectronSystemService {
|
||||
return dispatch('getDesktopAppState');
|
||||
}
|
||||
|
||||
async closeWindow(): Promise<void> {
|
||||
return dispatch('closeWindow');
|
||||
}
|
||||
|
||||
async maximizeWindow(): Promise<void> {
|
||||
return dispatch('maximizeWindow');
|
||||
}
|
||||
|
||||
async minimizeWindow(): Promise<void> {
|
||||
return dispatch('minimizeWindow');
|
||||
}
|
||||
|
||||
// Add other system-related service methods here if needed in the future
|
||||
}
|
||||
|
||||
|
||||