@lobehub/chat 1.84.12 → 1.84.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/apps/desktop/build/icon-beta.ico +0 -0
- package/apps/desktop/build/icon-nightly.ico +0 -0
- package/apps/desktop/build/icon.ico +0 -0
- package/apps/desktop/electron-builder.js +2 -2
- package/apps/desktop/resources/error.html +0 -1
- package/apps/desktop/resources/tray-icon.png +0 -0
- package/apps/desktop/src/main/controllers/BrowserWindowsCtr.ts +16 -0
- package/apps/desktop/src/main/controllers/TrayMenuCtr.ts +109 -0
- package/apps/desktop/src/main/core/App.ts +22 -3
- package/apps/desktop/src/main/core/Browser.ts +2 -6
- package/apps/desktop/src/main/core/BrowserManager.ts +36 -1
- package/apps/desktop/src/main/core/Tray.ts +231 -0
- package/apps/desktop/src/main/core/TrayManager.ts +131 -0
- package/apps/desktop/src/main/types/ipcClientEvent.ts +3 -0
- package/changelog/v1.json +18 -0
- package/package.json +1 -1
- package/packages/electron-client-ipc/src/events/index.ts +3 -1
- package/packages/electron-client-ipc/src/events/system.ts +3 -0
- package/packages/electron-client-ipc/src/events/tray.ts +31 -0
- package/packages/electron-client-ipc/src/types/index.ts +1 -0
- package/packages/electron-client-ipc/src/types/tray.ts +39 -0
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files/FileItem/File.tsx +1 -4
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files/FileItem/Image.tsx +6 -17
- package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/TopicPanel.tsx +1 -0
- package/src/app/[variants]/(main)/settings/provider/features/ProviderConfig/Checker.tsx +4 -0
- package/src/features/ElectronTitlebar/WinControl/index.tsx +91 -0
- package/src/features/ElectronTitlebar/const.ts +1 -0
- package/src/features/ElectronTitlebar/index.tsx +19 -5
- package/src/services/electron/system.ts +12 -0
- package/apps/desktop/build/favicon.ico +0 -0
package/CHANGELOG.md
CHANGED
@@ -2,6 +2,56 @@
|
|
2
2
|
|
3
3
|
# Changelog
|
4
4
|
|
5
|
+
### [Version 1.84.14](https://github.com/lobehub/lobe-chat/compare/v1.84.13...v1.84.14)
|
6
|
+
|
7
|
+
<sup>Released on **2025-05-01**</sup>
|
8
|
+
|
9
|
+
#### 💄 Styles
|
10
|
+
|
11
|
+
- **misc**: Add windows control and tray.
|
12
|
+
|
13
|
+
<br/>
|
14
|
+
|
15
|
+
<details>
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
17
|
+
|
18
|
+
#### Styles
|
19
|
+
|
20
|
+
- **misc**: Add windows control and tray, closes [#7665](https://github.com/lobehub/lobe-chat/issues/7665) ([c5f3d13](https://github.com/lobehub/lobe-chat/commit/c5f3d13))
|
21
|
+
|
22
|
+
</details>
|
23
|
+
|
24
|
+
<div align="right">
|
25
|
+
|
26
|
+
[](#readme-top)
|
27
|
+
|
28
|
+
</div>
|
29
|
+
|
30
|
+
### [Version 1.84.13](https://github.com/lobehub/lobe-chat/compare/v1.84.12...v1.84.13)
|
31
|
+
|
32
|
+
<sup>Released on **2025-05-01**</sup>
|
33
|
+
|
34
|
+
#### 💄 Styles
|
35
|
+
|
36
|
+
- **misc**: Fix style issues.
|
37
|
+
|
38
|
+
<br/>
|
39
|
+
|
40
|
+
<details>
|
41
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
42
|
+
|
43
|
+
#### Styles
|
44
|
+
|
45
|
+
- **misc**: Fix style issues, closes [#7659](https://github.com/lobehub/lobe-chat/issues/7659) ([3da871b](https://github.com/lobehub/lobe-chat/commit/3da871b))
|
46
|
+
|
47
|
+
</details>
|
48
|
+
|
49
|
+
<div align="right">
|
50
|
+
|
51
|
+
[](#readme-top)
|
52
|
+
|
53
|
+
</div>
|
54
|
+
|
5
55
|
### [Version 1.84.12](https://github.com/lobehub/lobe-chat/compare/v1.84.11...v1.84.12)
|
6
56
|
|
7
57
|
<sup>Released on **2025-04-30**</sup>
|
Binary file
|
Binary file
|
Binary file
|
@@ -79,10 +79,10 @@ const config = {
|
|
79
79
|
},
|
80
80
|
npmRebuild: true,
|
81
81
|
nsis: {
|
82
|
+
allowToChangeInstallationDirectory: true,
|
82
83
|
artifactName: '${productName}-${version}-setup.${ext}',
|
83
84
|
createDesktopShortcut: 'always',
|
84
|
-
|
85
|
-
// oneClick: false,
|
85
|
+
oneClick: false,
|
86
86
|
shortcutName: '${productName}',
|
87
87
|
uninstallDisplayName: '${productName}',
|
88
88
|
},
|
Binary file
|
@@ -2,6 +2,7 @@ import { InterceptRouteParams } from '@lobechat/electron-client-ipc';
|
|
2
2
|
import { extractSubPath, findMatchingRoute } from '~common/routes';
|
3
3
|
|
4
4
|
import { AppBrowsersIdentifiers, BrowsersIdentifiers } from '@/appBrowsers';
|
5
|
+
import { IpcClientEventSender } from '@/types/ipcClientEvent';
|
5
6
|
|
6
7
|
import { ControllerModule, ipcClientEvent, shortcut } from './index';
|
7
8
|
|
@@ -26,6 +27,21 @@ export default class BrowserWindowsCtr extends ControllerModule {
|
|
26
27
|
}
|
27
28
|
}
|
28
29
|
|
30
|
+
@ipcClientEvent('closeWindow')
|
31
|
+
closeWindow(data: undefined, sender: IpcClientEventSender) {
|
32
|
+
this.app.browserManager.closeWindow(sender.identifier);
|
33
|
+
}
|
34
|
+
|
35
|
+
@ipcClientEvent('minimizeWindow')
|
36
|
+
minimizeWindow(data: undefined, sender: IpcClientEventSender) {
|
37
|
+
this.app.browserManager.minimizeWindow(sender.identifier);
|
38
|
+
}
|
39
|
+
|
40
|
+
@ipcClientEvent('maximizeWindow')
|
41
|
+
maximizeWindow(data: undefined, sender: IpcClientEventSender) {
|
42
|
+
this.app.browserManager.maximizeWindow(sender.identifier);
|
43
|
+
}
|
44
|
+
|
29
45
|
/**
|
30
46
|
* Handle route interception requests
|
31
47
|
* Responsible for handling route interception requests from the renderer process
|
@@ -0,0 +1,109 @@
|
|
1
|
+
import {
|
2
|
+
ShowTrayNotificationParams,
|
3
|
+
UpdateTrayIconParams,
|
4
|
+
UpdateTrayTooltipParams,
|
5
|
+
} from '@lobechat/electron-client-ipc';
|
6
|
+
|
7
|
+
import { createLogger } from '@/utils/logger';
|
8
|
+
|
9
|
+
import { ControllerModule, ipcClientEvent, shortcut } from './index';
|
10
|
+
|
11
|
+
// 创建日志记录器
|
12
|
+
const logger = createLogger('controllers:TrayMenuCtr');
|
13
|
+
|
14
|
+
export default class TrayMenuCtr extends ControllerModule {
|
15
|
+
/**
|
16
|
+
* 使用快捷键切换窗口可见性
|
17
|
+
*/
|
18
|
+
@shortcut('toggleMainWindow')
|
19
|
+
async toggleMainWindow() {
|
20
|
+
logger.debug('通过快捷键切换主窗口可见性');
|
21
|
+
const mainWindow = this.app.browserManager.getMainWindow();
|
22
|
+
mainWindow.toggleVisible();
|
23
|
+
}
|
24
|
+
|
25
|
+
/**
|
26
|
+
* 显示托盘气泡通知
|
27
|
+
* @param options 气泡选项
|
28
|
+
* @returns 操作结果
|
29
|
+
*/
|
30
|
+
@ipcClientEvent('showTrayNotification')
|
31
|
+
async showNotification(options: ShowTrayNotificationParams) {
|
32
|
+
logger.debug('显示托盘气泡通知');
|
33
|
+
|
34
|
+
if (process.platform === 'win32') {
|
35
|
+
const mainTray = this.app.trayManager.getMainTray();
|
36
|
+
|
37
|
+
if (mainTray) {
|
38
|
+
mainTray.displayBalloon({
|
39
|
+
content: options.content,
|
40
|
+
iconType: options.iconType || 'info',
|
41
|
+
title: options.title,
|
42
|
+
});
|
43
|
+
|
44
|
+
return { success: true };
|
45
|
+
}
|
46
|
+
}
|
47
|
+
|
48
|
+
return {
|
49
|
+
error: '托盘通知仅在 Windows 平台支持',
|
50
|
+
success: false
|
51
|
+
};
|
52
|
+
}
|
53
|
+
|
54
|
+
/**
|
55
|
+
* 更新托盘图标
|
56
|
+
* @param options 图标选项
|
57
|
+
* @returns 操作结果
|
58
|
+
*/
|
59
|
+
@ipcClientEvent('updateTrayIcon')
|
60
|
+
async updateTrayIcon(options: UpdateTrayIconParams) {
|
61
|
+
logger.debug('更新托盘图标');
|
62
|
+
|
63
|
+
if (process.platform === 'win32') {
|
64
|
+
const mainTray = this.app.trayManager.getMainTray();
|
65
|
+
|
66
|
+
if (mainTray && options.iconPath) {
|
67
|
+
try {
|
68
|
+
mainTray.updateIcon(options.iconPath);
|
69
|
+
return { success: true };
|
70
|
+
} catch (error) {
|
71
|
+
logger.error('更新托盘图标失败:', error);
|
72
|
+
return {
|
73
|
+
error: String(error),
|
74
|
+
success: false
|
75
|
+
};
|
76
|
+
}
|
77
|
+
}
|
78
|
+
}
|
79
|
+
|
80
|
+
return {
|
81
|
+
error: '托盘功能仅在 Windows 平台支持',
|
82
|
+
success: false
|
83
|
+
};
|
84
|
+
}
|
85
|
+
|
86
|
+
/**
|
87
|
+
* 更新托盘提示文本
|
88
|
+
* @param options 提示文本选项
|
89
|
+
* @returns 操作结果
|
90
|
+
*/
|
91
|
+
@ipcClientEvent('updateTrayTooltip')
|
92
|
+
async updateTrayTooltip(options: UpdateTrayTooltipParams) {
|
93
|
+
logger.debug('更新托盘提示文本');
|
94
|
+
|
95
|
+
if (process.platform === 'win32') {
|
96
|
+
const mainTray = this.app.trayManager.getMainTray();
|
97
|
+
|
98
|
+
if (mainTray && options.tooltip) {
|
99
|
+
mainTray.updateTooltip(options.tooltip);
|
100
|
+
return { success: true };
|
101
|
+
}
|
102
|
+
}
|
103
|
+
|
104
|
+
return {
|
105
|
+
error: '托盘功能仅在 Windows 平台支持',
|
106
|
+
success: false
|
107
|
+
};
|
108
|
+
}
|
109
|
+
}
|
@@ -9,6 +9,7 @@ import { buildDir, nextStandaloneDir } from '@/const/dir';
|
|
9
9
|
import { isDev } from '@/const/env';
|
10
10
|
import { IControlModule } from '@/controllers';
|
11
11
|
import { IServiceModule } from '@/services';
|
12
|
+
import { IpcClientEventSender } from '@/types/ipcClientEvent';
|
12
13
|
import { createLogger } from '@/utils/logger';
|
13
14
|
import { CustomRequestHandler, createHandler } from '@/utils/next-electron-rsc';
|
14
15
|
|
@@ -18,6 +19,7 @@ import { IoCContainer } from './IoCContainer';
|
|
18
19
|
import MenuManager from './MenuManager';
|
19
20
|
import { ShortcutManager } from './ShortcutManager';
|
20
21
|
import { StoreManager } from './StoreManager';
|
22
|
+
import TrayManager from './TrayManager';
|
21
23
|
import { UpdaterManager } from './UpdaterManager';
|
22
24
|
|
23
25
|
const logger = createLogger('core:App');
|
@@ -38,6 +40,7 @@ export class App {
|
|
38
40
|
storeManager: StoreManager;
|
39
41
|
updaterManager: UpdaterManager;
|
40
42
|
shortcutManager: ShortcutManager;
|
43
|
+
trayManager: TrayManager;
|
41
44
|
|
42
45
|
/**
|
43
46
|
* whether app is in quiting
|
@@ -92,6 +95,7 @@ export class App {
|
|
92
95
|
this.menuManager = new MenuManager(this);
|
93
96
|
this.updaterManager = new UpdaterManager(this);
|
94
97
|
this.shortcutManager = new ShortcutManager(this);
|
98
|
+
this.trayManager = new TrayManager(this);
|
95
99
|
|
96
100
|
// register the schema to interceptor url
|
97
101
|
// it should register before app ready
|
@@ -130,6 +134,11 @@ export class App {
|
|
130
134
|
|
131
135
|
this.browserManager.initializeBrowsers();
|
132
136
|
|
137
|
+
// Initialize tray manager
|
138
|
+
if (process.platform === 'win32') {
|
139
|
+
this.trayManager.initializeTrays();
|
140
|
+
}
|
141
|
+
|
133
142
|
// Initialize updater manager
|
134
143
|
await this.updaterManager.initialize();
|
135
144
|
|
@@ -340,9 +349,13 @@ export class App {
|
|
340
349
|
this.ipcClientEventMap.forEach((eventInfo, key) => {
|
341
350
|
const { controller, methodName } = eventInfo;
|
342
351
|
|
343
|
-
ipcMain.handle(key, async (e,
|
352
|
+
ipcMain.handle(key, async (e, data) => {
|
353
|
+
// 从 WebContents 获取对应的 BrowserWindow id
|
354
|
+
const senderIdentifier = this.browserManager.getIdentifierByWebContents(e.sender);
|
344
355
|
try {
|
345
|
-
return await controller[methodName](
|
356
|
+
return await controller[methodName](data, {
|
357
|
+
identifier: senderIdentifier,
|
358
|
+
} as IpcClientEventSender);
|
346
359
|
} catch (error) {
|
347
360
|
logger.error(`Error handling IPC event ${key}:`, error);
|
348
361
|
return { error: error.message };
|
@@ -370,7 +383,13 @@ export class App {
|
|
370
383
|
|
371
384
|
// 新增 before-quit 处理函数
|
372
385
|
private handleBeforeQuit = () => {
|
373
|
-
|
386
|
+
logger.info('Application is preparing to quit');
|
387
|
+
this.isQuiting = true;
|
388
|
+
|
389
|
+
// 销毁托盘
|
390
|
+
if (process.platform === 'win32') {
|
391
|
+
this.trayManager.destroyAll();
|
392
|
+
}
|
374
393
|
|
375
394
|
// 执行清理操作
|
376
395
|
this.unregisterAllRequestHandlers();
|
@@ -184,12 +184,11 @@ export default class Browser {
|
|
184
184
|
|
185
185
|
const browserWindow = new BrowserWindow({
|
186
186
|
...res,
|
187
|
-
|
188
187
|
height: savedState?.height || height,
|
189
188
|
|
189
|
+
// Always create hidden first
|
190
190
|
show: false,
|
191
191
|
|
192
|
-
// Always create hidden first
|
193
192
|
title,
|
194
193
|
|
195
194
|
transparent: true,
|
@@ -199,11 +198,7 @@ export default class Browser {
|
|
199
198
|
// https://www.electronjs.org/docs/tutorial/context-isolation
|
200
199
|
contextIsolation: true,
|
201
200
|
preload: join(preloadDir, 'index.js'),
|
202
|
-
// devTools: isDev,
|
203
201
|
},
|
204
|
-
// Use saved state if available, otherwise use options. Do not set x/y
|
205
|
-
// x: savedState?.x, // Don't restore x
|
206
|
-
// y: savedState?.y, // Don't restore y
|
207
202
|
width: savedState?.width || width,
|
208
203
|
});
|
209
204
|
|
@@ -215,6 +210,7 @@ export default class Browser {
|
|
215
210
|
session: browserWindow.webContents.session,
|
216
211
|
});
|
217
212
|
|
213
|
+
console.log('platform:',process.platform);
|
218
214
|
// Windows 11 can use this new API
|
219
215
|
if (process.platform === 'win32' && browserWindow.setBackgroundMaterial) {
|
220
216
|
logger.debug(`[${this.identifier}] Setting window background material for Windows 11`);
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import { MainBroadcastEventKey, MainBroadcastParams } from '@lobechat/electron-client-ipc';
|
2
|
+
import { WebContents } from 'electron';
|
2
3
|
|
3
4
|
import { createLogger } from '@/utils/logger';
|
4
5
|
|
@@ -15,6 +16,8 @@ export default class BrowserManager {
|
|
15
16
|
|
16
17
|
browsers: Map<AppBrowsersIdentifiers, Browser> = new Map();
|
17
18
|
|
19
|
+
private webContentsMap = new Map<WebContents, AppBrowsersIdentifiers>();
|
20
|
+
|
18
21
|
constructor(app: App) {
|
19
22
|
logger.debug('Initializing BrowserManager');
|
20
23
|
this.app = app;
|
@@ -147,8 +150,40 @@ export default class BrowserManager {
|
|
147
150
|
logger.debug(`Creating new browser: ${options.identifier}`);
|
148
151
|
browser = new Browser(options, this.app);
|
149
152
|
|
150
|
-
|
153
|
+
const identifier = options.identifier as AppBrowsersIdentifiers;
|
154
|
+
this.browsers.set(identifier, browser);
|
155
|
+
|
156
|
+
// 记录 WebContents 和 identifier 的映射
|
157
|
+
this.webContentsMap.set(browser.browserWindow.webContents, identifier);
|
158
|
+
|
159
|
+
// 当窗口关闭时清理映射
|
160
|
+
browser.browserWindow.on('closed', () => {
|
161
|
+
this.webContentsMap.delete(browser.browserWindow.webContents);
|
162
|
+
});
|
151
163
|
|
152
164
|
return browser;
|
153
165
|
}
|
166
|
+
|
167
|
+
closeWindow(identifier: string) {
|
168
|
+
const browser = this.browsers.get(identifier as AppBrowsersIdentifiers);
|
169
|
+
browser?.close();
|
170
|
+
}
|
171
|
+
|
172
|
+
minimizeWindow(identifier: string) {
|
173
|
+
const browser = this.browsers.get(identifier as AppBrowsersIdentifiers);
|
174
|
+
browser?.browserWindow.minimize();
|
175
|
+
}
|
176
|
+
|
177
|
+
maximizeWindow(identifier: string) {
|
178
|
+
const browser = this.browsers.get(identifier as AppBrowsersIdentifiers);
|
179
|
+
if (browser.browserWindow.isMaximized()) {
|
180
|
+
browser?.browserWindow.unmaximize();
|
181
|
+
} else {
|
182
|
+
browser?.browserWindow.maximize();
|
183
|
+
}
|
184
|
+
}
|
185
|
+
|
186
|
+
getIdentifierByWebContents(webContents: WebContents): AppBrowsersIdentifiers | null {
|
187
|
+
return this.webContentsMap.get(webContents) || null;
|
188
|
+
}
|
154
189
|
}
|
@@ -0,0 +1,231 @@
|
|
1
|
+
import { MainBroadcastEventKey, MainBroadcastParams } from '@lobechat/electron-client-ipc';
|
2
|
+
import {
|
3
|
+
DisplayBalloonOptions,
|
4
|
+
Tray as ElectronTray,
|
5
|
+
Menu,
|
6
|
+
MenuItemConstructorOptions,
|
7
|
+
app,
|
8
|
+
nativeImage,
|
9
|
+
} from 'electron';
|
10
|
+
import { join } from 'node:path';
|
11
|
+
|
12
|
+
import { resourcesDir } from '@/const/dir';
|
13
|
+
import { createLogger } from '@/utils/logger';
|
14
|
+
|
15
|
+
import type { App } from './App';
|
16
|
+
|
17
|
+
// 创建日志记录器
|
18
|
+
const logger = createLogger('core:Tray');
|
19
|
+
|
20
|
+
export interface TrayOptions {
|
21
|
+
/**
|
22
|
+
* 托盘图标路径(相对于资源目录)
|
23
|
+
*/
|
24
|
+
iconPath: string;
|
25
|
+
|
26
|
+
/**
|
27
|
+
* 托盘标识符
|
28
|
+
*/
|
29
|
+
identifier: string;
|
30
|
+
|
31
|
+
/**
|
32
|
+
* 托盘提示文本
|
33
|
+
*/
|
34
|
+
tooltip?: string;
|
35
|
+
}
|
36
|
+
|
37
|
+
export default class Tray {
|
38
|
+
private app: App;
|
39
|
+
|
40
|
+
/**
|
41
|
+
* 内部 Electron 托盘
|
42
|
+
*/
|
43
|
+
private _tray?: ElectronTray;
|
44
|
+
|
45
|
+
/**
|
46
|
+
* 标识符
|
47
|
+
*/
|
48
|
+
identifier: string;
|
49
|
+
|
50
|
+
/**
|
51
|
+
* 创建时的选项
|
52
|
+
*/
|
53
|
+
options: TrayOptions;
|
54
|
+
|
55
|
+
/**
|
56
|
+
* 获取托盘实例
|
57
|
+
*/
|
58
|
+
get tray() {
|
59
|
+
return this.retrieveOrInitialize();
|
60
|
+
}
|
61
|
+
|
62
|
+
/**
|
63
|
+
* 构造托盘对象
|
64
|
+
* @param options 托盘选项
|
65
|
+
* @param application 应用实例
|
66
|
+
*/
|
67
|
+
constructor(options: TrayOptions, application: App) {
|
68
|
+
logger.debug(`创建托盘实例: ${options.identifier}`);
|
69
|
+
logger.debug(`托盘选项: ${JSON.stringify(options)}`);
|
70
|
+
this.app = application;
|
71
|
+
this.identifier = options.identifier;
|
72
|
+
this.options = options;
|
73
|
+
|
74
|
+
// 初始化
|
75
|
+
this.retrieveOrInitialize();
|
76
|
+
}
|
77
|
+
|
78
|
+
/**
|
79
|
+
* 初始化托盘
|
80
|
+
*/
|
81
|
+
retrieveOrInitialize() {
|
82
|
+
// 如果托盘已存在且未被销毁,则返回
|
83
|
+
if (this._tray) {
|
84
|
+
logger.debug(`[${this.identifier}] 返回现有托盘实例`);
|
85
|
+
return this._tray;
|
86
|
+
}
|
87
|
+
|
88
|
+
const { iconPath, tooltip } = this.options;
|
89
|
+
|
90
|
+
// 加载托盘图标
|
91
|
+
logger.info(`创建新的托盘实例: ${this.identifier}`);
|
92
|
+
const iconFile = join(resourcesDir, iconPath);
|
93
|
+
logger.debug(`[${this.identifier}] 加载图标: ${iconFile}`);
|
94
|
+
|
95
|
+
try {
|
96
|
+
const icon = nativeImage.createFromPath(iconFile);
|
97
|
+
this._tray = new ElectronTray(icon);
|
98
|
+
|
99
|
+
// 设置工具提示
|
100
|
+
if (tooltip) {
|
101
|
+
logger.debug(`[${this.identifier}] 设置提示文本: ${tooltip}`);
|
102
|
+
this._tray.setToolTip(tooltip);
|
103
|
+
}
|
104
|
+
|
105
|
+
// 设置默认上下文菜单
|
106
|
+
this.setContextMenu();
|
107
|
+
|
108
|
+
// 设置点击事件
|
109
|
+
this._tray.on('click', () => {
|
110
|
+
logger.debug(`[${this.identifier}] 托盘被点击`);
|
111
|
+
this.onClick();
|
112
|
+
});
|
113
|
+
|
114
|
+
logger.debug(`[${this.identifier}] 托盘实例创建完成`);
|
115
|
+
return this._tray;
|
116
|
+
} catch (error) {
|
117
|
+
logger.error(`[${this.identifier}] 创建托盘失败:`, error);
|
118
|
+
throw error;
|
119
|
+
}
|
120
|
+
}
|
121
|
+
|
122
|
+
/**
|
123
|
+
* 设置托盘上下文菜单
|
124
|
+
* @param template 菜单模板,如果未提供则使用默认模板
|
125
|
+
*/
|
126
|
+
setContextMenu(template?: MenuItemConstructorOptions[]) {
|
127
|
+
logger.debug(`[${this.identifier}] 设置托盘上下文菜单`);
|
128
|
+
|
129
|
+
// 如果未提供模板,使用默认菜单
|
130
|
+
const defaultTemplate: MenuItemConstructorOptions[] = template || [
|
131
|
+
{
|
132
|
+
click: () => {
|
133
|
+
logger.debug(`[${this.identifier}] 菜单项 "显示主窗口" 被点击`);
|
134
|
+
this.app.browserManager.showMainWindow();
|
135
|
+
},
|
136
|
+
label: '显示主窗口',
|
137
|
+
},
|
138
|
+
{ type: 'separator' },
|
139
|
+
{
|
140
|
+
click: () => {
|
141
|
+
logger.debug(`[${this.identifier}] 菜单项 "退出" 被点击`);
|
142
|
+
app.quit();
|
143
|
+
},
|
144
|
+
label: '退出',
|
145
|
+
},
|
146
|
+
];
|
147
|
+
|
148
|
+
const contextMenu = Menu.buildFromTemplate(defaultTemplate);
|
149
|
+
this._tray?.setContextMenu(contextMenu);
|
150
|
+
logger.debug(`[${this.identifier}] 托盘上下文菜单已设置`);
|
151
|
+
}
|
152
|
+
|
153
|
+
/**
|
154
|
+
* 处理托盘点击事件
|
155
|
+
*/
|
156
|
+
onClick() {
|
157
|
+
logger.debug(`[${this.identifier}] 处理托盘点击事件`);
|
158
|
+
const mainWindow = this.app.browserManager.getMainWindow();
|
159
|
+
|
160
|
+
if (mainWindow) {
|
161
|
+
if (mainWindow.browserWindow.isVisible() && mainWindow.browserWindow.isFocused()) {
|
162
|
+
logger.debug(`[${this.identifier}] 主窗口已可见且聚焦,现在隐藏它`);
|
163
|
+
mainWindow.hide();
|
164
|
+
} else {
|
165
|
+
logger.debug(`[${this.identifier}] 显示并聚焦主窗口`);
|
166
|
+
mainWindow.show();
|
167
|
+
mainWindow.browserWindow.focus();
|
168
|
+
}
|
169
|
+
}
|
170
|
+
}
|
171
|
+
|
172
|
+
/**
|
173
|
+
* 更新托盘图标
|
174
|
+
* @param iconPath 新图标路径(相对于资源目录)
|
175
|
+
*/
|
176
|
+
updateIcon(iconPath: string) {
|
177
|
+
logger.debug(`[${this.identifier}] 更新图标: ${iconPath}`);
|
178
|
+
try {
|
179
|
+
const iconFile = join(resourcesDir, iconPath);
|
180
|
+
const icon = nativeImage.createFromPath(iconFile);
|
181
|
+
this._tray?.setImage(icon);
|
182
|
+
this.options.iconPath = iconPath;
|
183
|
+
logger.debug(`[${this.identifier}] 图标已更新`);
|
184
|
+
} catch (error) {
|
185
|
+
logger.error(`[${this.identifier}] 更新图标失败:`, error);
|
186
|
+
}
|
187
|
+
}
|
188
|
+
|
189
|
+
/**
|
190
|
+
* 更新提示文本
|
191
|
+
* @param tooltip 新提示文本
|
192
|
+
*/
|
193
|
+
updateTooltip(tooltip: string) {
|
194
|
+
logger.debug(`[${this.identifier}] 更新提示文本: ${tooltip}`);
|
195
|
+
this._tray?.setToolTip(tooltip);
|
196
|
+
this.options.tooltip = tooltip;
|
197
|
+
}
|
198
|
+
|
199
|
+
/**
|
200
|
+
* 显示气泡通知(仅在 Windows 上支持)
|
201
|
+
* @param options 气泡选项
|
202
|
+
*/
|
203
|
+
displayBalloon(options: DisplayBalloonOptions) {
|
204
|
+
if (process.platform === 'win32' && this._tray) {
|
205
|
+
logger.debug(`[${this.identifier}] 显示气泡通知: ${JSON.stringify(options)}`);
|
206
|
+
this._tray.displayBalloon(options);
|
207
|
+
} else {
|
208
|
+
logger.debug(`[${this.identifier}] 气泡通知仅在 Windows 上支持`);
|
209
|
+
}
|
210
|
+
}
|
211
|
+
|
212
|
+
/**
|
213
|
+
* 广播事件
|
214
|
+
*/
|
215
|
+
broadcast = <T extends MainBroadcastEventKey>(channel: T, data?: MainBroadcastParams<T>) => {
|
216
|
+
logger.debug(`向托盘 ${this.identifier} 广播, 频道: ${channel}`);
|
217
|
+
// 可以通过 App 实例的 browserManager 将消息转发到主窗口
|
218
|
+
this.app.browserManager.getMainWindow()?.broadcast(channel, data);
|
219
|
+
};
|
220
|
+
|
221
|
+
/**
|
222
|
+
* 销毁托盘实例
|
223
|
+
*/
|
224
|
+
destroy() {
|
225
|
+
logger.debug(`销毁托盘实例: ${this.identifier}`);
|
226
|
+
if (this._tray) {
|
227
|
+
this._tray.destroy();
|
228
|
+
this._tray = undefined;
|
229
|
+
}
|
230
|
+
}
|
231
|
+
}
|
@@ -0,0 +1,131 @@
|
|
1
|
+
import { MainBroadcastEventKey, MainBroadcastParams } from '@lobechat/electron-client-ipc';
|
2
|
+
|
3
|
+
import { name } from '@/../../package.json';
|
4
|
+
import { createLogger } from '@/utils/logger';
|
5
|
+
|
6
|
+
import type { App } from './App';
|
7
|
+
import Tray, { TrayOptions } from './Tray';
|
8
|
+
|
9
|
+
// 创建日志记录器
|
10
|
+
const logger = createLogger('core:TrayManager');
|
11
|
+
|
12
|
+
/**
|
13
|
+
* 托盘标识符类型
|
14
|
+
*/
|
15
|
+
export type TrayIdentifiers = 'main';
|
16
|
+
|
17
|
+
export default class TrayManager {
|
18
|
+
app: App;
|
19
|
+
|
20
|
+
/**
|
21
|
+
* 存储所有托盘实例
|
22
|
+
*/
|
23
|
+
trays: Map<TrayIdentifiers, Tray> = new Map();
|
24
|
+
|
25
|
+
/**
|
26
|
+
* 构造方法
|
27
|
+
* @param app 应用实例
|
28
|
+
*/
|
29
|
+
constructor(app: App) {
|
30
|
+
logger.debug('初始化 TrayManager');
|
31
|
+
this.app = app;
|
32
|
+
}
|
33
|
+
|
34
|
+
/**
|
35
|
+
* 初始化所有托盘
|
36
|
+
*/
|
37
|
+
initializeTrays() {
|
38
|
+
logger.debug('初始化应用托盘');
|
39
|
+
|
40
|
+
// 初始化主托盘
|
41
|
+
this.initializeMainTray();
|
42
|
+
}
|
43
|
+
|
44
|
+
/**
|
45
|
+
* 获取主托盘
|
46
|
+
*/
|
47
|
+
getMainTray() {
|
48
|
+
return this.retrieveByIdentifier('main');
|
49
|
+
}
|
50
|
+
|
51
|
+
/**
|
52
|
+
* 初始化主托盘
|
53
|
+
*/
|
54
|
+
initializeMainTray() {
|
55
|
+
logger.debug('初始化主托盘');
|
56
|
+
return this.retrieveOrInitialize({
|
57
|
+
iconPath: 'tray-icon.png',
|
58
|
+
identifier: 'main', // 使用应用图标,需要确保资源目录中有此文件
|
59
|
+
tooltip: name, // 可以使用 app.getName() 或本地化字符串
|
60
|
+
});
|
61
|
+
}
|
62
|
+
|
63
|
+
/**
|
64
|
+
* 通过标识符获取托盘实例
|
65
|
+
* @param identifier 托盘标识符
|
66
|
+
*/
|
67
|
+
retrieveByIdentifier(identifier: TrayIdentifiers) {
|
68
|
+
logger.debug(`通过标识符获取托盘: ${identifier}`);
|
69
|
+
return this.trays.get(identifier);
|
70
|
+
}
|
71
|
+
|
72
|
+
/**
|
73
|
+
* 向所有托盘广播消息
|
74
|
+
* @param event 事件名称
|
75
|
+
* @param data 事件数据
|
76
|
+
*/
|
77
|
+
broadcastToAllTrays = <T extends MainBroadcastEventKey>(
|
78
|
+
event: T,
|
79
|
+
data: MainBroadcastParams<T>,
|
80
|
+
) => {
|
81
|
+
logger.debug(`向所有托盘广播事件 ${event}`);
|
82
|
+
this.trays.forEach((tray) => {
|
83
|
+
tray.broadcast(event, data);
|
84
|
+
});
|
85
|
+
};
|
86
|
+
|
87
|
+
/**
|
88
|
+
* 向指定托盘广播消息
|
89
|
+
* @param identifier 托盘标识符
|
90
|
+
* @param event 事件名称
|
91
|
+
* @param data 事件数据
|
92
|
+
*/
|
93
|
+
broadcastToTray = <T extends MainBroadcastEventKey>(
|
94
|
+
identifier: TrayIdentifiers,
|
95
|
+
event: T,
|
96
|
+
data: MainBroadcastParams<T>,
|
97
|
+
) => {
|
98
|
+
logger.debug(`向托盘 ${identifier} 广播事件 ${event}`);
|
99
|
+
this.trays.get(identifier)?.broadcast(event, data);
|
100
|
+
};
|
101
|
+
|
102
|
+
/**
|
103
|
+
* 获取或创建托盘实例
|
104
|
+
* @param options 托盘选项
|
105
|
+
*/
|
106
|
+
private retrieveOrInitialize(options: TrayOptions) {
|
107
|
+
let tray = this.trays.get(options.identifier as TrayIdentifiers);
|
108
|
+
if (tray) {
|
109
|
+
logger.debug(`获取现有托盘: ${options.identifier}`);
|
110
|
+
return tray;
|
111
|
+
}
|
112
|
+
|
113
|
+
logger.debug(`创建新托盘: ${options.identifier}`);
|
114
|
+
tray = new Tray(options, this.app);
|
115
|
+
|
116
|
+
this.trays.set(options.identifier as TrayIdentifiers, tray);
|
117
|
+
|
118
|
+
return tray;
|
119
|
+
}
|
120
|
+
|
121
|
+
/**
|
122
|
+
* 销毁所有托盘
|
123
|
+
*/
|
124
|
+
destroyAll() {
|
125
|
+
logger.debug('销毁所有托盘');
|
126
|
+
this.trays.forEach((tray) => {
|
127
|
+
tray.destroy();
|
128
|
+
});
|
129
|
+
this.trays.clear();
|
130
|
+
}
|
131
|
+
}
|
package/changelog/v1.json
CHANGED
@@ -1,4 +1,22 @@
|
|
1
1
|
[
|
2
|
+
{
|
3
|
+
"children": {
|
4
|
+
"improvements": [
|
5
|
+
"Add windows control and tray."
|
6
|
+
]
|
7
|
+
},
|
8
|
+
"date": "2025-05-01",
|
9
|
+
"version": "1.84.14"
|
10
|
+
},
|
11
|
+
{
|
12
|
+
"children": {
|
13
|
+
"improvements": [
|
14
|
+
"Fix style issues."
|
15
|
+
]
|
16
|
+
},
|
17
|
+
"date": "2025-05-01",
|
18
|
+
"version": "1.84.13"
|
19
|
+
},
|
2
20
|
{
|
3
21
|
"children": {
|
4
22
|
"improvements": [
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@lobehub/chat",
|
3
|
-
"version": "1.84.
|
3
|
+
"version": "1.84.14",
|
4
4
|
"description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
5
5
|
"keywords": [
|
6
6
|
"framework",
|
@@ -3,6 +3,7 @@ import { MenuDispatchEvents } from './menu';
|
|
3
3
|
import { RemoteServerBroadcastEvents, RemoteServerDispatchEvents } from './remoteServer';
|
4
4
|
import { ShortcutDispatchEvents } from './shortcut';
|
5
5
|
import { SystemDispatchEvents } from './system';
|
6
|
+
import { TrayDispatchEvents } from './tray';
|
6
7
|
import { AutoUpdateBroadcastEvents, AutoUpdateDispatchEvents } from './update';
|
7
8
|
import { UploadFilesDispatchEvents } from './upload';
|
8
9
|
import { WindowsDispatchEvents } from './windows';
|
@@ -19,7 +20,8 @@ export interface ClientDispatchEvents
|
|
19
20
|
AutoUpdateDispatchEvents,
|
20
21
|
ShortcutDispatchEvents,
|
21
22
|
RemoteServerDispatchEvents,
|
22
|
-
UploadFilesDispatchEvents
|
23
|
+
UploadFilesDispatchEvents,
|
24
|
+
TrayDispatchEvents {}
|
23
25
|
|
24
26
|
export type ClientDispatchEventKey = keyof ClientDispatchEvents;
|
25
27
|
|
@@ -2,7 +2,10 @@ import { ElectronAppState } from '../types';
|
|
2
2
|
|
3
3
|
export interface SystemDispatchEvents {
|
4
4
|
checkSystemAccessibility: () => boolean | undefined;
|
5
|
+
closeWindow: () => void;
|
5
6
|
getDesktopAppState: () => ElectronAppState;
|
7
|
+
maximizeWindow: () => void;
|
8
|
+
minimizeWindow: () => void;
|
6
9
|
openExternalLink: (url: string) => void;
|
7
10
|
/**
|
8
11
|
* 更新应用语言设置
|
@@ -0,0 +1,31 @@
|
|
1
|
+
import {
|
2
|
+
ShowTrayNotificationParams,
|
3
|
+
UpdateTrayIconParams,
|
4
|
+
UpdateTrayTooltipParams,
|
5
|
+
} from '../types';
|
6
|
+
|
7
|
+
export interface TrayDispatchEvents {
|
8
|
+
/**
|
9
|
+
* 显示托盘通知
|
10
|
+
* @param params 通知参数
|
11
|
+
* @returns 操作结果
|
12
|
+
*/
|
13
|
+
showTrayNotification: (params: ShowTrayNotificationParams) => {
|
14
|
+
error?: string;
|
15
|
+
success: boolean;
|
16
|
+
};
|
17
|
+
|
18
|
+
/**
|
19
|
+
* 更新托盘图标
|
20
|
+
* @param params 图标参数
|
21
|
+
* @returns 操作结果
|
22
|
+
*/
|
23
|
+
updateTrayIcon: (params: UpdateTrayIconParams) => { error?: string; success: boolean };
|
24
|
+
|
25
|
+
/**
|
26
|
+
* 更新托盘提示文本
|
27
|
+
* @param params 提示文本参数
|
28
|
+
* @returns 操作结果
|
29
|
+
*/
|
30
|
+
updateTrayTooltip: (params: UpdateTrayTooltipParams) => { error?: string; success: boolean };
|
31
|
+
}
|
@@ -0,0 +1,39 @@
|
|
1
|
+
/**
|
2
|
+
* 显示托盘通知的参数
|
3
|
+
*/
|
4
|
+
export interface ShowTrayNotificationParams {
|
5
|
+
/**
|
6
|
+
* 通知内容
|
7
|
+
*/
|
8
|
+
content: string;
|
9
|
+
|
10
|
+
/**
|
11
|
+
* 图标类型
|
12
|
+
*/
|
13
|
+
iconType?: 'info' | 'warning' | 'error' | 'none';
|
14
|
+
|
15
|
+
/**
|
16
|
+
* 通知标题
|
17
|
+
*/
|
18
|
+
title: string;
|
19
|
+
}
|
20
|
+
|
21
|
+
/**
|
22
|
+
* 更新托盘图标的参数
|
23
|
+
*/
|
24
|
+
export interface UpdateTrayIconParams {
|
25
|
+
/**
|
26
|
+
* 图标路径(相对于资源目录)
|
27
|
+
*/
|
28
|
+
iconPath: string;
|
29
|
+
}
|
30
|
+
|
31
|
+
/**
|
32
|
+
* 更新托盘提示文本的参数
|
33
|
+
*/
|
34
|
+
export interface UpdateTrayTooltipParams {
|
35
|
+
/**
|
36
|
+
* 提示文本
|
37
|
+
*/
|
38
|
+
tooltip: string;
|
39
|
+
}
|
@@ -3,8 +3,6 @@ import { createStyles } from 'antd-style';
|
|
3
3
|
import { Trash } from 'lucide-react';
|
4
4
|
import { memo } from 'react';
|
5
5
|
|
6
|
-
import { usePlatform } from '@/hooks/usePlatform';
|
7
|
-
|
8
6
|
import { MIN_IMAGE_SIZE } from './style';
|
9
7
|
|
10
8
|
const useStyles = createStyles(({ css, token }) => ({
|
@@ -21,15 +19,9 @@ const useStyles = createStyles(({ css, token }) => ({
|
|
21
19
|
box-shadow: 0 0 0 1px ${token.colorFill} inset;
|
22
20
|
`,
|
23
21
|
image: css`
|
22
|
+
width: 64px !important;
|
23
|
+
height: 64px !important;
|
24
24
|
margin-block: 0 !important;
|
25
|
-
|
26
|
-
.ant-image {
|
27
|
-
height: 100% !important;
|
28
|
-
|
29
|
-
img {
|
30
|
-
height: 100% !important;
|
31
|
-
}
|
32
|
-
}
|
33
25
|
`,
|
34
26
|
}));
|
35
27
|
|
@@ -43,7 +35,6 @@ interface FileItemProps {
|
|
43
35
|
const FileItem = memo<FileItemProps>(({ alt, onRemove, src, loading }) => {
|
44
36
|
const IMAGE_SIZE = MIN_IMAGE_SIZE;
|
45
37
|
const { styles, cx } = useStyles();
|
46
|
-
const { isSafari } = usePlatform();
|
47
38
|
|
48
39
|
return (
|
49
40
|
<Image
|
@@ -56,19 +47,17 @@ const FileItem = memo<FileItemProps>(({ alt, onRemove, src, loading }) => {
|
|
56
47
|
e.stopPropagation();
|
57
48
|
onRemove?.();
|
58
49
|
}}
|
59
|
-
|
60
|
-
blockSize: '28px',
|
61
|
-
fontSize: '20px',
|
62
|
-
}}
|
50
|
+
size={'small'}
|
63
51
|
/>
|
64
52
|
}
|
65
53
|
alt={alt || ''}
|
66
54
|
alwaysShowActions
|
67
|
-
height={
|
55
|
+
height={64}
|
68
56
|
isLoading={loading}
|
57
|
+
objectFit={'cover'}
|
69
58
|
size={IMAGE_SIZE as any}
|
70
59
|
src={src}
|
71
|
-
|
60
|
+
width={64}
|
72
61
|
wrapperClassName={cx(styles.image, styles.editableImage)}
|
73
62
|
/>
|
74
63
|
);
|
@@ -144,6 +144,10 @@ const Checker = memo<ConnectionCheckerProps>(
|
|
144
144
|
);
|
145
145
|
}}
|
146
146
|
options={totalModels.map((id) => ({ label: id, value: id }))}
|
147
|
+
style={{
|
148
|
+
flex: 1,
|
149
|
+
overflow: 'hidden',
|
150
|
+
}}
|
147
151
|
suffixIcon={isProviderConfigUpdating && <Icon icon={Loader2Icon} spin />}
|
148
152
|
value={checkModel}
|
149
153
|
virtual
|
@@ -0,0 +1,91 @@
|
|
1
|
+
import { Icon } from '@lobehub/ui';
|
2
|
+
import { createStyles } from 'antd-style';
|
3
|
+
import { Minus, Square, XIcon } from 'lucide-react';
|
4
|
+
|
5
|
+
import { electronSystemService } from '@/services/electron/system';
|
6
|
+
|
7
|
+
import { TITLE_BAR_HEIGHT } from '../const';
|
8
|
+
|
9
|
+
const useStyles = createStyles(({ css, cx, token }) => {
|
10
|
+
const icon = css`
|
11
|
+
display: flex;
|
12
|
+
align-items: center;
|
13
|
+
justify-content: center;
|
14
|
+
|
15
|
+
width: 64px;
|
16
|
+
min-height: ${TITLE_BAR_HEIGHT}px;
|
17
|
+
|
18
|
+
color: ${token.colorTextSecondary};
|
19
|
+
|
20
|
+
transition: all ease-in-out 100ms;
|
21
|
+
|
22
|
+
-webkit-app-region: no-drag;
|
23
|
+
|
24
|
+
&:hover {
|
25
|
+
background: ${token.colorFillTertiary};
|
26
|
+
}
|
27
|
+
|
28
|
+
&:active {
|
29
|
+
background: ${token.colorFillSecondary};
|
30
|
+
}
|
31
|
+
`;
|
32
|
+
return {
|
33
|
+
close: cx(
|
34
|
+
icon,
|
35
|
+
css`
|
36
|
+
&:hover {
|
37
|
+
color: ${token.colorTextLightSolid};
|
38
|
+
|
39
|
+
/* win11 的色值,亮暗色均不变 */
|
40
|
+
background: #d33328;
|
41
|
+
}
|
42
|
+
|
43
|
+
&:active {
|
44
|
+
color: ${token.colorTextLightSolid};
|
45
|
+
|
46
|
+
/* win11 的色值 */
|
47
|
+
background: #8b2b25;
|
48
|
+
}
|
49
|
+
`,
|
50
|
+
),
|
51
|
+
container: css`
|
52
|
+
cursor: pointer;
|
53
|
+
display: flex;
|
54
|
+
`,
|
55
|
+
icon,
|
56
|
+
};
|
57
|
+
});
|
58
|
+
|
59
|
+
const WinControl = () => {
|
60
|
+
const { styles } = useStyles();
|
61
|
+
return (
|
62
|
+
<div className={styles.container}>
|
63
|
+
<div
|
64
|
+
className={styles.icon}
|
65
|
+
onClick={() => {
|
66
|
+
electronSystemService.minimizeWindow();
|
67
|
+
}}
|
68
|
+
>
|
69
|
+
<Icon icon={Minus} style={{ fontSize: 18 }} />
|
70
|
+
</div>
|
71
|
+
<div
|
72
|
+
className={styles.icon}
|
73
|
+
onClick={() => {
|
74
|
+
electronSystemService.maximizeWindow();
|
75
|
+
}}
|
76
|
+
>
|
77
|
+
<Icon icon={Square} />
|
78
|
+
</div>
|
79
|
+
<div
|
80
|
+
className={styles.close}
|
81
|
+
onClick={() => {
|
82
|
+
electronSystemService.closeWindow();
|
83
|
+
}}
|
84
|
+
>
|
85
|
+
<Icon icon={XIcon} style={{ fontSize: 18 }} />
|
86
|
+
</div>
|
87
|
+
</div>
|
88
|
+
);
|
89
|
+
};
|
90
|
+
|
91
|
+
export default WinControl;
|
@@ -0,0 +1 @@
|
|
1
|
+
export const TITLE_BAR_HEIGHT = 36;
|
@@ -1,14 +1,18 @@
|
|
1
|
+
import { Divider } from 'antd';
|
1
2
|
import { memo } from 'react';
|
2
3
|
import { Flexbox } from 'react-layout-kit';
|
3
4
|
|
4
5
|
import { useElectronStore } from '@/store/electron';
|
5
6
|
import { electronStylish } from '@/styles/electron';
|
7
|
+
import { isMacOS } from '@/utils/platform';
|
6
8
|
|
7
9
|
import Connection from './Connection';
|
8
10
|
import { UpdateModal } from './UpdateModal';
|
9
11
|
import { UpdateNotification } from './UpdateNotification';
|
12
|
+
import WinControl from './WinControl';
|
13
|
+
import { TITLE_BAR_HEIGHT } from './const';
|
10
14
|
|
11
|
-
|
15
|
+
const isMac = isMacOS();
|
12
16
|
|
13
17
|
const TitleBar = memo(() => {
|
14
18
|
const initElectronAppState = useElectronStore((s) => s.useInitElectronAppState);
|
@@ -22,16 +26,24 @@ const TitleBar = memo(() => {
|
|
22
26
|
height={TITLE_BAR_HEIGHT}
|
23
27
|
horizontal
|
24
28
|
justify={'space-between'}
|
25
|
-
paddingInline={12}
|
29
|
+
paddingInline={isMac ? 12 : '12px 0'}
|
26
30
|
style={{ minHeight: TITLE_BAR_HEIGHT }}
|
27
31
|
width={'100%'}
|
28
32
|
>
|
29
33
|
<div />
|
30
34
|
<div>{/* TODO */}</div>
|
31
35
|
|
32
|
-
<Flexbox
|
33
|
-
<
|
34
|
-
|
36
|
+
<Flexbox align={'center'} gap={4} horizontal>
|
37
|
+
<Flexbox className={electronStylish.nodrag} gap={8} horizontal>
|
38
|
+
<UpdateNotification />
|
39
|
+
<Connection />
|
40
|
+
</Flexbox>
|
41
|
+
{!isMac && (
|
42
|
+
<>
|
43
|
+
<Divider type={'vertical'} />
|
44
|
+
<WinControl />
|
45
|
+
</>
|
46
|
+
)}
|
35
47
|
</Flexbox>
|
36
48
|
<UpdateModal />
|
37
49
|
</Flexbox>
|
@@ -39,3 +51,5 @@ const TitleBar = memo(() => {
|
|
39
51
|
});
|
40
52
|
|
41
53
|
export default TitleBar;
|
54
|
+
|
55
|
+
export { TITLE_BAR_HEIGHT } from './const';
|
@@ -14,6 +14,18 @@ class ElectronSystemService {
|
|
14
14
|
return dispatch('getDesktopAppState');
|
15
15
|
}
|
16
16
|
|
17
|
+
async closeWindow(): Promise<void> {
|
18
|
+
return dispatch('closeWindow');
|
19
|
+
}
|
20
|
+
|
21
|
+
async maximizeWindow(): Promise<void> {
|
22
|
+
return dispatch('maximizeWindow');
|
23
|
+
}
|
24
|
+
|
25
|
+
async minimizeWindow(): Promise<void> {
|
26
|
+
return dispatch('minimizeWindow');
|
27
|
+
}
|
28
|
+
|
17
29
|
// Add other system-related service methods here if needed in the future
|
18
30
|
}
|
19
31
|
|
Binary file
|