💄 style: fix desktop titlebar style in window (#8439)

* 💄 style: Fix win electron style

📝 docs: Update readme

💄 style: Update useWatchThemeUpdate

💄 style: Update Tray icon

🔧 chore: Update windows

🔧 chore: Update filetree

🔧 chore: Update core

💄 style: Fix desktop draw style

💄 style: Update style

💄 style: Fix backgroundColor

💄 style: Update titlebar style

* 💄 style: Fix windows icon

* 🔧 chore: Clean

* update theme

* 💄 style: Update broswer

* 💄 style: HandleAppThemeChange

* clean

* fix memory leak

---------

Co-authored-by: arvinxx <arvinx@foxmail.com>
This commit is contained in:
CanisMinor
2025-07-24 11:02:07 +08:00
committed by GitHub
parent 66dbb246d9
commit fd7662c3ac
30 changed files with 367 additions and 286 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 171 KiB

View File

@@ -11,8 +11,9 @@ console.log(`[electron-vite.config.ts] Detected UPDATE_CHANNEL: ${updateChannel}
export default defineConfig({
main: {
build: {
minify: !isDev,
outDir: 'dist/main',
sourcemap: isDev,
sourcemap: isDev ? 'inline' : false,
},
// 这里是关键:在构建时进行文本替换
define: {
@@ -30,8 +31,9 @@ export default defineConfig({
},
preload: {
build: {
minify: !isDev,
outDir: 'dist/preload',
sourcemap: isDev,
sourcemap: isDev ? 'inline' : false,
},
plugins: [externalizeDepsPlugin({})],
resolve: {

View File

@@ -31,6 +31,7 @@
},
"dependencies": {
"electron-updater": "^6.6.2",
"electron-window-state": "^5.0.3",
"get-port-please": "^3.1.2",
"pdfjs-dist": "4.10.38"
},

View File

@@ -1,4 +1,4 @@
import type { BrowserWindowOpts } from './core/Browser';
import type { BrowserWindowOpts } from './core/browser/Browser';
export const BrowsersIdentifiers = {
chat: 'chat',

View File

@@ -1,12 +1,13 @@
import { dev, linux, macOS, windows } from 'electron-is';
import os from 'node:os';
export const isDev = process.env.NODE_ENV === 'development';
export const isDev = dev();
export const OFFICIAL_CLOUD_SERVER = process.env.OFFICIAL_CLOUD_SERVER || 'https://lobechat.com';
export const isMac = process.platform === 'darwin';
export const isWindows = process.platform === 'win32';
export const isLinux = process.platform === 'linux';
export const isMac = macOS();
export const isWindows = windows();
export const isLinux = linux();
function getIsWindows11() {
if (!isWindows) return false;

View File

@@ -31,4 +31,5 @@ export const STORE_DEFAULTS: ElectronMainStore = {
networkProxy: defaultProxySettings,
shortcuts: DEFAULT_SHORTCUTS_CONFIG,
storagePath: appStorageDir,
themeMode: 'auto',
};

View File

@@ -0,0 +1,11 @@
// Theme colors
export const BACKGROUND_DARK = '#000';
export const BACKGROUND_LIGHT = '#f8f8f8';
export const SYMBOL_COLOR_DARK = '#ffffff80';
export const SYMBOL_COLOR_LIGHT = '#00000080';
// Window dimensions and constraints
export const TITLE_BAR_HEIGHT = 29;
// Default window configuration
export const THEME_CHANGE_DELAY = 100;

View File

@@ -77,10 +77,8 @@ export default class NotificationCtr extends ControllerModule {
const notification = new Notification({
body: params.body,
// 添加更多配置以确保通知能正常显示
hasReply: false,
silent: params.silent || false,
hasReply: false,
silent: params.silent || false,
timeoutType: 'default',
title: params.title,
urgency: 'normal',

View File

@@ -83,7 +83,11 @@ export default class SystemController extends ControllerModule {
@ipcClientEvent('updateThemeMode')
async updateThemeModeHandler(themeMode: ThemeMode) {
this.app.storeManager.set('themeMode', themeMode);
this.app.browserManager.broadcastToAllWindows('themeChanged', { themeMode });
// Apply visual effects to all browser windows when theme mode changes
this.app.browserManager.handleAppThemeChange();
}
@ipcServerEvent('getDatabasePath')

View File

@@ -2,7 +2,7 @@ import type { ClientDispatchEvents } from '@lobechat/electron-client-ipc';
import type { ServerDispatchEvents } from '@lobechat/electron-server-ipc';
import type { App } from '@/core/App';
import { IoCContainer } from '@/core/IoCContainer';
import { IoCContainer } from '@/core/infrastructure/IoCContainer';
import { ShortcutActionType } from '@/shortcuts';
const ipcDecorator =

View File

@@ -9,20 +9,19 @@ import { buildDir, nextStandaloneDir } from '@/const/dir';
import { isDev } from '@/const/env';
import { IControlModule } from '@/controllers';
import { IServiceModule } from '@/services';
import FileService from '@/services/fileSrv';
import { IpcClientEventSender } from '@/types/ipcClientEvent';
import { createLogger } from '@/utils/logger';
import { CustomRequestHandler, createHandler } from '@/utils/next-electron-rsc';
import BrowserManager from './BrowserManager';
import { I18nManager } from './I18nManager';
import { IoCContainer } from './IoCContainer';
import MenuManager from './MenuManager';
import { ShortcutManager } from './ShortcutManager';
import { StaticFileServerManager } from './StaticFileServerManager';
import { StoreManager } from './StoreManager';
import TrayManager from './TrayManager';
import { UpdaterManager } from './UpdaterManager';
import { BrowserManager } from './browser/BrowserManager';
import { I18nManager } from './infrastructure/I18nManager';
import { IoCContainer } from './infrastructure/IoCContainer';
import { StaticFileServerManager } from './infrastructure/StaticFileServerManager';
import { StoreManager } from './infrastructure/StoreManager';
import { UpdaterManager } from './infrastructure/UpdaterManager';
import { MenuManager } from './ui/MenuManager';
import { ShortcutManager } from './ui/ShortcutManager';
import { TrayManager } from './ui/TrayManager';
const logger = createLogger('core:App');

View File

@@ -6,13 +6,21 @@ import {
nativeTheme,
screen,
} from 'electron';
import os from 'node:os';
import { join } from 'node:path';
import { buildDir, preloadDir, resourcesDir } from '@/const/dir';
import { isDev, isWindows } from '@/const/env';
import {
BACKGROUND_DARK,
BACKGROUND_LIGHT,
SYMBOL_COLOR_DARK,
SYMBOL_COLOR_LIGHT,
THEME_CHANGE_DELAY,
TITLE_BAR_HEIGHT,
} from '@/const/theme';
import { createLogger } from '@/utils/logger';
import { preloadDir, resourcesDir } from '../const/dir';
import type { App } from './App';
import type { App } from '../App';
// Create logger
const logger = createLogger('core:Browser');
@@ -20,9 +28,6 @@ const logger = createLogger('core:Browser');
export interface BrowserWindowOpts extends BrowserWindowConstructorOptions {
devTools?: boolean;
height?: number;
/**
* URL
*/
identifier: string;
keepAlive?: boolean;
parentIdentifier?: string;
@@ -34,38 +39,18 @@ export interface BrowserWindowOpts extends BrowserWindowConstructorOptions {
export default class Browser {
private app: App;
/**
* Internal electron window
*/
private _browserWindow?: BrowserWindow;
private themeListenerSetup = false;
private stopInterceptHandler;
/**
* Identifier
*/
identifier: string;
/**
* Options at creation
*/
options: BrowserWindowOpts;
/**
* Key for storing window state in storeManager
*/
private readonly windowStateKey: string;
/**
* Method to expose window externally
*/
get browserWindow() {
return this.retrieveOrInitialize();
}
get webContents() {
if (this._browserWindow.isDestroyed()) return null;
return this._browserWindow.webContents;
}
@@ -86,6 +71,101 @@ export default class Browser {
this.retrieveOrInitialize();
}
/**
* Get platform-specific theme configuration for window creation
*/
private getPlatformThemeConfig(isDarkMode?: boolean): Record<string, any> {
const darkMode = isDarkMode ?? nativeTheme.shouldUseDarkColors;
if (isWindows) {
return this.getWindowsThemeConfig(darkMode);
}
return {};
}
/**
* Get Windows-specific theme configuration
*/
private getWindowsThemeConfig(isDarkMode: boolean) {
return {
backgroundColor: isDarkMode ? BACKGROUND_DARK : BACKGROUND_LIGHT,
icon: isDev ? join(buildDir, 'icon-dev.ico') : undefined,
titleBarOverlay: {
color: isDarkMode ? BACKGROUND_DARK : BACKGROUND_LIGHT,
height: TITLE_BAR_HEIGHT,
symbolColor: isDarkMode ? SYMBOL_COLOR_DARK : SYMBOL_COLOR_LIGHT,
},
titleBarStyle: 'hidden' as const,
};
}
private setupThemeListener(): void {
if (this.themeListenerSetup) return;
nativeTheme.on('updated', this.handleThemeChange);
this.themeListenerSetup = true;
}
private handleThemeChange = (): void => {
logger.debug(`[${this.identifier}] System theme changed, reapplying visual effects.`);
setTimeout(() => {
this.applyVisualEffects();
}, THEME_CHANGE_DELAY);
};
/**
* Handle application theme mode change (called from BrowserManager)
*/
handleAppThemeChange = (): void => {
logger.debug(`[${this.identifier}] App theme mode changed, reapplying visual effects.`);
setTimeout(() => {
this.applyVisualEffects();
}, THEME_CHANGE_DELAY);
};
private applyVisualEffects(): void {
if (!this._browserWindow || this._browserWindow.isDestroyed()) return;
logger.debug(`[${this.identifier}] Applying visual effects for platform`);
const isDarkMode = this.isDarkMode;
try {
if (isWindows) {
this.applyWindowsVisualEffects(isDarkMode);
}
logger.debug(
`[${this.identifier}] Visual effects applied successfully (dark mode: ${isDarkMode})`,
);
} catch (error) {
logger.error(`[${this.identifier}] Failed to apply visual effects:`, error);
}
}
private applyWindowsVisualEffects(isDarkMode: boolean): void {
const config = this.getWindowsThemeConfig(isDarkMode);
this._browserWindow.setBackgroundColor(config.backgroundColor);
this._browserWindow.setTitleBarOverlay(config.titleBarOverlay);
}
private cleanupThemeListener(): void {
if (this.themeListenerSetup) {
// Note: nativeTheme listeners are global, consider using a centralized theme manager
nativeTheme.off('updated', this.handleThemeChange);
// for multiple windows to avoid duplicate listeners
this.themeListenerSetup = false;
}
}
private get isDarkMode() {
const themeMode = this.app.storeManager.get('themeMode');
if (themeMode === 'auto') return nativeTheme.shouldUseDarkColors;
return themeMode === 'dark';
}
loadUrl = async (path: string) => {
const initUrl = this.app.nextServerUrl + path;
@@ -203,6 +283,7 @@ export default class Browser {
destroy() {
logger.debug(`Destroying window instance: ${this.identifier}`);
this.stopInterceptHandler?.();
this.cleanupThemeListener();
this._browserWindow = undefined;
}
@@ -228,45 +309,37 @@ export default class Browser {
`[${this.identifier}] Saved window state (only size used): ${JSON.stringify(savedState)}`,
);
const { isWindows11, isWindows } = this.getWindowsVersion();
const isDarkMode = nativeTheme.shouldUseDarkColors;
const browserWindow = new BrowserWindow({
...res,
...(isWindows
? {
titleBarStyle: 'hidden',
}
: {}),
...(isWindows11
? {
backgroundMaterial: isDarkMode ? 'mica' : 'acrylic',
vibrancy: 'under-window',
visualEffectState: 'active',
}
: {}),
autoHideMenuBar: true,
backgroundColor: '#00000000',
darkTheme: isDarkMode,
frame: false,
height: savedState?.height || height,
// Always create hidden first
show: false,
title,
vibrancy: 'sidebar',
visualEffectState: 'active',
webPreferences: {
// Context isolation environment
// https://www.electronjs.org/docs/tutorial/context-isolation
backgroundThrottling: false,
contextIsolation: true,
preload: join(preloadDir, 'index.js'),
},
width: savedState?.width || width,
...this.getPlatformThemeConfig(isDarkMode),
});
this._browserWindow = browserWindow;
logger.debug(`[${this.identifier}] BrowserWindow instance created.`);
if (isWindows11) this.applyVisualEffects();
// Initialize theme listener for this window to handle theme changes
this.setupThemeListener();
logger.debug(`[${this.identifier}] Theme listener setup and applying initial visual effects.`);
// Apply initial visual effects
this.applyVisualEffects();
logger.debug(`[${this.identifier}] Setting up nextInterceptor.`);
this.stopInterceptHandler = this.app.nextInterceptor({
@@ -320,8 +393,9 @@ export default class Browser {
} catch (error) {
logger.error(`[${this.identifier}] Failed to save window state on quit:`, error);
}
// Need to clean up intercept handler
// Need to clean up intercept handler and theme manager
this.stopInterceptHandler?.();
this.cleanupThemeListener();
return;
}
@@ -355,8 +429,9 @@ export default class Browser {
} catch (error) {
logger.error(`[${this.identifier}] Failed to save window state on close:`, error);
}
// Need to clean up intercept handler
// Need to clean up intercept handler and theme manager
this.stopInterceptHandler?.();
this.cleanupThemeListener();
}
});
@@ -387,16 +462,6 @@ export default class Browser {
this._browserWindow.webContents.send(channel, data);
};
applyVisualEffects() {
// Windows 11 can use this new API
if (this._browserWindow) {
logger.debug(`[${this.identifier}] Setting window background material for Windows 11`);
const isDarkMode = nativeTheme.shouldUseDarkColors;
this._browserWindow?.setBackgroundMaterial(isDarkMode ? 'mica' : 'acrylic');
this._browserWindow?.setVibrancy('under-window');
}
}
toggleVisible() {
logger.debug(`Toggling visibility for window: ${this.identifier}`);
if (this._browserWindow.isVisible() && this._browserWindow.isFocused()) {
@@ -407,35 +472,11 @@ export default class Browser {
}
}
getWindowsVersion() {
if (process.platform !== 'win32') {
return {
isWindows: false,
isWindows10: false,
isWindows11: false,
version: null,
};
}
// 获取操作系统版本(如 "10.0.22621"
const release = os.release();
const parts = release.split('.');
// 主版本和次版本
const majorVersion = parseInt(parts[0], 10);
const minorVersion = parseInt(parts[1], 10);
// 构建号是第三部分
const buildNumber = parseInt(parts[2], 10);
// Windows 11 的构建号从 22000 开始
const isWindows11 = majorVersion === 10 && minorVersion === 0 && buildNumber >= 22_000;
return {
buildNumber,
isWindows: true,
isWindows11,
version: release,
};
/**
* Manually reapply visual effects (useful for fixing lost effects after window state changes)
*/
reapplyVisualEffects(): void {
logger.debug(`[${this.identifier}] Manually reapplying visual effects via Browser.`);
this.applyVisualEffects();
}
}

View File

@@ -3,15 +3,15 @@ import { WebContents } from 'electron';
import { createLogger } from '@/utils/logger';
import { AppBrowsersIdentifiers, appBrowsers } from '../appBrowsers';
import type { App } from './App';
import { AppBrowsersIdentifiers, appBrowsers } from '../../appBrowsers';
import type { App } from '../App';
import type { BrowserWindowOpts } from './Browser';
import Browser from './Browser';
// Create logger
const logger = createLogger('core:BrowserManager');
export default class BrowserManager {
export class BrowserManager {
app: App;
browsers: Map<AppBrowsersIdentifiers, Browser> = new Map();
@@ -194,4 +194,14 @@ export default class BrowserManager {
getIdentifierByWebContents(webContents: WebContents): AppBrowsersIdentifiers | null {
return this.webContentsMap.get(webContents) || null;
}
/**
* Handle application theme mode changes and reapply visual effects to all windows
*/
handleAppThemeChange(): void {
logger.debug('Handling app theme change for all browser windows');
this.browsers.forEach((browser) => {
browser.handleAppThemeChange();
});
}
}

View File

@@ -5,7 +5,7 @@ import { LOCAL_STORAGE_URL_PREFIX } from '@/const/dir';
import FileService from '@/services/fileSrv';
import { createLogger } from '@/utils/logger';
import type { App } from './App';
import type { App } from '../App';
const logger = createLogger('core:StaticFileServerManager');
@@ -54,9 +54,12 @@ export class StaticFileServerManager {
try {
// 使用 get-port-please 获取可用端口
this.serverPort = await getPort({
port: 33250, // 首选端口
ports: [33251, 33252, 33253, 33254, 33255], // 备用端口
// 备用端口
host: '127.0.0.1',
port: 33_250,
// 首选端口
ports: [33_251, 33_252, 33_253, 33_254, 33_255],
});
logger.debug(`Found available port: ${this.serverPort}`);
@@ -64,7 +67,7 @@ export class StaticFileServerManager {
return new Promise((resolve, reject) => {
const server = createServer(async (req, res) => {
// 设置请求超时
req.setTimeout(30000, () => {
req.setTimeout(30_000, () => {
logger.warn('Request timeout, closing connection');
if (!res.destroyed && !res.headersSent) {
res.writeHead(408, { 'Content-Type': 'text/plain' });
@@ -155,10 +158,13 @@ export class StaticFileServerManager {
// 设置响应头
res.writeHead(200, {
'Content-Type': fileResult.mimeType,
'Cache-Control': 'public, max-age=31536000', // 缓存一年
'Access-Control-Allow-Origin': 'http://localhost:*', // 允许 localhost 的任意端口
// 缓存一年
'Access-Control-Allow-Origin': 'http://localhost:*',
'Cache-Control': 'public, max-age=31536000',
// 允许 localhost 的任意端口
'Content-Length': Buffer.byteLength(fileResult.content),
'Content-Type': fileResult.mimeType,
});
// 发送文件内容

View File

@@ -5,7 +5,7 @@ import { ElectronMainStore, StoreKey } from '@/types/store';
import { makeSureDirExist } from '@/utils/file-system';
import { createLogger } from '@/utils/logger';
import { App } from './App';
import { App } from '../App';
// Create logger
const logger = createLogger('core:StoreManager');

View File

@@ -5,7 +5,7 @@ import { isDev } from '@/const/env';
import { UPDATE_CHANNEL as channel, updaterConfig } from '@/modules/updater/configs';
import { createLogger } from '@/utils/logger';
import type { App as AppCore } from './App';
import type { App as AppCore } from '../App';
// Create logger
const logger = createLogger('core:UpdaterManager');

View File

@@ -3,12 +3,12 @@ import { Menu } from 'electron';
import { IMenuPlatform, MenuOptions, createMenuImpl } from '@/menus';
import { createLogger } from '@/utils/logger';
import type { App } from './App';
import type { App } from '../App';
// Create logger
const logger = createLogger('core:MenuManager');
export default class MenuManager {
export class MenuManager {
app: App;
private platformImpl: IMenuPlatform;

View File

@@ -3,7 +3,7 @@ import { globalShortcut } from 'electron';
import { DEFAULT_SHORTCUTS_CONFIG } from '@/shortcuts';
import { createLogger } from '@/utils/logger';
import type { App } from './App';
import type { App } from '../App';
// Create logger
const logger = createLogger('core:ShortcutManager');

View File

@@ -12,157 +12,157 @@ import { join } from 'node:path';
import { resourcesDir } from '@/const/dir';
import { createLogger } from '@/utils/logger';
import type { App } from './App';
import type { App } from '../App';
// 创建日志记录器
// Create logger
const logger = createLogger('core:Tray');
export interface TrayOptions {
/**
*
* Tray icon path (relative to resource directory)
*/
iconPath: string;
/**
*
* Tray identifier
*/
identifier: string;
/**
*
* Tray tooltip text
*/
tooltip?: string;
}
export default class Tray {
export class Tray {
private app: App;
/**
* Electron
* Internal Electron tray
*/
private _tray?: ElectronTray;
/**
*
* Identifier
*/
identifier: string;
/**
*
* Options when created
*/
options: TrayOptions;
/**
*
* Get tray instance
*/
get tray() {
return this.retrieveOrInitialize();
}
/**
*
* @param options
* @param application
* Construct tray object
* @param options Tray options
* @param application App instance
*/
constructor(options: TrayOptions, application: App) {
logger.debug(`创建托盘实例: ${options.identifier}`);
logger.debug(`托盘选项: ${JSON.stringify(options)}`);
logger.debug(`Creating tray instance: ${options.identifier}`);
logger.debug(`Tray options: ${JSON.stringify(options)}`);
this.app = application;
this.identifier = options.identifier;
this.options = options;
// 初始化
// Initialize
this.retrieveOrInitialize();
}
/**
*
* Initialize tray
*/
retrieveOrInitialize() {
// 如果托盘已存在且未被销毁,则返回
// If tray already exists and is not destroyed, return it
if (this._tray) {
logger.debug(`[${this.identifier}] 返回现有托盘实例`);
logger.debug(`[${this.identifier}] Returning existing tray instance`);
return this._tray;
}
const { iconPath, tooltip } = this.options;
// 加载托盘图标
logger.info(`创建新的托盘实例: ${this.identifier}`);
// Load tray icon
logger.info(`Creating new tray instance: ${this.identifier}`);
const iconFile = join(resourcesDir, iconPath);
logger.debug(`[${this.identifier}] 加载图标: ${iconFile}`);
logger.debug(`[${this.identifier}] Loading icon: ${iconFile}`);
try {
const icon = nativeImage.createFromPath(iconFile);
this._tray = new ElectronTray(icon);
// 设置工具提示
// Set tooltip
if (tooltip) {
logger.debug(`[${this.identifier}] 设置提示文本: ${tooltip}`);
logger.debug(`[${this.identifier}] Setting tooltip: ${tooltip}`);
this._tray.setToolTip(tooltip);
}
// 设置默认上下文菜单
// Set default context menu
this.setContextMenu();
// 设置点击事件
// Set click event
this._tray.on('click', () => {
logger.debug(`[${this.identifier}] 托盘被点击`);
logger.debug(`[${this.identifier}] Tray clicked`);
this.onClick();
});
logger.debug(`[${this.identifier}] 托盘实例创建完成`);
logger.debug(`[${this.identifier}] Tray instance created successfully`);
return this._tray;
} catch (error) {
logger.error(`[${this.identifier}] 创建托盘失败:`, error);
logger.error(`[${this.identifier}] Failed to create tray:`, error);
throw error;
}
}
/**
*
* @param template 使
* Set tray context menu
* @param template Menu template, if not provided default template will be used
*/
setContextMenu(template?: MenuItemConstructorOptions[]) {
logger.debug(`[${this.identifier}] 设置托盘上下文菜单`);
logger.debug(`[${this.identifier}] Setting tray context menu`);
// 如果未提供模板,使用默认菜单
// If no template provided, use default menu
const defaultTemplate: MenuItemConstructorOptions[] = template || [
{
click: () => {
logger.debug(`[${this.identifier}] 菜单项 "显示主窗口" 被点击`);
logger.debug(`[${this.identifier}] Menu item "Show Main Window" clicked`);
this.app.browserManager.showMainWindow();
},
label: '显示主窗口',
label: 'Show Main Window',
},
{ type: 'separator' },
{
click: () => {
logger.debug(`[${this.identifier}] 菜单项 "退出" 被点击`);
logger.debug(`[${this.identifier}] Menu item "Quit" clicked`);
app.quit();
},
label: '退出',
label: 'Quit',
},
];
const contextMenu = Menu.buildFromTemplate(defaultTemplate);
this._tray?.setContextMenu(contextMenu);
logger.debug(`[${this.identifier}] 托盘上下文菜单已设置`);
logger.debug(`[${this.identifier}] Tray context menu has been set`);
}
/**
*
* Handle tray click event
*/
onClick() {
logger.debug(`[${this.identifier}] 处理托盘点击事件`);
logger.debug(`[${this.identifier}] Handling tray click event`);
const mainWindow = this.app.browserManager.getMainWindow();
if (mainWindow) {
if (mainWindow.browserWindow.isVisible() && mainWindow.browserWindow.isFocused()) {
logger.debug(`[${this.identifier}] 主窗口已可见且聚焦,现在隐藏它`);
logger.debug(`[${this.identifier}] Main window is visible and focused, hiding it now`);
mainWindow.hide();
} else {
logger.debug(`[${this.identifier}] 显示并聚焦主窗口`);
logger.debug(`[${this.identifier}] Showing and focusing main window`);
mainWindow.show();
mainWindow.browserWindow.focus();
}
@@ -170,59 +170,61 @@ export default class Tray {
}
/**
*
* @param iconPath
* Update tray icon
* @param iconPath New icon path (relative to resource directory)
*/
updateIcon(iconPath: string) {
logger.debug(`[${this.identifier}] 更新图标: ${iconPath}`);
logger.debug(`[${this.identifier}] Updating icon: ${iconPath}`);
try {
const iconFile = join(resourcesDir, iconPath);
const icon = nativeImage.createFromPath(iconFile);
this._tray?.setImage(icon);
this.options.iconPath = iconPath;
logger.debug(`[${this.identifier}] 图标已更新`);
logger.debug(`[${this.identifier}] Icon updated successfully`);
} catch (error) {
logger.error(`[${this.identifier}] 更新图标失败:`, error);
logger.error(`[${this.identifier}] Failed to update icon:`, error);
}
}
/**
*
* @param tooltip
* Update tooltip text
* @param tooltip New tooltip text
*/
updateTooltip(tooltip: string) {
logger.debug(`[${this.identifier}] 更新提示文本: ${tooltip}`);
logger.debug(`[${this.identifier}] Updating tooltip: ${tooltip}`);
this._tray?.setToolTip(tooltip);
this.options.tooltip = tooltip;
}
/**
* Windows
* @param options
* Display balloon notification (only supported on Windows)
* @param options Balloon options
*/
displayBalloon(options: DisplayBalloonOptions) {
if (process.platform === 'win32' && this._tray) {
logger.debug(`[${this.identifier}] 显示气泡通知: ${JSON.stringify(options)}`);
logger.debug(
`[${this.identifier}] Displaying balloon notification: ${JSON.stringify(options)}`,
);
this._tray.displayBalloon(options);
} else {
logger.debug(`[${this.identifier}] 气泡通知仅在 Windows 上支持`);
logger.debug(`[${this.identifier}] Balloon notification is only supported on Windows`);
}
}
/**
* 广
* Broadcast event
*/
broadcast = <T extends MainBroadcastEventKey>(channel: T, data?: MainBroadcastParams<T>) => {
logger.debug(`向托盘 ${this.identifier} 广播, 频道: ${channel}`);
// 可以通过 App 实例的 browserManager 将消息转发到主窗口
logger.debug(`Broadcasting to tray ${this.identifier}, channel: ${channel}`);
// Can forward message to main window through App instance's browserManager
this.app.browserManager.getMainWindow()?.broadcast(channel, data);
};
/**
*
* Destroy tray instance
*/
destroy() {
logger.debug(`销毁托盘实例: ${this.identifier}`);
logger.debug(`Destroying tray instance: ${this.identifier}`);
if (this._tray) {
this._tray.destroy();
this._tray = undefined;

View File

@@ -5,8 +5,8 @@ import { name } from '@/../../package.json';
import { isMac } from '@/const/env';
import { createLogger } from '@/utils/logger';
import type { App } from './App';
import Tray, { TrayOptions } from './Tray';
import type { App } from '../App';
import { Tray, TrayOptions } from './Tray';
// 创建日志记录器
const logger = createLogger('core:TrayManager');
@@ -16,7 +16,7 @@ const logger = createLogger('core:TrayManager');
*/
export type TrayIdentifiers = 'main';
export default class TrayManager {
export class TrayManager {
app: App;
/**
@@ -61,8 +61,8 @@ export default class TrayManager {
? 'tray-dark.png'
: 'tray-light.png'
: 'tray.png',
identifier: 'main', // 使用应用图标,需要确保资源目录中有此文件
tooltip: name, // 可以使用 app.getName() 或本地化字符串
identifier: 'main', // Use app icon, ensure this file exists in resources directory
tooltip: name, // Can use app.getName() or localized string
});
}

View File

@@ -11,6 +11,7 @@ export interface ElectronMainStore {
networkProxy: NetworkProxySettings;
shortcuts: Record<string, string>;
storagePath: string;
themeMode: 'dark' | 'light' | 'auto';
}
export type StoreKey = keyof ElectronMainStore;

View File

@@ -10,8 +10,10 @@ import HeaderContent from '@/app/[variants]/(main)/chat/settings/features/Header
import BrandWatermark from '@/components/BrandWatermark';
import PanelTitle from '@/components/PanelTitle';
import { INBOX_SESSION_ID } from '@/const/session';
import { isDesktop } from '@/const/version';
import { AgentCategory, AgentSettings as Settings } from '@/features/AgentSetting';
import { AgentSettingsProvider } from '@/features/AgentSetting/AgentSettingsProvider';
import { TITLE_BAR_HEIGHT } from '@/features/ElectronTitlebar';
import Footer from '@/features/Setting/Footer';
import { useInitAgentConfig } from '@/hooks/useInitAgentConfig';
import { useAgentStore } from '@/store/agent';
@@ -49,7 +51,7 @@ const AgentSettings = memo(() => {
>
<Drawer
containerMaxWidth={1280}
height={'100vh'}
height={isDesktop ? `calc(100vh - ${TITLE_BAR_HEIGHT}px)` : '100vh'}
noHeader
onClose={() => useAgentStore.setState({ showAgentSetting: false })}
open={showAgentSetting}
@@ -74,7 +76,7 @@ const AgentSettings = memo(() => {
sidebarContent: {
gap: 48,
justifyContent: 'space-between',
minHeight: '100%',
minHeight: isDesktop ? `calc(100% - ${TITLE_BAR_HEIGHT}px)` : '100%',
paddingBlock: 24,
paddingInline: 48,
},

View File

@@ -1,95 +1,90 @@
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: ${TITLE_BAR_HEIGHT * 1.2}px;
min-height: ${TITLE_BAR_HEIGHT}px;
color: ${token.colorTextSecondary};
transition: all ease-in-out 100ms;
-webkit-app-region: no-drag;
&:hover {
color: ${token.colorText};
background: ${token.colorFillTertiary};
}
&:active {
color: ${token.colorText};
background: ${token.colorFillSecondary};
}
`;
return {
close: cx(
icon,
css`
padding-inline-end: 2px;
&:hover {
color: ${token.colorTextLightSolid};
/* win11 的色值,亮暗色均不变 */
background: #d33328;
}
&:active {
color: ${token.colorTextLightSolid};
/* win11 的色值 */
background: #8b2b25;
}
`,
),
container: css`
cursor: pointer;
display: flex;
`,
icon,
};
});
// const useStyles = createStyles(({ css, cx, token }) => {
// const icon = css`
// display: flex;
// align-items: center;
// justify-content: center;
//
// width: ${TITLE_BAR_HEIGHT * 1.2}px;
// min-height: ${TITLE_BAR_HEIGHT}px;
//
// color: ${token.colorTextSecondary};
//
// transition: all ease-in-out 100ms;
//
// -webkit-app-region: no-drag;
//
// &:hover {
// color: ${token.colorText};
// background: ${token.colorFillTertiary};
// }
//
// &:active {
// color: ${token.colorText};
// background: ${token.colorFillSecondary};
// }
// `;
// return {
// close: cx(
// icon,
// css`
// padding-inline-end: 2px;
//
// &:hover {
// color: ${token.colorTextLightSolid};
//
// /* win11 的色值,亮暗色均不变 */
// background: #d33328;
// }
//
// &:active {
// color: ${token.colorTextLightSolid};
//
// /* win11 的色值 */
// background: #8b2b25;
// }
// `,
// ),
// container: css`
// cursor: pointer;
// display: flex;
// `,
// icon,
// };
// });
const WinControl = () => {
const { styles } = useStyles();
return <div style={{ width: 132 }} />;
return (
<div className={styles.container}>
<div
className={styles.icon}
onClick={() => {
electronSystemService.minimizeWindow();
}}
>
<Minus absoluteStrokeWidth size={14} strokeWidth={1.2} />
</div>
<div
className={styles.icon}
onClick={() => {
electronSystemService.maximizeWindow();
}}
>
<Square absoluteStrokeWidth size={10} strokeWidth={1.2} />
</div>
<div
className={styles.close}
onClick={() => {
electronSystemService.closeWindow();
}}
>
<XIcon absoluteStrokeWidth size={14} strokeWidth={1.2} />
</div>
</div>
);
// const { styles } = useStyles();
//
// return (
// <div className={styles.container}>
// <div
// className={styles.icon}
// onClick={() => {
// electronSystemService.minimizeWindow();
// }}
// >
// <Minus absoluteStrokeWidth size={14} strokeWidth={1.2} />
// </div>
// <div
// className={styles.icon}
// onClick={() => {
// electronSystemService.maximizeWindow();
// }}
// >
// <Square absoluteStrokeWidth size={10} strokeWidth={1.2} />
// </div>
// <div
// className={styles.close}
// onClick={() => {
// electronSystemService.closeWindow();
// }}
// >
// <XIcon absoluteStrokeWidth size={14} strokeWidth={1.2} />
// </div>
// </div>
// );
};
export default WinControl;

View File

@@ -7,10 +7,14 @@ import { useElectronStore } from '@/store/electron';
import { useGlobalStore } from '@/store/global';
export const useWatchThemeUpdate = () => {
const [systemAppearance, updateElectronAppState] = useElectronStore((s) => [
s.appState.systemAppearance,
s.updateElectronAppState,
]);
const [isAppStateInit, systemAppearance, updateElectronAppState, isMac] = useElectronStore(
(s) => [
s.isAppStateInit,
s.appState.systemAppearance,
s.updateElectronAppState,
s.appState.isMac,
],
);
const switchThemeMode = useGlobalStore((s) => s.switchThemeMode);
const theme = useTheme();
@@ -24,11 +28,12 @@ export const useWatchThemeUpdate = () => {
});
useEffect(() => {
if (!isAppStateInit || !isMac) return;
document.documentElement.style.background = 'none';
// https://x.com/alanblogsooo/status/1939208908993896684
const isNotSameTheme = !systemAppearance ? true : theme.appearance !== systemAppearance;
document.body.style.background = rgba(theme.colorBgLayout, isNotSameTheme ? 0.95 : 0.66);
}, [theme, systemAppearance]);
}, [theme, systemAppearance, isAppStateInit, isMac]);
};

View File

@@ -7,6 +7,8 @@ import { Trans, useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';
import { WIKI_PLUGIN_GUIDE } from '@/const/url';
import { isDesktop } from '@/const/version';
import { TITLE_BAR_HEIGHT } from '@/features/ElectronTitlebar';
import { LobeToolCustomPlugin } from '@/types/tool/plugin';
import MCPManifestForm from './MCPManifestForm';
@@ -112,7 +114,7 @@ const DevModal = memo<DevModalProps>(
containerMaxWidth={'auto'}
destroyOnHidden
footer={footer}
height={'100vh'}
height={isDesktop ? `calc(100vh - ${TITLE_BAR_HEIGHT}px)` : '100vh'}
onClose={(e) => {
e.stopPropagation();
onOpenChange(false);