mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +07:00
⚡️ perf: move settings into one page (#10229)
* move settings into one page * fix: change the jump link to react-router-dom --------- Co-authored-by: ONLY-yours <1349021570@qq.com>
This commit is contained in:
@@ -33,12 +33,6 @@ export interface RouteInterceptConfig {
|
||||
* 定义了所有需要特殊处理的路由
|
||||
*/
|
||||
export const interceptRoutes: RouteInterceptConfig[] = [
|
||||
{
|
||||
description: '设置页面',
|
||||
enabled: true,
|
||||
pathPrefix: '/settings',
|
||||
targetWindow: 'settings',
|
||||
},
|
||||
{
|
||||
description: '开发者工具',
|
||||
enabled: true,
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { BrowserWindowOpts } from './core/browser/Browser';
|
||||
export const BrowsersIdentifiers = {
|
||||
chat: 'chat',
|
||||
devtools: 'devtools',
|
||||
settings: 'settings',
|
||||
};
|
||||
|
||||
export const appBrowsers = {
|
||||
@@ -32,18 +31,6 @@ export const appBrowsers = {
|
||||
vibrancy: 'under-window',
|
||||
width: 1000,
|
||||
},
|
||||
settings: {
|
||||
autoHideMenuBar: true,
|
||||
height: 800,
|
||||
identifier: 'settings',
|
||||
keepAlive: true,
|
||||
minWidth: 600,
|
||||
parentIdentifier: 'chat',
|
||||
path: '/settings',
|
||||
titleBarStyle: 'hidden',
|
||||
vibrancy: 'under-window',
|
||||
width: 1000,
|
||||
},
|
||||
} satisfies Record<string, BrowserWindowOpts>;
|
||||
|
||||
// Window templates for multi-instance windows
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { InterceptRouteParams, OpenSettingsWindowOptions } from '@lobechat/electron-client-ipc';
|
||||
import { extractSubPath, findMatchingRoute } from '~common/routes';
|
||||
import { findMatchingRoute } from '~common/routes';
|
||||
|
||||
import {
|
||||
AppBrowsersIdentifiers,
|
||||
BrowsersIdentifiers,
|
||||
WindowTemplateIdentifiers,
|
||||
} from '@/appBrowsers';
|
||||
import { IpcClientEventSender } from '@/types/ipcClientEvent';
|
||||
@@ -24,14 +23,32 @@ export default class BrowserWindowsCtr extends ControllerModule {
|
||||
? { tab: typeof options === 'string' ? options : undefined }
|
||||
: options;
|
||||
|
||||
console.log('[BrowserWindowsCtr] Received request to open settings window', normalizedOptions);
|
||||
console.log('[BrowserWindowsCtr] Received request to open settings', normalizedOptions);
|
||||
|
||||
try {
|
||||
await this.app.browserManager.showSettingsWindowWithTab(normalizedOptions);
|
||||
const query = new URLSearchParams();
|
||||
if (normalizedOptions.searchParams) {
|
||||
Object.entries(normalizedOptions.searchParams).forEach(([key, value]) => {
|
||||
if (value !== undefined) query.set(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
const tab = normalizedOptions.tab;
|
||||
if (tab && tab !== 'common' && !query.has('active')) {
|
||||
query.set('active', tab);
|
||||
}
|
||||
|
||||
const queryString = query.toString();
|
||||
const subPath = tab && !queryString ? `/${tab}` : '';
|
||||
const fullPath = `/settings${subPath}${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const mainWindow = this.app.browserManager.getMainWindow();
|
||||
await mainWindow.loadUrl(fullPath);
|
||||
mainWindow.show();
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[BrowserWindowsCtr] Failed to open settings window:', error);
|
||||
console.error('[BrowserWindowsCtr] Failed to open settings:', error);
|
||||
return { error: error.message, success: false };
|
||||
}
|
||||
}
|
||||
@@ -76,50 +93,14 @@ export default class BrowserWindowsCtr extends ControllerModule {
|
||||
);
|
||||
|
||||
try {
|
||||
if (matchedRoute.targetWindow === BrowsersIdentifiers.settings) {
|
||||
const extractedSubPath = extractSubPath(path, matchedRoute.pathPrefix);
|
||||
const sanitizedSubPath =
|
||||
extractedSubPath && !extractedSubPath.startsWith('?') ? extractedSubPath : undefined;
|
||||
let searchParams: Record<string, string> | undefined;
|
||||
try {
|
||||
const url = new URL(params.url);
|
||||
const entries = Array.from(url.searchParams.entries());
|
||||
if (entries.length > 0) {
|
||||
searchParams = entries.reduce<Record<string, string>>((acc, [key, value]) => {
|
||||
acc[key] = value;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'[BrowserWindowsCtr] Failed to parse URL for settings route interception:',
|
||||
params.url,
|
||||
error,
|
||||
);
|
||||
}
|
||||
await this.openTargetWindow(matchedRoute.targetWindow as AppBrowsersIdentifiers);
|
||||
|
||||
await this.app.browserManager.showSettingsWindowWithTab({
|
||||
searchParams,
|
||||
tab: sanitizedSubPath,
|
||||
});
|
||||
|
||||
return {
|
||||
intercepted: true,
|
||||
path,
|
||||
source,
|
||||
subPath: sanitizedSubPath,
|
||||
targetWindow: matchedRoute.targetWindow,
|
||||
};
|
||||
} else {
|
||||
await this.openTargetWindow(matchedRoute.targetWindow as AppBrowsersIdentifiers);
|
||||
|
||||
return {
|
||||
intercepted: true,
|
||||
path,
|
||||
source,
|
||||
targetWindow: matchedRoute.targetWindow,
|
||||
};
|
||||
}
|
||||
return {
|
||||
intercepted: true,
|
||||
path,
|
||||
source,
|
||||
targetWindow: matchedRoute.targetWindow,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[BrowserWindowsCtr] Error while processing route interception:', error);
|
||||
return {
|
||||
|
||||
@@ -9,36 +9,40 @@ import BrowserWindowsCtr from '../BrowserWindowsCtr';
|
||||
|
||||
// 模拟 App 及其依赖项
|
||||
const mockToggleVisible = vi.fn();
|
||||
const mockShowSettingsWindowWithTab = vi.fn();
|
||||
const mockLoadUrl = vi.fn();
|
||||
const mockShow = vi.fn();
|
||||
const mockRedirectToPage = vi.fn();
|
||||
const mockCloseWindow = vi.fn();
|
||||
const mockMinimizeWindow = vi.fn();
|
||||
const mockMaximizeWindow = vi.fn();
|
||||
const mockRetrieveByIdentifier = vi.fn();
|
||||
const mockGetMainWindow = vi.fn(() => ({
|
||||
toggleVisible: mockToggleVisible,
|
||||
loadUrl: mockLoadUrl,
|
||||
show: mockShow,
|
||||
}));
|
||||
const mockShow = vi.fn();
|
||||
const mockShowOther = vi.fn();
|
||||
|
||||
// mock findMatchingRoute and extractSubPath
|
||||
vi.mock('~common/routes', async () => ({
|
||||
findMatchingRoute: vi.fn(),
|
||||
extractSubPath: vi.fn(),
|
||||
}));
|
||||
const { findMatchingRoute, extractSubPath } = await import('~common/routes');
|
||||
const { findMatchingRoute } = await import('~common/routes');
|
||||
|
||||
const mockApp = {
|
||||
browserManager: {
|
||||
getMainWindow: mockGetMainWindow,
|
||||
showSettingsWindowWithTab: mockShowSettingsWindowWithTab,
|
||||
redirectToPage: mockRedirectToPage,
|
||||
closeWindow: mockCloseWindow,
|
||||
minimizeWindow: mockMinimizeWindow,
|
||||
maximizeWindow: mockMaximizeWindow,
|
||||
retrieveByIdentifier: mockRetrieveByIdentifier.mockImplementation(
|
||||
(identifier: AppBrowsersIdentifiers | string) => {
|
||||
if (identifier === BrowsersIdentifiers.settings || identifier === 'some-other-window') {
|
||||
return { show: mockShow };
|
||||
if (identifier === 'some-other-window') {
|
||||
return { show: mockShowOther };
|
||||
}
|
||||
return { show: mockShow }; // Default mock for other identifiers
|
||||
return { show: mockShowOther }; // Default mock for other identifiers
|
||||
},
|
||||
),
|
||||
},
|
||||
@@ -61,16 +65,18 @@ describe('BrowserWindowsCtr', () => {
|
||||
});
|
||||
|
||||
describe('openSettingsWindow', () => {
|
||||
it('should show the settings window with the specified tab', async () => {
|
||||
it('should navigate to settings in main window with the specified tab', async () => {
|
||||
const tab = 'appearance';
|
||||
const result = await browserWindowsCtr.openSettingsWindow(tab);
|
||||
expect(mockShowSettingsWindowWithTab).toHaveBeenCalledWith({ tab });
|
||||
expect(mockGetMainWindow).toHaveBeenCalled();
|
||||
expect(mockLoadUrl).toHaveBeenCalledWith('/settings?active=appearance');
|
||||
expect(mockShow).toHaveBeenCalled();
|
||||
expect(result).toEqual({ success: true });
|
||||
});
|
||||
|
||||
it('should return error if showing settings window fails', async () => {
|
||||
const errorMessage = 'Failed to show';
|
||||
mockShowSettingsWindowWithTab.mockRejectedValueOnce(new Error(errorMessage));
|
||||
it('should return error if navigation fails', async () => {
|
||||
const errorMessage = 'Failed to navigate';
|
||||
mockLoadUrl.mockRejectedValueOnce(new Error(errorMessage));
|
||||
const result = await browserWindowsCtr.openSettingsWindow('display');
|
||||
expect(result).toEqual({ error: errorMessage, success: false });
|
||||
});
|
||||
@@ -117,36 +123,7 @@ describe('BrowserWindowsCtr', () => {
|
||||
expect(result).toEqual({ intercepted: false, path: params.path, source: params.source });
|
||||
});
|
||||
|
||||
it('should show settings window if matched route target is settings', async () => {
|
||||
const params: InterceptRouteParams = {
|
||||
...baseParams,
|
||||
path: '/settings/provider',
|
||||
url: 'app://host/settings/provider?active=provider&provider=ollama',
|
||||
};
|
||||
const matchedRoute = { targetWindow: BrowsersIdentifiers.settings, pathPrefix: '/settings' };
|
||||
const subPath = 'provider';
|
||||
(findMatchingRoute as Mock).mockReturnValue(matchedRoute);
|
||||
(extractSubPath as Mock).mockReturnValue(subPath);
|
||||
|
||||
const result = await browserWindowsCtr.interceptRoute(params);
|
||||
|
||||
expect(findMatchingRoute).toHaveBeenCalledWith(params.path);
|
||||
expect(extractSubPath).toHaveBeenCalledWith(params.path, matchedRoute.pathPrefix);
|
||||
expect(mockShowSettingsWindowWithTab).toHaveBeenCalledWith({
|
||||
searchParams: { active: 'provider', provider: 'ollama' },
|
||||
tab: subPath,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
intercepted: true,
|
||||
path: params.path,
|
||||
source: params.source,
|
||||
subPath,
|
||||
targetWindow: matchedRoute.targetWindow,
|
||||
});
|
||||
expect(mockShow).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should open target window if matched route target is not settings', async () => {
|
||||
it('should open target window if matched route is found', async () => {
|
||||
const params: InterceptRouteParams = {
|
||||
...baseParams,
|
||||
path: '/other/page',
|
||||
@@ -160,44 +137,16 @@ describe('BrowserWindowsCtr', () => {
|
||||
|
||||
expect(findMatchingRoute).toHaveBeenCalledWith(params.path);
|
||||
expect(mockRetrieveByIdentifier).toHaveBeenCalledWith(targetWindowIdentifier);
|
||||
expect(mockShow).toHaveBeenCalled();
|
||||
expect(mockShowOther).toHaveBeenCalled();
|
||||
expect(result).toEqual({
|
||||
intercepted: true,
|
||||
path: params.path,
|
||||
source: params.source,
|
||||
targetWindow: matchedRoute.targetWindow,
|
||||
});
|
||||
expect(mockShowSettingsWindowWithTab).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return error if processing route interception fails for settings', async () => {
|
||||
const params: InterceptRouteParams = {
|
||||
...baseParams,
|
||||
path: '/settings',
|
||||
url: 'app://host/settings?active=general',
|
||||
};
|
||||
const matchedRoute = { targetWindow: BrowsersIdentifiers.settings, pathPrefix: '/settings' };
|
||||
const subPath = undefined;
|
||||
const errorMessage = 'Processing error for settings';
|
||||
(findMatchingRoute as Mock).mockReturnValue(matchedRoute);
|
||||
(extractSubPath as Mock).mockReturnValue(subPath);
|
||||
mockShowSettingsWindowWithTab.mockRejectedValueOnce(new Error(errorMessage));
|
||||
|
||||
const result = await browserWindowsCtr.interceptRoute(params);
|
||||
|
||||
expect(mockShowSettingsWindowWithTab).toHaveBeenCalledWith({
|
||||
searchParams: { active: 'general' },
|
||||
tab: subPath,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
error: errorMessage,
|
||||
intercepted: false,
|
||||
path: params.path,
|
||||
source: params.source,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error if processing route interception fails for other window', async () => {
|
||||
it('should return error if processing route interception fails', async () => {
|
||||
const params: InterceptRouteParams = {
|
||||
...baseParams,
|
||||
path: '/another/custom',
|
||||
|
||||
@@ -336,6 +336,7 @@ export default class Browser {
|
||||
vibrancy: 'sidebar',
|
||||
visualEffectState: 'active',
|
||||
webPreferences: {
|
||||
backgroundThrottling: false,
|
||||
contextIsolation: true,
|
||||
preload: join(preloadDir, 'index.js'),
|
||||
},
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
MainBroadcastEventKey,
|
||||
MainBroadcastParams,
|
||||
OpenSettingsWindowOptions,
|
||||
} from '@lobechat/electron-client-ipc';
|
||||
import { MainBroadcastEventKey, MainBroadcastParams } from '@lobechat/electron-client-ipc';
|
||||
import { WebContents } from 'electron';
|
||||
|
||||
import { createLogger } from '@/utils/logger';
|
||||
@@ -42,13 +38,6 @@ export class BrowserManager {
|
||||
window.show();
|
||||
}
|
||||
|
||||
showSettingsWindow() {
|
||||
logger.debug('Showing settings window');
|
||||
const window = this.retrieveByIdentifier('settings');
|
||||
window.show();
|
||||
return window;
|
||||
}
|
||||
|
||||
broadcastToAllWindows = <T extends MainBroadcastEventKey>(
|
||||
event: T,
|
||||
data: MainBroadcastParams<T>,
|
||||
@@ -68,50 +57,6 @@ export class BrowserManager {
|
||||
this.browsers.get(identifier)?.broadcast(event, data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Display the settings window and navigate to a specific tab
|
||||
* @param tab Settings window sub-path tab
|
||||
*/
|
||||
async showSettingsWindowWithTab(options?: OpenSettingsWindowOptions) {
|
||||
const tab = options?.tab;
|
||||
const searchParams = options?.searchParams;
|
||||
|
||||
const query = new URLSearchParams();
|
||||
if (searchParams) {
|
||||
Object.entries(searchParams).forEach(([key, value]) => {
|
||||
if (value !== undefined) query.set(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
if (tab && tab !== 'common' && !query.has('active')) {
|
||||
query.set('active', tab);
|
||||
}
|
||||
|
||||
const queryString = query.toString();
|
||||
const activeTab = query.get('active') ?? tab;
|
||||
|
||||
logger.debug(
|
||||
`Showing settings window with navigation: active=${activeTab || 'default'}, query=${
|
||||
queryString || 'none'
|
||||
}`,
|
||||
);
|
||||
|
||||
if (queryString) {
|
||||
const browser = await this.redirectToPage('settings', undefined, queryString);
|
||||
|
||||
// make provider page more large
|
||||
if (activeTab?.startsWith('provider')) {
|
||||
logger.debug('Resizing window for provider settings');
|
||||
browser.setWindowSize({ height: 1000, width: 1400 });
|
||||
browser.moveToCenter();
|
||||
}
|
||||
|
||||
return browser;
|
||||
} else {
|
||||
return this.showSettingsWindow();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate window to specific sub-path
|
||||
* @param identifier Window identifier
|
||||
|
||||
@@ -81,8 +81,10 @@ export class MacOSMenu extends BaseMenuPlatform implements IMenuPlatform {
|
||||
{ type: 'separator' },
|
||||
{
|
||||
accelerator: 'Command+,',
|
||||
click: () => {
|
||||
this.app.browserManager.showSettingsWindow();
|
||||
click: async () => {
|
||||
const mainWindow = this.app.browserManager.getMainWindow();
|
||||
await mainWindow.loadUrl('/settings');
|
||||
mainWindow.show();
|
||||
},
|
||||
label: t('macOS.preferences'),
|
||||
},
|
||||
@@ -337,7 +339,11 @@ export class MacOSMenu extends BaseMenuPlatform implements IMenuPlatform {
|
||||
label: t('tray.show', { appName }),
|
||||
},
|
||||
{
|
||||
click: () => this.app.browserManager.retrieveByIdentifier('settings').show(),
|
||||
click: async () => {
|
||||
const mainWindow = this.app.browserManager.getMainWindow();
|
||||
await mainWindow.loadUrl('/settings');
|
||||
mainWindow.show();
|
||||
},
|
||||
label: t('file.preferences'),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
|
||||
Reference in New Issue
Block a user