mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +07:00
🐛 fix: auto sync theme mode in desktop (#7970)
* auto sync theme * improve style * improve code
This commit is contained in:
154
.cursor/rules/desktop-feature-implementation.mdc
Normal file
154
.cursor/rules/desktop-feature-implementation.mdc
Normal file
@@ -0,0 +1,154 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
**桌面端新功能实现指南**
|
||||
|
||||
## 桌面端应用架构概述
|
||||
|
||||
LobeChat 桌面端基于 Electron 框架构建,采用主进程-渲染进程架构:
|
||||
|
||||
1. **主进程 (Main Process)**:
|
||||
- 位置:`apps/desktop/src/main`
|
||||
- 职责:控制应用生命周期、系统API交互、窗口管理、后台服务
|
||||
|
||||
2. **渲染进程 (Renderer Process)**:
|
||||
- 复用 Web 端代码,位于 `src` 目录
|
||||
- 通过 IPC 与主进程通信
|
||||
|
||||
3. **预加载脚本 (Preload)**:
|
||||
- 位置:`apps/desktop/src/preload`
|
||||
- 职责:安全地暴露主进程功能给渲染进程
|
||||
|
||||
## 添加新桌面端功能流程
|
||||
|
||||
### 1. 确定功能需求与设计
|
||||
|
||||
首先确定新功能的需求和设计,包括:
|
||||
- 功能描述和用例
|
||||
- 是否需要系统级API(如文件系统、网络等)
|
||||
- UI/UX设计(如必要)
|
||||
- 与现有功能的交互方式
|
||||
|
||||
### 2. 在主进程中实现核心功能
|
||||
|
||||
1. **创建控制器 (Controller)**
|
||||
- 位置:`apps/desktop/src/main/controllers/`
|
||||
- 示例:创建 `NewFeatureCtr.ts`
|
||||
- 规范:按 `_template.ts` 模板格式实现
|
||||
- 注册:在 `apps/desktop/src/main/controllers/index.ts` 导出
|
||||
|
||||
2. **定义 IPC 事件处理器**
|
||||
- 使用 `@ipcClientEvent('eventName')` 装饰器注册事件处理函数
|
||||
- 处理函数应接收前端传递的参数并返回结果
|
||||
- 处理可能的错误情况
|
||||
|
||||
3. **实现业务逻辑**
|
||||
- 可能需要调用 Electron API 或 Node.js 原生模块
|
||||
- 对于复杂功能,可以创建专门的服务类 (`services/`)
|
||||
|
||||
### 3. 定义 IPC 通信类型
|
||||
|
||||
1. **在共享类型定义中添加新类型**
|
||||
- 位置:`packages/electron-client-ipc/src/types.ts`
|
||||
- 添加参数类型接口(如 `NewFeatureParams`)
|
||||
- 添加返回结果类型接口(如 `NewFeatureResult`)
|
||||
|
||||
### 4. 在渲染进程实现前端功能
|
||||
|
||||
1. **创建服务层**
|
||||
- 位置:`src/services/electron/`
|
||||
- 添加服务方法调用 IPC
|
||||
- 使用 `dispatch` 或 `invoke` 函数
|
||||
|
||||
```typescript
|
||||
// src/services/electron/newFeatureService.ts
|
||||
import { dispatch } from '@lobechat/electron-client-ipc';
|
||||
import { NewFeatureParams } from 'types';
|
||||
|
||||
export const newFeatureService = async (params: NewFeatureParams) => {
|
||||
return dispatch('newFeatureEventName', params);
|
||||
};
|
||||
```
|
||||
|
||||
2. **实现 Store Action**
|
||||
- 位置:`src/store/`
|
||||
- 添加状态更新逻辑和错误处理
|
||||
|
||||
3. **添加 UI 组件**
|
||||
- 根据需要在适当位置添加UI组件
|
||||
- 通过 Store 或 Service 层调用功能
|
||||
|
||||
### 5. 如果是新增内置工具,遵循工具实现流程
|
||||
|
||||
参考 [desktop-local-tools-implement.mdc](mdc:desktop-local-tools-implement.mdc) 了解更多关于添加内置工具的详细步骤。
|
||||
|
||||
### 6. 添加测试
|
||||
|
||||
1. **单元测试**
|
||||
- 位置:`apps/desktop/src/main/controllers/__tests__/`
|
||||
- 测试主进程组件功能
|
||||
|
||||
2. **集成测试**
|
||||
- 测试 IPC 通信和功能完整流程
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **安全性考虑**
|
||||
- 谨慎处理用户数据和文件系统访问
|
||||
- 适当验证和清理输入数据
|
||||
- 限制暴露给渲染进程的API范围
|
||||
|
||||
2. **性能优化**
|
||||
- 对于耗时操作,考虑使用异步方法
|
||||
- 大型数据传输考虑分批处理
|
||||
|
||||
3. **用户体验**
|
||||
- 为长时间操作添加进度指示
|
||||
- 提供适当的错误反馈
|
||||
- 考虑操作的可撤销性
|
||||
|
||||
4. **代码组织**
|
||||
- 遵循项目现有的命名和代码风格约定
|
||||
- 为新功能添加适当的文档和注释
|
||||
- 功能模块化,避免过度耦合
|
||||
|
||||
## 示例:实现系统通知功能
|
||||
|
||||
```typescript
|
||||
// apps/desktop/src/main/controllers/NotificationCtr.ts
|
||||
import { BrowserWindow, Notification } from 'electron';
|
||||
import { ipcClientEvent } from 'electron-client-ipc';
|
||||
|
||||
interface ShowNotificationParams {
|
||||
title: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export class NotificationCtr {
|
||||
@ipcClientEvent('showNotification')
|
||||
async handleShowNotification({ title, body }: ShowNotificationParams) {
|
||||
try {
|
||||
if (!Notification.isSupported()) {
|
||||
return { success: false, error: 'Notifications not supported' };
|
||||
}
|
||||
|
||||
const notification = new Notification({
|
||||
title,
|
||||
body,
|
||||
});
|
||||
|
||||
notification.show();
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Failed to show notification:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
197
.cursor/rules/desktop-menu-configuration.mdc
Normal file
197
.cursor/rules/desktop-menu-configuration.mdc
Normal file
@@ -0,0 +1,197 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
**桌面端菜单配置指南**
|
||||
|
||||
## 菜单系统概述
|
||||
|
||||
LobeChat 桌面应用有三种主要的菜单类型:
|
||||
|
||||
1. **应用菜单 (App Menu)**:显示在应用窗口顶部(macOS)或窗口标题栏(Windows/Linux)
|
||||
2. **上下文菜单 (Context Menu)**:右键点击时显示的菜单
|
||||
3. **托盘菜单 (Tray Menu)**:点击系统托盘图标显示的菜单
|
||||
|
||||
## 菜单相关文件结构
|
||||
|
||||
```
|
||||
apps/desktop/src/main/
|
||||
├── menus/ # 菜单定义
|
||||
│ ├── appMenu.ts # 应用菜单配置
|
||||
│ ├── contextMenu.ts # 上下文菜单配置
|
||||
│ └── factory.ts # 菜单工厂函数
|
||||
├── controllers/
|
||||
│ ├── MenuCtr.ts # 菜单控制器
|
||||
│ └── TrayMenuCtr.ts # 托盘菜单控制器
|
||||
```
|
||||
|
||||
## 菜单配置流程
|
||||
|
||||
### 1. 应用菜单配置
|
||||
|
||||
应用菜单在 `apps/desktop/src/main/menus/appMenu.ts` 中定义:
|
||||
|
||||
1. **导入依赖**
|
||||
```typescript
|
||||
import { app, BrowserWindow, Menu, MenuItem, MenuItemConstructorOptions } from 'electron';
|
||||
import { is } from 'electron-util';
|
||||
```
|
||||
|
||||
2. **定义菜单项**
|
||||
- 使用 `MenuItemConstructorOptions` 类型定义菜单结构
|
||||
- 每个菜单项可以包含:label, accelerator (快捷键), role, submenu, click 等属性
|
||||
|
||||
3. **创建菜单工厂函数**
|
||||
```typescript
|
||||
export const createAppMenu = (win: BrowserWindow) => {
|
||||
const template = [
|
||||
// 定义菜单项...
|
||||
];
|
||||
|
||||
return Menu.buildFromTemplate(template);
|
||||
};
|
||||
```
|
||||
|
||||
4. **注册菜单**
|
||||
- 在 `MenuCtr.ts` 控制器中使用 `Menu.setApplicationMenu(menu)` 设置应用菜单
|
||||
|
||||
### 2. 上下文菜单配置
|
||||
|
||||
上下文菜单通常在特定元素上右键点击时显示:
|
||||
|
||||
1. **在主进程中定义菜单模板**
|
||||
```typescript
|
||||
// apps/desktop/src/main/menus/contextMenu.ts
|
||||
export const createContextMenu = () => {
|
||||
const template = [
|
||||
// 定义菜单项...
|
||||
];
|
||||
|
||||
return Menu.buildFromTemplate(template);
|
||||
};
|
||||
```
|
||||
|
||||
2. **在适当的事件处理器中显示菜单**
|
||||
```typescript
|
||||
const menu = createContextMenu();
|
||||
menu.popup();
|
||||
```
|
||||
|
||||
### 3. 托盘菜单配置
|
||||
|
||||
托盘菜单在 `TrayMenuCtr.ts` 中配置:
|
||||
|
||||
1. **创建托盘图标**
|
||||
```typescript
|
||||
this.tray = new Tray(trayIconPath);
|
||||
```
|
||||
|
||||
2. **定义托盘菜单**
|
||||
```typescript
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{ label: '显示主窗口', click: this.showMainWindow },
|
||||
{ type: 'separator' },
|
||||
{ label: '退出', click: () => app.quit() },
|
||||
]);
|
||||
```
|
||||
|
||||
3. **设置托盘菜单**
|
||||
```typescript
|
||||
this.tray.setContextMenu(contextMenu);
|
||||
```
|
||||
|
||||
## 多语言支持
|
||||
|
||||
为菜单添加多语言支持:
|
||||
|
||||
1. **导入本地化工具**
|
||||
```typescript
|
||||
import { i18n } from '../locales';
|
||||
```
|
||||
|
||||
2. **使用翻译函数**
|
||||
```typescript
|
||||
const template = [
|
||||
{
|
||||
label: i18n.t('menu.file'),
|
||||
submenu: [
|
||||
{ label: i18n.t('menu.new'), click: createNew },
|
||||
// ...
|
||||
]
|
||||
},
|
||||
// ...
|
||||
];
|
||||
```
|
||||
|
||||
3. **在语言切换时更新菜单**
|
||||
在 `MenuCtr.ts` 中监听语言变化事件并重新创建菜单
|
||||
|
||||
## 添加新菜单项流程
|
||||
|
||||
1. **确定菜单位置**
|
||||
- 决定添加到哪个菜单(应用菜单、上下文菜单或托盘菜单)
|
||||
- 确定在菜单中的位置(主菜单项或子菜单项)
|
||||
|
||||
2. **定义菜单项**
|
||||
```typescript
|
||||
const newMenuItem: MenuItemConstructorOptions = {
|
||||
label: '新功能',
|
||||
accelerator: 'CmdOrCtrl+N',
|
||||
click: (_, window) => {
|
||||
// 处理点击事件
|
||||
if (window) window.webContents.send('trigger-new-feature');
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
3. **添加到菜单模板**
|
||||
将新菜单项添加到相应的菜单模板中
|
||||
|
||||
4. **对于与渲染进程交互的功能**
|
||||
- 使用 `window.webContents.send()` 发送 IPC 消息到渲染进程
|
||||
- 在渲染进程中监听该消息并处理
|
||||
|
||||
## 菜单项启用/禁用控制
|
||||
|
||||
动态控制菜单项状态:
|
||||
|
||||
1. **保存对菜单项的引用**
|
||||
```typescript
|
||||
this.menuItems = {};
|
||||
const menu = Menu.buildFromTemplate(template);
|
||||
this.menuItems.newFeature = menu.getMenuItemById('new-feature');
|
||||
```
|
||||
|
||||
2. **根据条件更新状态**
|
||||
```typescript
|
||||
updateMenuState(state) {
|
||||
if (this.menuItems.newFeature) {
|
||||
this.menuItems.newFeature.enabled = state.canUseNewFeature;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **使用标准角色**
|
||||
- 尽可能使用 Electron 预定义的角色(如 `role: 'copy'`)以获得本地化和一致的行为
|
||||
|
||||
2. **平台特定菜单**
|
||||
- 使用 `process.platform` 检查为不同平台提供不同菜单
|
||||
```typescript
|
||||
if (process.platform === 'darwin') {
|
||||
template.unshift({ role: 'appMenu' });
|
||||
}
|
||||
```
|
||||
|
||||
3. **快捷键冲突**
|
||||
- 避免与系统快捷键冲突
|
||||
- 使用 `CmdOrCtrl` 代替 `Ctrl` 以支持 macOS 和 Windows/Linux
|
||||
|
||||
4. **保持菜单简洁**
|
||||
- 避免过多嵌套的子菜单
|
||||
- 将相关功能分组在一起
|
||||
|
||||
5. **添加分隔符**
|
||||
- 使用 `{ type: 'separator' }` 在逻辑上分隔不同组的菜单项
|
||||
296
.cursor/rules/desktop-window-management.mdc
Normal file
296
.cursor/rules/desktop-window-management.mdc
Normal file
@@ -0,0 +1,296 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
**桌面端窗口管理指南**
|
||||
|
||||
## 窗口管理概述
|
||||
|
||||
LobeChat 桌面应用使用 Electron 的 `BrowserWindow` 管理应用窗口。主要的窗口管理功能包括:
|
||||
|
||||
1. **窗口创建和配置**
|
||||
2. **窗口状态管理**(大小、位置、最大化等)
|
||||
3. **多窗口协调**
|
||||
4. **窗口事件处理**
|
||||
|
||||
## 相关文件结构
|
||||
|
||||
```
|
||||
apps/desktop/src/main/
|
||||
├── appBrowsers.ts # 窗口管理的核心文件
|
||||
├── controllers/
|
||||
│ └── BrowserWindowsCtr.ts # 窗口控制器
|
||||
└── modules/
|
||||
└── browserWindowManager.ts # 窗口管理模块
|
||||
```
|
||||
|
||||
## 窗口管理流程
|
||||
|
||||
### 1. 窗口创建
|
||||
|
||||
在 `appBrowsers.ts` 或 `BrowserWindowsCtr.ts` 中定义窗口创建逻辑:
|
||||
|
||||
```typescript
|
||||
export const createMainWindow = () => {
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
minWidth: 600,
|
||||
minHeight: 400,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, '../preload/index.js'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
},
|
||||
// 其他窗口配置项...
|
||||
});
|
||||
|
||||
// 加载应用内容
|
||||
if (isDev) {
|
||||
mainWindow.loadURL('http://localhost:3000');
|
||||
mainWindow.webContents.openDevTools();
|
||||
} else {
|
||||
mainWindow.loadFile(path.join(__dirname, '../../renderer/index.html'));
|
||||
}
|
||||
|
||||
return mainWindow;
|
||||
};
|
||||
```
|
||||
|
||||
### 2. 窗口状态管理
|
||||
|
||||
实现窗口状态持久化保存和恢复:
|
||||
|
||||
1. **保存窗口状态**
|
||||
```typescript
|
||||
const saveWindowState = (window: BrowserWindow) => {
|
||||
if (!window.isMinimized() && !window.isMaximized()) {
|
||||
const position = window.getPosition();
|
||||
const size = window.getSize();
|
||||
|
||||
settings.set('windowState', {
|
||||
x: position[0],
|
||||
y: position[1],
|
||||
width: size[0],
|
||||
height: size[1],
|
||||
});
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
2. **恢复窗口状态**
|
||||
```typescript
|
||||
const restoreWindowState = (window: BrowserWindow) => {
|
||||
const savedState = settings.get('windowState');
|
||||
|
||||
if (savedState) {
|
||||
window.setBounds({
|
||||
x: savedState.x,
|
||||
y: savedState.y,
|
||||
width: savedState.width,
|
||||
height: savedState.height,
|
||||
});
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
3. **监听窗口事件**
|
||||
```typescript
|
||||
window.on('close', () => saveWindowState(window));
|
||||
window.on('moved', () => saveWindowState(window));
|
||||
window.on('resized', () => saveWindowState(window));
|
||||
```
|
||||
|
||||
### 3. 实现多窗口管理
|
||||
|
||||
对于需要多窗口支持的功能:
|
||||
|
||||
1. **跟踪窗口**
|
||||
```typescript
|
||||
export class WindowManager {
|
||||
private windows: Map<string, BrowserWindow> = new Map();
|
||||
|
||||
createWindow(id: string, options: BrowserWindowConstructorOptions) {
|
||||
const window = new BrowserWindow(options);
|
||||
this.windows.set(id, window);
|
||||
|
||||
window.on('closed', () => {
|
||||
this.windows.delete(id);
|
||||
});
|
||||
|
||||
return window;
|
||||
}
|
||||
|
||||
getWindow(id: string) {
|
||||
return this.windows.get(id);
|
||||
}
|
||||
|
||||
getAllWindows() {
|
||||
return Array.from(this.windows.values());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **窗口间通信**
|
||||
```typescript
|
||||
// 从一个窗口向另一个窗口发送消息
|
||||
sendMessageToWindow(targetWindowId, channel, data) {
|
||||
const targetWindow = this.getWindow(targetWindowId);
|
||||
if (targetWindow) {
|
||||
targetWindow.webContents.send(channel, data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 窗口与渲染进程通信
|
||||
|
||||
通过 IPC 实现窗口操作:
|
||||
|
||||
1. **在主进程中注册 IPC 处理器**
|
||||
```typescript
|
||||
// BrowserWindowsCtr.ts
|
||||
@ipcClientEvent('minimizeWindow')
|
||||
handleMinimizeWindow() {
|
||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
if (focusedWindow) {
|
||||
focusedWindow.minimize();
|
||||
}
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@ipcClientEvent('maximizeWindow')
|
||||
handleMaximizeWindow() {
|
||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
if (focusedWindow) {
|
||||
if (focusedWindow.isMaximized()) {
|
||||
focusedWindow.restore();
|
||||
} else {
|
||||
focusedWindow.maximize();
|
||||
}
|
||||
}
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@ipcClientEvent('closeWindow')
|
||||
handleCloseWindow() {
|
||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
if (focusedWindow) {
|
||||
focusedWindow.close();
|
||||
}
|
||||
return { success: true };
|
||||
}
|
||||
```
|
||||
|
||||
2. **在渲染进程中调用**
|
||||
```typescript
|
||||
// src/services/electron/windowService.ts
|
||||
import { dispatch } from '@lobechat/electron-client-ipc';
|
||||
|
||||
export const windowService = {
|
||||
minimize: () => dispatch('minimizeWindow'),
|
||||
maximize: () => dispatch('maximizeWindow'),
|
||||
close: () => dispatch('closeWindow'),
|
||||
};
|
||||
```
|
||||
|
||||
### 5. 自定义窗口控制 (无边框窗口)
|
||||
|
||||
对于自定义窗口标题栏:
|
||||
|
||||
1. **创建无边框窗口**
|
||||
```typescript
|
||||
const window = new BrowserWindow({
|
||||
frame: false,
|
||||
titleBarStyle: 'hidden',
|
||||
// 其他选项...
|
||||
});
|
||||
```
|
||||
|
||||
2. **在渲染进程中实现拖拽区域**
|
||||
```css
|
||||
/* CSS */
|
||||
.titlebar {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.titlebar-button {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **性能考虑**
|
||||
- 避免创建过多窗口
|
||||
- 使用 `show: false` 创建窗口,在内容加载完成后再显示,避免白屏
|
||||
|
||||
2. **安全性**
|
||||
- 始终设置适当的 `webPreferences` 确保安全
|
||||
```typescript
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, '../preload/index.js'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
sandbox: true,
|
||||
}
|
||||
```
|
||||
|
||||
3. **跨平台兼容性**
|
||||
- 考虑不同操作系统的窗口行为差异
|
||||
- 使用 `process.platform` 为不同平台提供特定实现
|
||||
|
||||
4. **崩溃恢复**
|
||||
- 监听 `webContents.on('crashed')` 事件处理崩溃
|
||||
- 提供崩溃恢复选项
|
||||
|
||||
5. **内存管理**
|
||||
- 确保窗口关闭时清理所有相关资源
|
||||
- 使用 `window.on('closed')` 而不是 `window.on('close')` 进行最终清理
|
||||
|
||||
## 示例:创建设置窗口
|
||||
|
||||
```typescript
|
||||
// apps/desktop/src/main/controllers/BrowserWindowsCtr.ts
|
||||
|
||||
@ipcClientEvent('openSettings')
|
||||
handleOpenSettings() {
|
||||
// 检查设置窗口是否已经存在
|
||||
if (this.settingsWindow && !this.settingsWindow.isDestroyed()) {
|
||||
// 如果窗口已存在,将其置于前台
|
||||
this.settingsWindow.focus();
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// 创建新窗口
|
||||
this.settingsWindow = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600,
|
||||
title: 'Settings',
|
||||
parent: this.mainWindow, // 设置父窗口,使其成为模态窗口
|
||||
modal: true,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, '../preload/index.js'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
},
|
||||
});
|
||||
|
||||
// 加载设置页面
|
||||
if (isDev) {
|
||||
this.settingsWindow.loadURL('http://localhost:3000/settings');
|
||||
} else {
|
||||
this.settingsWindow.loadFile(
|
||||
path.join(__dirname, '../../renderer/index.html'),
|
||||
{ hash: 'settings' }
|
||||
);
|
||||
}
|
||||
|
||||
// 监听窗口关闭事件
|
||||
this.settingsWindow.on('closed', () => {
|
||||
this.settingsWindow = null;
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
```
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ElectronAppState } from '@lobechat/electron-client-ipc';
|
||||
import { ElectronAppState, ThemeMode } from '@lobechat/electron-client-ipc';
|
||||
import { app, shell, systemPreferences } from 'electron';
|
||||
import { macOS } from 'electron-is';
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
@@ -68,6 +68,11 @@ export default class SystemController extends ControllerModule {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@ipcClientEvent('updateThemeMode')
|
||||
async updateThemeModeHandler(themeMode: ThemeMode) {
|
||||
this.app.browserManager.broadcastToAllWindows('themeChanged', { themeMode });
|
||||
}
|
||||
|
||||
@ipcServerEvent('getDatabasePath')
|
||||
async getDatabasePath() {
|
||||
return join(this.app.appStoragePath, LOCAL_DATABASE_DIR);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { LocalFilesDispatchEvents } from './localFile';
|
||||
import { MenuDispatchEvents } from './menu';
|
||||
import { RemoteServerBroadcastEvents, RemoteServerDispatchEvents } from './remoteServer';
|
||||
import { ShortcutDispatchEvents } from './shortcut';
|
||||
import { SystemDispatchEvents } from './system';
|
||||
import { SystemBroadcastEvents, SystemDispatchEvents } from './system';
|
||||
import { TrayDispatchEvents } from './tray';
|
||||
import { AutoUpdateBroadcastEvents, AutoUpdateDispatchEvents } from './update';
|
||||
import { UploadFilesDispatchEvents } from './upload';
|
||||
@@ -35,7 +35,8 @@ export type ClientEventReturnType<T extends ClientDispatchEventKey> = ReturnType
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface MainBroadcastEvents
|
||||
extends AutoUpdateBroadcastEvents,
|
||||
RemoteServerBroadcastEvents {}
|
||||
RemoteServerBroadcastEvents,
|
||||
SystemBroadcastEvents {}
|
||||
|
||||
export type MainBroadcastEventKey = keyof MainBroadcastEvents;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ElectronAppState } from '../types';
|
||||
import { ElectronAppState, ThemeMode } from '../types';
|
||||
|
||||
export interface SystemDispatchEvents {
|
||||
checkSystemAccessibility: () => boolean | undefined;
|
||||
@@ -12,4 +12,9 @@ export interface SystemDispatchEvents {
|
||||
* @param locale 语言设置
|
||||
*/
|
||||
updateLocale: (locale: string) => { success: boolean };
|
||||
updateThemeMode: (themeMode: ThemeMode) => void;
|
||||
}
|
||||
|
||||
export interface SystemBroadcastEvents {
|
||||
themeChanged: (data: { themeMode: ThemeMode }) => void;
|
||||
}
|
||||
|
||||
@@ -22,3 +22,5 @@ export interface UserPathData {
|
||||
userData: string;
|
||||
videos?: string; // User's home directory
|
||||
}
|
||||
|
||||
export type ThemeMode = 'auto' | 'dark' | 'light';
|
||||
|
||||
21
src/features/ElectronTitlebar/hooks/useWatchThemeUpdate.ts
Normal file
21
src/features/ElectronTitlebar/hooks/useWatchThemeUpdate.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { useWatchBroadcast } from '@lobechat/electron-client-ipc';
|
||||
import { useTheme } from 'antd-style';
|
||||
import { rgba } from 'polished';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
|
||||
export const useWatchThemeUpdate = () => {
|
||||
const switchThemeMode = useGlobalStore((s) => s.switchThemeMode);
|
||||
|
||||
const token = useTheme();
|
||||
|
||||
useWatchBroadcast('themeChanged', ({ themeMode }) => {
|
||||
switchThemeMode(themeMode, { skipBroadcast: true });
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.style.background = 'none';
|
||||
document.body.style.background = rgba(token.colorBgLayout, 0.66);
|
||||
}, [token]);
|
||||
};
|
||||
@@ -11,6 +11,7 @@ import { UpdateModal } from './UpdateModal';
|
||||
import { UpdateNotification } from './UpdateNotification';
|
||||
import WinControl from './WinControl';
|
||||
import { TITLE_BAR_HEIGHT } from './const';
|
||||
import { useWatchThemeUpdate } from './hooks/useWatchThemeUpdate';
|
||||
|
||||
const isMac = isMacOS();
|
||||
|
||||
@@ -21,6 +22,7 @@ const TitleBar = memo(() => {
|
||||
]);
|
||||
|
||||
initElectronAppState();
|
||||
useWatchThemeUpdate();
|
||||
|
||||
const showWinControl = isAppStateInit && !isMac;
|
||||
return (
|
||||
|
||||
@@ -21,7 +21,7 @@ const n = setNamespace('g');
|
||||
|
||||
export interface GlobalGeneralAction {
|
||||
switchLocale: (locale: LocaleMode) => void;
|
||||
switchThemeMode: (themeMode: ThemeMode) => void;
|
||||
switchThemeMode: (themeMode: ThemeMode, params?: { skipBroadcast?: boolean }) => void;
|
||||
updateSystemStatus: (status: Partial<SystemStatus>, action?: any) => void;
|
||||
useCheckLatestVersion: (enabledCheck?: boolean) => SWRResponse<string>;
|
||||
useInitSystemStatus: () => SWRResponse;
|
||||
@@ -50,10 +50,21 @@ export const generalActionSlice: StateCreator<
|
||||
})();
|
||||
}
|
||||
},
|
||||
switchThemeMode: (themeMode) => {
|
||||
switchThemeMode: (themeMode, { skipBroadcast } = {}) => {
|
||||
get().updateSystemStatus({ themeMode });
|
||||
|
||||
setCookie(LOBE_THEME_APPEARANCE, themeMode === 'auto' ? undefined : themeMode);
|
||||
|
||||
if (isDesktop && !skipBroadcast) {
|
||||
(async () => {
|
||||
try {
|
||||
const { dispatch } = await import('@lobechat/electron-client-ipc');
|
||||
await dispatch('updateThemeMode', themeMode);
|
||||
} catch (error) {
|
||||
console.error('Failed to update theme in main process:', error);
|
||||
}
|
||||
})();
|
||||
}
|
||||
},
|
||||
updateSystemStatus: (status, action) => {
|
||||
if (!get().isStatusInit) return;
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { Theme, css } from 'antd-style';
|
||||
import { rgba } from 'polished';
|
||||
|
||||
import { isDesktop } from '@/const/version';
|
||||
|
||||
// fix ios input keyboard
|
||||
// overflow: hidden;
|
||||
@@ -25,15 +22,10 @@ export default ({ token }: { prefixCls: string; token: Theme }) => css`
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
background: ${isDesktop ? 'none' : token.colorBgLayout};
|
||||
}
|
||||
|
||||
body {
|
||||
/* 提高合成层级,强制硬件加速,否则会有渲染黑边出现 */
|
||||
will-change: opacity;
|
||||
transform: translateZ(0);
|
||||
background: ${isDesktop ? rgba(token.colorBgLayout, 0.66) : token.colorBgLayout};
|
||||
}
|
||||
|
||||
* {
|
||||
|
||||
Reference in New Issue
Block a user