@lobehub/lobehub 2.0.0-next.242 → 2.0.0-next.243
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/rules/desktop-feature-implementation.mdc +2 -2
- package/.cursor/rules/desktop-local-tools-implement.mdc +2 -2
- package/CHANGELOG.md +27 -0
- package/apps/desktop/Development.md +1 -6
- package/apps/desktop/README.md +2 -17
- package/apps/desktop/README.zh-CN.md +1 -15
- package/apps/desktop/src/main/controllers/index.ts +1 -1
- package/apps/desktop/src/main/controllers/registry.ts +0 -9
- package/apps/desktop/src/main/core/App.ts +1 -11
- package/apps/desktop/src/main/core/browser/Browser.ts +278 -457
- package/apps/desktop/src/main/core/browser/WindowStateManager.ts +180 -0
- package/apps/desktop/src/main/core/browser/WindowThemeManager.ts +167 -0
- package/apps/desktop/src/main/core/browser/__tests__/WindowStateManager.test.ts +237 -0
- package/apps/desktop/src/main/core/browser/__tests__/WindowThemeManager.test.ts +240 -0
- package/apps/desktop/src/main/exports.d.ts +1 -1
- package/apps/desktop/src/main/exports.ts +1 -1
- package/apps/desktop/src/main/utils/__tests__/http-headers.test.ts +131 -0
- package/apps/desktop/src/main/utils/http-headers.ts +61 -0
- package/apps/desktop/src/main/utils/ipc/__tests__/base.test.ts +1 -22
- package/apps/desktop/src/main/utils/ipc/base.ts +0 -20
- package/apps/desktop/src/main/utils/ipc/index.ts +1 -9
- package/changelog/v1.json +5 -0
- package/package.json +1 -1
- package/src/features/ChatInput/InputEditor/Placeholder.tsx +4 -1
- package/apps/desktop/src/main/controllers/UploadFileServerCtr.ts +0 -33
- package/apps/desktop/src/main/controllers/__tests__/UploadFileServerCtr.test.ts +0 -55
- package/src/server/modules/ElectronIPCClient/index.ts +0 -92
|
@@ -4,32 +4,27 @@ import {
|
|
|
4
4
|
BrowserWindowConstructorOptions,
|
|
5
5
|
session as electronSession,
|
|
6
6
|
ipcMain,
|
|
7
|
-
nativeTheme,
|
|
8
7
|
screen,
|
|
9
8
|
} from 'electron';
|
|
10
9
|
import console from 'node:console';
|
|
11
10
|
import { join } from 'node:path';
|
|
12
11
|
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
12
|
+
import { preloadDir, resourcesDir } from '@/const/dir';
|
|
13
|
+
import { isMac } from '@/const/env';
|
|
15
14
|
import { ELECTRON_BE_PROTOCOL_SCHEME } from '@/const/protocol';
|
|
16
|
-
import {
|
|
17
|
-
BACKGROUND_DARK,
|
|
18
|
-
BACKGROUND_LIGHT,
|
|
19
|
-
SYMBOL_COLOR_DARK,
|
|
20
|
-
SYMBOL_COLOR_LIGHT,
|
|
21
|
-
THEME_CHANGE_DELAY,
|
|
22
|
-
TITLE_BAR_HEIGHT,
|
|
23
|
-
} from '@/const/theme';
|
|
24
15
|
import RemoteServerConfigCtr from '@/controllers/RemoteServerConfigCtr';
|
|
25
16
|
import { backendProxyProtocolManager } from '@/core/infrastructure/BackendProxyProtocolManager';
|
|
17
|
+
import { setResponseHeader } from '@/utils/http-headers';
|
|
26
18
|
import { createLogger } from '@/utils/logger';
|
|
27
19
|
|
|
28
20
|
import type { App } from '../App';
|
|
21
|
+
import { WindowStateManager } from './WindowStateManager';
|
|
22
|
+
import { WindowThemeManager } from './WindowThemeManager';
|
|
29
23
|
|
|
30
|
-
// Create logger
|
|
31
24
|
const logger = createLogger('core:Browser');
|
|
32
25
|
|
|
26
|
+
// ==================== Types ====================
|
|
27
|
+
|
|
33
28
|
export interface BrowserWindowOpts extends BrowserWindowConstructorOptions {
|
|
34
29
|
devTools?: boolean;
|
|
35
30
|
height?: number;
|
|
@@ -42,297 +37,193 @@ export interface BrowserWindowOpts extends BrowserWindowConstructorOptions {
|
|
|
42
37
|
width?: number;
|
|
43
38
|
}
|
|
44
39
|
|
|
45
|
-
|
|
46
|
-
height?: number;
|
|
47
|
-
width?: number;
|
|
48
|
-
x?: number;
|
|
49
|
-
y?: number;
|
|
50
|
-
}
|
|
40
|
+
// ==================== Browser Class ====================
|
|
51
41
|
|
|
52
42
|
export default class Browser {
|
|
53
|
-
private app: App;
|
|
43
|
+
private readonly app: App;
|
|
44
|
+
private readonly stateManager: WindowStateManager;
|
|
45
|
+
private readonly themeManager: WindowThemeManager;
|
|
46
|
+
|
|
54
47
|
private _browserWindow?: BrowserWindow;
|
|
55
|
-
private themeListenerSetup = false;
|
|
56
|
-
identifier: string;
|
|
57
|
-
options: BrowserWindowOpts;
|
|
58
|
-
private readonly windowStateKey: string;
|
|
59
48
|
|
|
60
|
-
|
|
49
|
+
readonly identifier: string;
|
|
50
|
+
readonly options: BrowserWindowOpts;
|
|
51
|
+
|
|
52
|
+
// ==================== Accessors ====================
|
|
53
|
+
|
|
54
|
+
get browserWindow(): BrowserWindow {
|
|
61
55
|
return this.retrieveOrInitialize();
|
|
62
56
|
}
|
|
57
|
+
|
|
63
58
|
get webContents() {
|
|
64
|
-
if (this._browserWindow
|
|
65
|
-
return this._browserWindow
|
|
59
|
+
if (this._browserWindow?.isDestroyed()) return null;
|
|
60
|
+
return this._browserWindow?.webContents ?? null;
|
|
66
61
|
}
|
|
67
62
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
* @param options
|
|
71
|
-
* @param application
|
|
72
|
-
*/
|
|
63
|
+
// ==================== Constructor ====================
|
|
64
|
+
|
|
73
65
|
constructor(options: BrowserWindowOpts, application: App) {
|
|
74
66
|
logger.debug(`Creating Browser instance: ${options.identifier}`);
|
|
75
67
|
logger.debug(`Browser options: ${JSON.stringify(options)}`);
|
|
68
|
+
|
|
76
69
|
this.app = application;
|
|
77
70
|
this.identifier = options.identifier;
|
|
78
71
|
this.options = options;
|
|
79
|
-
this.windowStateKey = `windowSize_${this.identifier}`;
|
|
80
72
|
|
|
81
|
-
//
|
|
73
|
+
// Initialize managers
|
|
74
|
+
this.stateManager = new WindowStateManager(application, {
|
|
75
|
+
identifier: options.identifier,
|
|
76
|
+
keepAlive: options.keepAlive,
|
|
77
|
+
});
|
|
78
|
+
this.themeManager = new WindowThemeManager(options.identifier);
|
|
79
|
+
|
|
80
|
+
// Initialize window
|
|
82
81
|
this.retrieveOrInitialize();
|
|
83
82
|
}
|
|
84
83
|
|
|
84
|
+
// ==================== Window Lifecycle ====================
|
|
85
|
+
|
|
85
86
|
/**
|
|
86
|
-
*
|
|
87
|
+
* Initialize or retrieve existing browser window
|
|
87
88
|
*/
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
|
|
89
|
+
retrieveOrInitialize(): BrowserWindow {
|
|
90
|
+
if (this._browserWindow && !this._browserWindow.isDestroyed()) {
|
|
91
|
+
logger.debug(`[${this.identifier}] Returning existing BrowserWindow instance.`);
|
|
92
|
+
return this._browserWindow;
|
|
91
93
|
}
|
|
92
94
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Get Windows-specific theme configuration
|
|
98
|
-
*/
|
|
99
|
-
private getWindowsThemeConfig(isDarkMode: boolean) {
|
|
100
|
-
return {
|
|
101
|
-
backgroundColor: isDarkMode ? BACKGROUND_DARK : BACKGROUND_LIGHT,
|
|
102
|
-
icon: isDev ? join(buildDir, 'icon-dev.ico') : undefined,
|
|
103
|
-
titleBarOverlay: {
|
|
104
|
-
color: isDarkMode ? BACKGROUND_DARK : BACKGROUND_LIGHT,
|
|
105
|
-
height: TITLE_BAR_HEIGHT,
|
|
106
|
-
symbolColor: isDarkMode ? SYMBOL_COLOR_DARK : SYMBOL_COLOR_LIGHT,
|
|
107
|
-
},
|
|
108
|
-
titleBarStyle: 'hidden' as const,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
95
|
+
const browserWindow = this.createBrowserWindow();
|
|
96
|
+
this._browserWindow = browserWindow;
|
|
111
97
|
|
|
112
|
-
|
|
113
|
-
if (this.themeListenerSetup) return;
|
|
98
|
+
this.setupWindow(browserWindow);
|
|
114
99
|
|
|
115
|
-
|
|
116
|
-
|
|
100
|
+
logger.debug(`[${this.identifier}] retrieveOrInitialize completed.`);
|
|
101
|
+
return browserWindow;
|
|
117
102
|
}
|
|
118
103
|
|
|
119
|
-
private handleThemeChange = (): void => {
|
|
120
|
-
logger.debug(`[${this.identifier}] System theme changed, reapplying visual effects.`);
|
|
121
|
-
setTimeout(() => {
|
|
122
|
-
this.applyVisualEffects();
|
|
123
|
-
}, THEME_CHANGE_DELAY);
|
|
124
|
-
};
|
|
125
|
-
|
|
126
104
|
/**
|
|
127
|
-
*
|
|
105
|
+
* Destroy window instance and cleanup resources
|
|
128
106
|
*/
|
|
129
|
-
|
|
130
|
-
logger.debug(`
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}, THEME_CHANGE_DELAY);
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
private applyVisualEffects(): void {
|
|
137
|
-
if (!this._browserWindow || this._browserWindow.isDestroyed()) return;
|
|
138
|
-
|
|
139
|
-
logger.debug(`[${this.identifier}] Applying visual effects for platform`);
|
|
140
|
-
const isDarkMode = this.isDarkMode;
|
|
141
|
-
|
|
142
|
-
try {
|
|
143
|
-
if (isWindows) {
|
|
144
|
-
this.applyWindowsVisualEffects(isDarkMode);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
logger.debug(
|
|
148
|
-
`[${this.identifier}] Visual effects applied successfully (dark mode: ${isDarkMode})`,
|
|
149
|
-
);
|
|
150
|
-
} catch (error) {
|
|
151
|
-
logger.error(`[${this.identifier}] Failed to apply visual effects:`, error);
|
|
152
|
-
}
|
|
107
|
+
destroy(): void {
|
|
108
|
+
logger.debug(`Destroying window instance: ${this.identifier}`);
|
|
109
|
+
this.themeManager.cleanup();
|
|
110
|
+
this._browserWindow = undefined;
|
|
153
111
|
}
|
|
154
112
|
|
|
155
|
-
|
|
156
|
-
const config = this.getWindowsThemeConfig(isDarkMode);
|
|
113
|
+
// ==================== Window Creation ====================
|
|
157
114
|
|
|
158
|
-
|
|
159
|
-
this.
|
|
160
|
-
}
|
|
115
|
+
private createBrowserWindow(): BrowserWindow {
|
|
116
|
+
const { title, width, height, ...rest } = this.options;
|
|
161
117
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
118
|
+
const resolvedState = this.stateManager.resolveState({ height, width });
|
|
119
|
+
logger.info(`Creating new BrowserWindow instance: ${this.identifier}`);
|
|
120
|
+
logger.debug(`[${this.identifier}] Resolved window state: ${JSON.stringify(resolvedState)}`);
|
|
165
121
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
122
|
+
return new BrowserWindow({
|
|
123
|
+
...rest,
|
|
124
|
+
autoHideMenuBar: true,
|
|
125
|
+
backgroundColor: '#00000000',
|
|
126
|
+
darkTheme: this.themeManager.isDarkMode,
|
|
127
|
+
frame: false,
|
|
128
|
+
height: resolvedState.height,
|
|
129
|
+
show: false,
|
|
130
|
+
title,
|
|
131
|
+
vibrancy: 'sidebar',
|
|
132
|
+
visualEffectState: 'active',
|
|
133
|
+
webPreferences: {
|
|
134
|
+
backgroundThrottling: false,
|
|
135
|
+
contextIsolation: true,
|
|
136
|
+
preload: join(preloadDir, 'index.js'),
|
|
137
|
+
sandbox: false,
|
|
138
|
+
},
|
|
139
|
+
width: resolvedState.width,
|
|
140
|
+
x: resolvedState.x,
|
|
141
|
+
y: resolvedState.y,
|
|
142
|
+
...this.themeManager.getPlatformConfig(),
|
|
185
143
|
});
|
|
144
|
+
}
|
|
186
145
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const resolvedHeight = typeof height === 'number' ? Math.min(height, workArea.height) : height;
|
|
146
|
+
private setupWindow(browserWindow: BrowserWindow): void {
|
|
147
|
+
logger.debug(`[${this.identifier}] BrowserWindow instance created.`);
|
|
190
148
|
|
|
191
|
-
|
|
192
|
-
|
|
149
|
+
// Setup theme management
|
|
150
|
+
this.themeManager.attach(browserWindow);
|
|
193
151
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
x: this.clampNumber(x, workArea.x, maxX),
|
|
198
|
-
y: this.clampNumber(y, workArea.y, maxY),
|
|
199
|
-
};
|
|
200
|
-
}
|
|
152
|
+
// Setup network interceptors
|
|
153
|
+
this.setupCORSBypass(browserWindow);
|
|
154
|
+
this.setupRemoteServerRequestHook(browserWindow);
|
|
201
155
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
this.
|
|
156
|
+
// Load content
|
|
157
|
+
this.initiateContentLoading();
|
|
158
|
+
|
|
159
|
+
// Setup devtools if enabled
|
|
160
|
+
if (this.options.devTools) {
|
|
161
|
+
logger.debug(`[${this.identifier}] Opening DevTools.`);
|
|
162
|
+
browserWindow.webContents.openDevTools();
|
|
208
163
|
}
|
|
209
|
-
}
|
|
210
164
|
|
|
211
|
-
|
|
212
|
-
|
|
165
|
+
// Setup event listeners
|
|
166
|
+
this.setupEventListeners(browserWindow);
|
|
213
167
|
}
|
|
214
168
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
console.log('[Browser] initUrl', urlWithLocale);
|
|
169
|
+
private initiateContentLoading(): void {
|
|
170
|
+
logger.debug(`[${this.identifier}] Initiating placeholder and URL loading sequence.`);
|
|
171
|
+
this.loadPlaceholder().then(() => {
|
|
172
|
+
this.loadUrl(this.options.path).catch((e) => {
|
|
173
|
+
logger.error(
|
|
174
|
+
`[${this.identifier}] Initial loadUrl error for path '${this.options.path}':`,
|
|
175
|
+
e,
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
}
|
|
227
180
|
|
|
228
|
-
|
|
229
|
-
logger.debug(`[${this.identifier}] Attempting to load URL: ${urlWithLocale}`);
|
|
230
|
-
await this._browserWindow.loadURL(urlWithLocale);
|
|
181
|
+
// ==================== Event Listeners ====================
|
|
231
182
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
183
|
+
private setupEventListeners(browserWindow: BrowserWindow): void {
|
|
184
|
+
this.setupReadyToShowListener(browserWindow);
|
|
185
|
+
this.setupCloseListener(browserWindow);
|
|
186
|
+
}
|
|
235
187
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
logger.debug(`
|
|
245
|
-
|
|
246
|
-
// Set retry logic
|
|
247
|
-
ipcMain.handle('retry-connection', async () => {
|
|
248
|
-
logger.info(`[${this.identifier}] Retry connection requested for: ${urlWithLocale}`);
|
|
249
|
-
try {
|
|
250
|
-
await this._browserWindow?.loadURL(urlWithLocale);
|
|
251
|
-
logger.info(`[${this.identifier}] Reconnection successful to ${urlWithLocale}`);
|
|
252
|
-
return { success: true };
|
|
253
|
-
} catch (err) {
|
|
254
|
-
logger.error(`[${this.identifier}] Retry connection failed for ${urlWithLocale}:`, err);
|
|
255
|
-
// Reload error page
|
|
256
|
-
try {
|
|
257
|
-
logger.info(`[${this.identifier}] Reloading error page after failed retry...`);
|
|
258
|
-
await this._browserWindow?.loadFile(join(resourcesDir, 'error.html'));
|
|
259
|
-
logger.info(`[${this.identifier}] Error page reloaded.`);
|
|
260
|
-
} catch (loadErr) {
|
|
261
|
-
logger.error('[${this.identifier}] Failed to reload error page:', loadErr);
|
|
262
|
-
}
|
|
263
|
-
return { error: err.message, success: false };
|
|
264
|
-
}
|
|
265
|
-
});
|
|
266
|
-
logger.debug(`[${this.identifier}] Set up retry-connection handler.`);
|
|
267
|
-
} catch (err) {
|
|
268
|
-
logger.error(`[${this.identifier}] Failed to load error page:`, err);
|
|
269
|
-
// If even the error page can't be loaded, at least show a simple error message
|
|
270
|
-
try {
|
|
271
|
-
logger.warn(`[${this.identifier}] Attempting to load fallback error HTML string...`);
|
|
272
|
-
await this._browserWindow.loadURL(
|
|
273
|
-
'data:text/html,<html><body><h1>Loading Failed</h1><p>Unable to connect to server, please restart the application</p></body></html>',
|
|
274
|
-
);
|
|
275
|
-
logger.info(`[${this.identifier}] Fallback error HTML string loaded.`);
|
|
276
|
-
} catch (finalErr) {
|
|
277
|
-
logger.error(`[${this.identifier}] Unable to display any page:`, finalErr);
|
|
278
|
-
}
|
|
188
|
+
private setupReadyToShowListener(browserWindow: BrowserWindow): void {
|
|
189
|
+
logger.debug(`[${this.identifier}] Setting up 'ready-to-show' event listener.`);
|
|
190
|
+
browserWindow.once('ready-to-show', () => {
|
|
191
|
+
logger.debug(`[${this.identifier}] Window 'ready-to-show' event fired.`);
|
|
192
|
+
if (this.options.showOnInit) {
|
|
193
|
+
logger.debug(`Showing window ${this.identifier} because showOnInit is true.`);
|
|
194
|
+
browserWindow.show();
|
|
195
|
+
} else {
|
|
196
|
+
logger.debug(`Window ${this.identifier} not shown because showOnInit is false.`);
|
|
279
197
|
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
loadPlaceholder = async () => {
|
|
284
|
-
logger.debug(`[${this.identifier}] Loading splash screen placeholder`);
|
|
285
|
-
// First load a local HTML loading page
|
|
286
|
-
await this._browserWindow.loadFile(join(resourcesDir, 'splash.html'));
|
|
287
|
-
logger.debug(`[${this.identifier}] Splash screen placeholder loaded.`);
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
show() {
|
|
291
|
-
logger.debug(`Showing window: ${this.identifier}`);
|
|
292
|
-
if (!this._browserWindow.isDestroyed()) this.determineWindowPosition();
|
|
198
|
+
});
|
|
199
|
+
}
|
|
293
200
|
|
|
294
|
-
|
|
201
|
+
private setupCloseListener(browserWindow: BrowserWindow): void {
|
|
202
|
+
logger.debug(`[${this.identifier}] Setting up 'close' event listener.`);
|
|
203
|
+
const closeHandler = this.stateManager.createCloseHandler(browserWindow, {
|
|
204
|
+
onCleanup: () => this.themeManager.cleanup(),
|
|
205
|
+
onHide: () => this.hide(),
|
|
206
|
+
});
|
|
207
|
+
browserWindow.on('close', closeHandler);
|
|
295
208
|
}
|
|
296
209
|
|
|
297
|
-
|
|
298
|
-
const { parentIdentifier } = this.options;
|
|
210
|
+
// ==================== Window Actions ====================
|
|
299
211
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
logger.debug(`[${this.identifier}] Found parent window: ${parentIdentifier}`);
|
|
305
|
-
|
|
306
|
-
const display = screen.getDisplayNearestPoint(parentWin.browserWindow.getContentBounds());
|
|
307
|
-
if (display) {
|
|
308
|
-
const {
|
|
309
|
-
workArea: { x, y, width: displayWidth, height: displayHeight },
|
|
310
|
-
} = display;
|
|
311
|
-
|
|
312
|
-
const { width, height } = this._browserWindow.getContentBounds();
|
|
313
|
-
logger.debug(
|
|
314
|
-
`[${this.identifier}] Display bounds: x=${x}, y=${y}, width=${displayWidth}, height=${displayHeight}`,
|
|
315
|
-
);
|
|
316
|
-
|
|
317
|
-
// Calculate new position
|
|
318
|
-
const newX = Math.floor(Math.max(x + (displayWidth - width) / 2, x));
|
|
319
|
-
const newY = Math.floor(Math.max(y + (displayHeight - height) / 2, y));
|
|
320
|
-
logger.debug(`[${this.identifier}] Calculated position: x=${newX}, y=${newY}`);
|
|
321
|
-
this._browserWindow.setPosition(newX, newY, false);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
212
|
+
show(): void {
|
|
213
|
+
logger.debug(`Showing window: ${this.identifier}`);
|
|
214
|
+
if (!this._browserWindow?.isDestroyed()) {
|
|
215
|
+
this.determineWindowPosition();
|
|
324
216
|
}
|
|
217
|
+
this.browserWindow.show();
|
|
325
218
|
}
|
|
326
219
|
|
|
327
|
-
hide() {
|
|
220
|
+
hide(): void {
|
|
328
221
|
logger.debug(`Hiding window: ${this.identifier}`);
|
|
329
222
|
|
|
330
223
|
// Fix for macOS fullscreen black screen issue
|
|
331
224
|
// See: https://github.com/electron/electron/issues/20263
|
|
332
225
|
if (isMac && this.browserWindow.isFullScreen()) {
|
|
333
|
-
logger.debug(
|
|
334
|
-
`[${this.identifier}] Window is in fullscreen mode, exiting fullscreen before hiding.`,
|
|
335
|
-
);
|
|
226
|
+
logger.debug(`[${this.identifier}] Exiting fullscreen before hiding.`);
|
|
336
227
|
this.browserWindow.once('leave-full-screen', () => {
|
|
337
228
|
this.browserWindow.hide();
|
|
338
229
|
});
|
|
@@ -342,284 +233,215 @@ export default class Browser {
|
|
|
342
233
|
}
|
|
343
234
|
}
|
|
344
235
|
|
|
345
|
-
close() {
|
|
236
|
+
close(): void {
|
|
346
237
|
logger.debug(`Attempting to close window: ${this.identifier}`);
|
|
347
238
|
this.browserWindow.close();
|
|
348
239
|
}
|
|
349
240
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
241
|
+
toggleVisible(): void {
|
|
242
|
+
logger.debug(`Toggling visibility for window: ${this.identifier}`);
|
|
243
|
+
if (this._browserWindow?.isVisible() && this._browserWindow.isFocused()) {
|
|
244
|
+
this.hide();
|
|
245
|
+
} else {
|
|
246
|
+
this._browserWindow?.show();
|
|
247
|
+
this._browserWindow?.focus();
|
|
248
|
+
}
|
|
357
249
|
}
|
|
358
250
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
// When there is this window and it has not been destroyed
|
|
364
|
-
if (this._browserWindow && !this._browserWindow.isDestroyed()) {
|
|
365
|
-
logger.debug(`[${this.identifier}] Returning existing BrowserWindow instance.`);
|
|
366
|
-
return this._browserWindow;
|
|
367
|
-
}
|
|
251
|
+
moveToCenter(): void {
|
|
252
|
+
logger.debug(`Centering window: ${this.identifier}`);
|
|
253
|
+
this._browserWindow?.center();
|
|
254
|
+
}
|
|
368
255
|
|
|
369
|
-
|
|
256
|
+
setWindowSize(boundSize: { height?: number; width?: number }): void {
|
|
257
|
+
logger.debug(`Setting window size for ${this.identifier}: ${JSON.stringify(boundSize)}`);
|
|
258
|
+
const currentBounds = this._browserWindow?.getBounds();
|
|
259
|
+
this._browserWindow?.setBounds({
|
|
260
|
+
height: boundSize.height || currentBounds?.height,
|
|
261
|
+
width: boundSize.width || currentBounds?.width,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
370
264
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
logger.info(`Creating new BrowserWindow instance: ${this.identifier}`);
|
|
376
|
-
logger.debug(`[${this.identifier}] Options for new window: ${JSON.stringify(this.options)}`);
|
|
377
|
-
logger.debug(`[${this.identifier}] Saved window state: ${JSON.stringify(savedState)}`);
|
|
265
|
+
setWindowResizable(resizable: boolean): void {
|
|
266
|
+
logger.debug(`[${this.identifier}] Setting window resizable: ${resizable}`);
|
|
267
|
+
this._browserWindow?.setResizable(resizable);
|
|
268
|
+
}
|
|
378
269
|
|
|
379
|
-
|
|
380
|
-
logger.debug(`[${this.identifier}] Resolved window state: ${JSON.stringify(resolvedState)}`);
|
|
270
|
+
// ==================== Window Position ====================
|
|
381
271
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
backgroundColor: '#00000000',
|
|
386
|
-
darkTheme: this.isDarkMode,
|
|
387
|
-
frame: false,
|
|
388
|
-
height: resolvedState.height,
|
|
389
|
-
show: false,
|
|
390
|
-
title,
|
|
391
|
-
vibrancy: 'sidebar',
|
|
392
|
-
visualEffectState: 'active',
|
|
393
|
-
webPreferences: {
|
|
394
|
-
backgroundThrottling: false,
|
|
395
|
-
contextIsolation: true,
|
|
396
|
-
preload: join(preloadDir, 'index.js'),
|
|
397
|
-
sandbox: false,
|
|
398
|
-
},
|
|
399
|
-
width: resolvedState.width,
|
|
400
|
-
x: resolvedState.x,
|
|
401
|
-
y: resolvedState.y,
|
|
402
|
-
...this.getPlatformThemeConfig(),
|
|
403
|
-
});
|
|
272
|
+
private determineWindowPosition(): void {
|
|
273
|
+
const { parentIdentifier } = this.options;
|
|
274
|
+
if (!parentIdentifier) return;
|
|
404
275
|
|
|
405
|
-
|
|
406
|
-
|
|
276
|
+
// todo: fix ts type
|
|
277
|
+
const parentWin = this.app.browserManager.retrieveByIdentifier(parentIdentifier as any);
|
|
278
|
+
if (!parentWin) return;
|
|
407
279
|
|
|
408
|
-
|
|
409
|
-
this.setupThemeListener();
|
|
410
|
-
logger.debug(`[${this.identifier}] Theme listener setup and applying initial visual effects.`);
|
|
280
|
+
logger.debug(`[${this.identifier}] Found parent window: ${parentIdentifier}`);
|
|
411
281
|
|
|
412
|
-
|
|
413
|
-
|
|
282
|
+
const display = screen.getDisplayNearestPoint(parentWin.browserWindow.getContentBounds());
|
|
283
|
+
if (!display) return;
|
|
414
284
|
|
|
415
|
-
|
|
416
|
-
this.
|
|
417
|
-
// Setup request hook for remote server sync (base URL rewrite + OIDC header)
|
|
418
|
-
this.setupRemoteServerRequestHook(browserWindow);
|
|
285
|
+
const { workArea } = display;
|
|
286
|
+
const { width, height } = this._browserWindow!.getContentBounds();
|
|
419
287
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
this.loadUrl(path).catch((e) => {
|
|
423
|
-
logger.error(`[${this.identifier}] Initial loadUrl error for path '${path}':`, e);
|
|
424
|
-
});
|
|
425
|
-
});
|
|
288
|
+
const newX = Math.floor(Math.max(workArea.x + (workArea.width - width) / 2, workArea.x));
|
|
289
|
+
const newY = Math.floor(Math.max(workArea.y + (workArea.height - height) / 2, workArea.y));
|
|
426
290
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
browserWindow.webContents.openDevTools();
|
|
431
|
-
}
|
|
291
|
+
logger.debug(`[${this.identifier}] Calculated position: x=${newX}, y=${newY}`);
|
|
292
|
+
this._browserWindow!.setPosition(newX, newY, false);
|
|
293
|
+
}
|
|
432
294
|
|
|
433
|
-
|
|
434
|
-
browserWindow.once('ready-to-show', () => {
|
|
435
|
-
logger.debug(`[${this.identifier}] Window 'ready-to-show' event fired.`);
|
|
436
|
-
if (showOnInit) {
|
|
437
|
-
logger.debug(`Showing window ${this.identifier} because showOnInit is true.`);
|
|
438
|
-
browserWindow?.show();
|
|
439
|
-
} else {
|
|
440
|
-
logger.debug(
|
|
441
|
-
`Window ${this.identifier} not shown on 'ready-to-show' because showOnInit is false.`,
|
|
442
|
-
);
|
|
443
|
-
}
|
|
444
|
-
});
|
|
295
|
+
// ==================== Content Loading ====================
|
|
445
296
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
);
|
|
297
|
+
loadPlaceholder = async (): Promise<void> => {
|
|
298
|
+
logger.debug(`[${this.identifier}] Loading splash screen placeholder`);
|
|
299
|
+
await this._browserWindow!.loadFile(join(resourcesDir, 'splash.html'));
|
|
300
|
+
logger.debug(`[${this.identifier}] Splash screen placeholder loaded.`);
|
|
301
|
+
};
|
|
452
302
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
// Save state before quitting
|
|
457
|
-
try {
|
|
458
|
-
const bounds = browserWindow.getBounds();
|
|
459
|
-
const sizeState = {
|
|
460
|
-
height: bounds.height,
|
|
461
|
-
width: bounds.width,
|
|
462
|
-
x: bounds.x,
|
|
463
|
-
y: bounds.y,
|
|
464
|
-
};
|
|
465
|
-
logger.debug(
|
|
466
|
-
`[${this.identifier}] Saving window state on quit: ${JSON.stringify(sizeState)}`,
|
|
467
|
-
);
|
|
468
|
-
this.app.storeManager.set(this.windowStateKey as any, sizeState);
|
|
469
|
-
} catch (error) {
|
|
470
|
-
logger.error(`[${this.identifier}] Failed to save window state on quit:`, error);
|
|
471
|
-
}
|
|
472
|
-
// Need to clean up theme manager
|
|
473
|
-
this.cleanupThemeListener();
|
|
474
|
-
return;
|
|
475
|
-
}
|
|
303
|
+
loadUrl = async (path: string): Promise<void> => {
|
|
304
|
+
const initUrl = await this.app.buildRendererUrl(path);
|
|
305
|
+
const urlWithLocale = this.buildUrlWithLocale(initUrl);
|
|
476
306
|
|
|
477
|
-
|
|
478
|
-
if (this.options.keepAlive) {
|
|
479
|
-
logger.debug(
|
|
480
|
-
`[${this.identifier}] keepAlive is true, preventing default close and hiding window.`,
|
|
481
|
-
);
|
|
482
|
-
// Optionally save state when hiding if desired, but primary save is on actual close/quit
|
|
483
|
-
// try {
|
|
484
|
-
// const bounds = browserWindow.getBounds();
|
|
485
|
-
// logger.debug(`[${this.identifier}] Saving window state on hide: ${JSON.stringify(bounds)}`);
|
|
486
|
-
// this.app.storeManager.set(this.windowStateKey, bounds);
|
|
487
|
-
// } catch (error) {
|
|
488
|
-
// logger.error(`[${this.identifier}] Failed to save window state on hide:`, error);
|
|
489
|
-
// }
|
|
490
|
-
e.preventDefault();
|
|
491
|
-
this.hide();
|
|
492
|
-
} else {
|
|
493
|
-
// Window is actually closing (not keepAlive)
|
|
494
|
-
logger.debug(
|
|
495
|
-
`[${this.identifier}] keepAlive is false, allowing window to close. Saving state...`,
|
|
496
|
-
);
|
|
497
|
-
try {
|
|
498
|
-
const bounds = browserWindow.getBounds();
|
|
499
|
-
const sizeState = {
|
|
500
|
-
height: bounds.height,
|
|
501
|
-
width: bounds.width,
|
|
502
|
-
x: bounds.x,
|
|
503
|
-
y: bounds.y,
|
|
504
|
-
};
|
|
505
|
-
logger.debug(
|
|
506
|
-
`[${this.identifier}] Saving window state on close: ${JSON.stringify(sizeState)}`,
|
|
507
|
-
);
|
|
508
|
-
this.app.storeManager.set(this.windowStateKey as any, sizeState);
|
|
509
|
-
} catch (error) {
|
|
510
|
-
logger.error(`[${this.identifier}] Failed to save window state on close:`, error);
|
|
511
|
-
}
|
|
512
|
-
// Need to clean up theme manager
|
|
513
|
-
this.cleanupThemeListener();
|
|
514
|
-
}
|
|
515
|
-
});
|
|
307
|
+
console.log('[Browser] initUrl', urlWithLocale);
|
|
516
308
|
|
|
517
|
-
|
|
518
|
-
|
|
309
|
+
try {
|
|
310
|
+
logger.debug(`[${this.identifier}] Attempting to load URL: ${urlWithLocale}`);
|
|
311
|
+
await this._browserWindow!.loadURL(urlWithLocale);
|
|
312
|
+
logger.debug(`[${this.identifier}] Successfully loaded URL: ${urlWithLocale}`);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
logger.error(`[${this.identifier}] Failed to load URL (${urlWithLocale}):`, error);
|
|
315
|
+
await this.handleLoadError(urlWithLocale);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
private buildUrlWithLocale(initUrl: string): string {
|
|
320
|
+
const storedLocale = this.app.storeManager.get('locale', 'auto');
|
|
321
|
+
if (storedLocale && storedLocale !== 'auto') {
|
|
322
|
+
return `${initUrl}${initUrl.includes('?') ? '&' : '?'}lng=${storedLocale}`;
|
|
323
|
+
}
|
|
324
|
+
return initUrl;
|
|
519
325
|
}
|
|
520
326
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
327
|
+
private async handleLoadError(urlWithLocale: string): Promise<void> {
|
|
328
|
+
try {
|
|
329
|
+
logger.info(`[${this.identifier}] Attempting to load error page...`);
|
|
330
|
+
await this._browserWindow!.loadFile(join(resourcesDir, 'error.html'));
|
|
331
|
+
logger.info(`[${this.identifier}] Error page loaded successfully.`);
|
|
332
|
+
|
|
333
|
+
this.setupRetryHandler(urlWithLocale);
|
|
334
|
+
} catch (err) {
|
|
335
|
+
logger.error(`[${this.identifier}] Failed to load error page:`, err);
|
|
336
|
+
await this.loadFallbackError();
|
|
337
|
+
}
|
|
524
338
|
}
|
|
525
339
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
340
|
+
private setupRetryHandler(urlWithLocale: string): void {
|
|
341
|
+
ipcMain.removeHandler('retry-connection');
|
|
342
|
+
logger.debug(`[${this.identifier}] Removed existing retry-connection handler if any.`);
|
|
343
|
+
|
|
344
|
+
ipcMain.handle('retry-connection', async () => {
|
|
345
|
+
logger.info(`[${this.identifier}] Retry connection requested for: ${urlWithLocale}`);
|
|
346
|
+
try {
|
|
347
|
+
await this._browserWindow?.loadURL(urlWithLocale);
|
|
348
|
+
logger.info(`[${this.identifier}] Reconnection successful to ${urlWithLocale}`);
|
|
349
|
+
return { success: true };
|
|
350
|
+
} catch (err: any) {
|
|
351
|
+
logger.error(`[${this.identifier}] Retry connection failed:`, err);
|
|
352
|
+
try {
|
|
353
|
+
await this._browserWindow?.loadFile(join(resourcesDir, 'error.html'));
|
|
354
|
+
} catch (loadErr) {
|
|
355
|
+
logger.error(`[${this.identifier}] Failed to reload error page:`, loadErr);
|
|
356
|
+
}
|
|
357
|
+
return { error: err.message, success: false };
|
|
358
|
+
}
|
|
534
359
|
});
|
|
360
|
+
logger.debug(`[${this.identifier}] Set up retry-connection handler.`);
|
|
535
361
|
}
|
|
536
362
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
363
|
+
private async loadFallbackError(): Promise<void> {
|
|
364
|
+
try {
|
|
365
|
+
logger.warn(`[${this.identifier}] Attempting to load fallback error HTML string...`);
|
|
366
|
+
await this._browserWindow!.loadURL(
|
|
367
|
+
'data:text/html,<html><body><h1>Loading Failed</h1><p>Unable to connect to server, please restart the application</p></body></html>',
|
|
368
|
+
);
|
|
369
|
+
logger.info(`[${this.identifier}] Fallback error HTML string loaded.`);
|
|
370
|
+
} catch (finalErr) {
|
|
371
|
+
logger.error(`[${this.identifier}] Unable to display any page:`, finalErr);
|
|
372
|
+
}
|
|
540
373
|
}
|
|
541
374
|
|
|
542
|
-
|
|
543
|
-
if (this._browserWindow.isDestroyed()) return;
|
|
375
|
+
// ==================== Communication ====================
|
|
544
376
|
|
|
377
|
+
broadcast = <T extends MainBroadcastEventKey>(
|
|
378
|
+
channel: T,
|
|
379
|
+
data?: MainBroadcastParams<T>,
|
|
380
|
+
): void => {
|
|
381
|
+
if (this._browserWindow?.isDestroyed()) return;
|
|
545
382
|
logger.debug(`Broadcasting to window ${this.identifier}, channel: ${channel}`);
|
|
546
|
-
this._browserWindow
|
|
383
|
+
this._browserWindow!.webContents.send(channel, data);
|
|
547
384
|
};
|
|
548
385
|
|
|
549
|
-
|
|
550
|
-
logger.debug(`Toggling visibility for window: ${this.identifier}`);
|
|
551
|
-
if (this._browserWindow.isVisible() && this._browserWindow.isFocused()) {
|
|
552
|
-
this.hide(); // Use the hide() method which handles fullscreen
|
|
553
|
-
} else {
|
|
554
|
-
this._browserWindow.show();
|
|
555
|
-
this._browserWindow.focus();
|
|
556
|
-
}
|
|
557
|
-
}
|
|
386
|
+
// ==================== Theme (Delegated) ====================
|
|
558
387
|
|
|
559
388
|
/**
|
|
560
|
-
*
|
|
389
|
+
* Handle application theme mode change (called from BrowserManager)
|
|
390
|
+
*/
|
|
391
|
+
handleAppThemeChange = (): void => {
|
|
392
|
+
this.themeManager.handleAppThemeChange();
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Manually reapply visual effects
|
|
561
397
|
*/
|
|
562
398
|
reapplyVisualEffects(): void {
|
|
563
|
-
|
|
564
|
-
this.applyVisualEffects();
|
|
399
|
+
this.themeManager.reapplyVisualEffects();
|
|
565
400
|
}
|
|
566
401
|
|
|
402
|
+
// ==================== Network Setup ====================
|
|
403
|
+
|
|
567
404
|
/**
|
|
568
405
|
* Setup CORS bypass for ALL requests
|
|
569
|
-
* In production, the renderer uses app://next protocol which triggers CORS
|
|
570
|
-
* This completely bypasses CORS by:
|
|
571
|
-
* 1. Removing Origin header from requests (prevents OPTIONS preflight)
|
|
572
|
-
* 2. Adding proper CORS response headers using the stored origin value
|
|
406
|
+
* In production, the renderer uses app://next protocol which triggers CORS
|
|
573
407
|
*/
|
|
574
408
|
private setupCORSBypass(browserWindow: BrowserWindow): void {
|
|
575
409
|
logger.debug(`[${this.identifier}] Setting up CORS bypass for all requests`);
|
|
576
410
|
|
|
577
411
|
const session = browserWindow.webContents.session;
|
|
578
|
-
|
|
579
|
-
// Store origin values for each request ID
|
|
580
412
|
const originMap = new Map<number, string>();
|
|
581
413
|
|
|
582
|
-
// Remove Origin header and store it for later use
|
|
583
414
|
session.webRequest.onBeforeSendHeaders((details, callback) => {
|
|
584
415
|
const requestHeaders = { ...details.requestHeaders };
|
|
585
416
|
|
|
586
|
-
// Store and remove Origin header to prevent CORS preflight
|
|
587
417
|
if (requestHeaders['Origin']) {
|
|
588
418
|
originMap.set(details.id, requestHeaders['Origin']);
|
|
589
419
|
delete requestHeaders['Origin'];
|
|
590
|
-
logger.debug(
|
|
591
|
-
`[${this.identifier}] Removed Origin header for: ${details.url} (stored: ${requestHeaders['Origin']})`,
|
|
592
|
-
);
|
|
420
|
+
logger.debug(`[${this.identifier}] Removed Origin header for: ${details.url}`);
|
|
593
421
|
}
|
|
594
422
|
|
|
595
423
|
callback({ requestHeaders });
|
|
596
424
|
});
|
|
597
425
|
|
|
598
|
-
// Add CORS headers to ALL responses using stored origin
|
|
599
426
|
session.webRequest.onHeadersReceived((details, callback) => {
|
|
600
427
|
const responseHeaders = details.responseHeaders || {};
|
|
601
|
-
|
|
602
|
-
// Get the original origin from our map, fallback to default
|
|
603
428
|
const origin = originMap.get(details.id) || '*';
|
|
604
429
|
|
|
605
|
-
//
|
|
606
|
-
responseHeaders
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
430
|
+
// Force set CORS headers (replace existing to avoid duplicates from case-insensitive keys)
|
|
431
|
+
setResponseHeader(responseHeaders, 'Access-Control-Allow-Origin', origin);
|
|
432
|
+
setResponseHeader(
|
|
433
|
+
responseHeaders,
|
|
434
|
+
'Access-Control-Allow-Methods',
|
|
435
|
+
'GET, POST, PUT, DELETE, OPTIONS, PATCH',
|
|
436
|
+
);
|
|
437
|
+
setResponseHeader(responseHeaders, 'Access-Control-Allow-Headers', '*');
|
|
438
|
+
setResponseHeader(responseHeaders, 'Access-Control-Allow-Credentials', 'true');
|
|
610
439
|
|
|
611
|
-
// Clean up the stored origin after response
|
|
612
440
|
originMap.delete(details.id);
|
|
613
441
|
|
|
614
|
-
// For OPTIONS requests, add preflight cache and override status
|
|
615
442
|
if (details.method === 'OPTIONS') {
|
|
616
|
-
responseHeaders
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
callback({
|
|
620
|
-
responseHeaders,
|
|
621
|
-
statusLine: 'HTTP/1.1 200 OK',
|
|
622
|
-
});
|
|
443
|
+
setResponseHeader(responseHeaders, 'Access-Control-Max-Age', '86400');
|
|
444
|
+
callback({ responseHeaders, statusLine: 'HTTP/1.1 200 OK' });
|
|
623
445
|
return;
|
|
624
446
|
}
|
|
625
447
|
|
|
@@ -630,10 +452,9 @@ export default class Browser {
|
|
|
630
452
|
}
|
|
631
453
|
|
|
632
454
|
/**
|
|
633
|
-
* Rewrite tRPC requests to remote server and inject OIDC token
|
|
634
|
-
* Replaces the previous proxyTRPCRequest IPC forwarding.
|
|
455
|
+
* Rewrite tRPC requests to remote server and inject OIDC token
|
|
635
456
|
*/
|
|
636
|
-
private setupRemoteServerRequestHook(browserWindow: BrowserWindow) {
|
|
457
|
+
private setupRemoteServerRequestHook(browserWindow: BrowserWindow): void {
|
|
637
458
|
const session = browserWindow.webContents.session;
|
|
638
459
|
const remoteServerConfigCtr = this.app.getController(RemoteServerConfigCtr);
|
|
639
460
|
|