💄 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>
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 171 KiB |
BIN
apps/desktop/build/icon-dev.ico
Normal file
|
After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 171 KiB |
@@ -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: {
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BrowserWindowOpts } from './core/Browser';
|
||||
import type { BrowserWindowOpts } from './core/browser/Browser';
|
||||
|
||||
export const BrowsersIdentifiers = {
|
||||
chat: 'chat',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -31,4 +31,5 @@ export const STORE_DEFAULTS: ElectronMainStore = {
|
||||
networkProxy: defaultProxySettings,
|
||||
shortcuts: DEFAULT_SHORTCUTS_CONFIG,
|
||||
storagePath: appStorageDir,
|
||||
themeMode: 'auto',
|
||||
};
|
||||
|
||||
11
apps/desktop/src/main/const/theme.ts
Normal 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;
|
||||
@@ -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',
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
// 发送文件内容
|
||||
@@ -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');
|
||||
@@ -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');
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
@@ -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;
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface ElectronMainStore {
|
||||
networkProxy: NetworkProxySettings;
|
||||
shortcuts: Record<string, string>;
|
||||
storagePath: string;
|
||||
themeMode: 'dark' | 'light' | 'auto';
|
||||
}
|
||||
|
||||
export type StoreKey = keyof ElectronMainStore;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||