Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ulixee
GitHub Repository: ulixee/secret-agent
Path: blob/main/replay/backend/models/Window.ts
1030 views
1
import { app, BrowserWindow, Rectangle } from 'electron';
2
import { resolve } from 'path';
3
import Application from '../Application';
4
import ReplayApi from '~backend/api';
5
import storage from '../storage';
6
import AppView from './AppView';
7
import ReplayView from './ReplayView';
8
import IWindowLocation, { InternalLocations } from '~shared/interfaces/IWindowLocation';
9
import ViewBackend from '~backend/models/ViewBackend';
10
import IReplayMeta from '~shared/interfaces/IReplayMeta';
11
import generateContextMenu from '~backend/menus/generateContextMenu';
12
import { ISessionTab } from '~shared/interfaces/ISaSession';
13
14
export default class Window {
15
public static list: Window[] = [];
16
public static current: Window;
17
public activeView: ViewBackend;
18
public browserWindow: BrowserWindow;
19
20
public get replayApi() {
21
if (this.isReplayActive) return this.replayView.replayApi;
22
}
23
24
public get isReplayActive() {
25
return this.activeView instanceof ReplayView;
26
}
27
28
public readonly replayView: ReplayView;
29
public readonly appView: AppView;
30
31
private readonly windowState: any = {};
32
33
private readonly navHistory: { location?: IWindowLocation; replayMeta?: IReplayMeta }[] = [];
34
35
private navCursor = -1;
36
37
private _fullscreen = false;
38
private isReady: Promise<void>;
39
40
protected constructor(state: { replayApi?: ReplayApi; location?: IWindowLocation }) {
41
this.browserWindow = new BrowserWindow({
42
minWidth: 400,
43
minHeight: 450,
44
width: 900,
45
height: 700,
46
transparent: false,
47
titleBarStyle: 'hiddenInset',
48
webPreferences: {
49
nodeIntegration: true,
50
contextIsolation: false,
51
javascript: true,
52
enableRemoteModule: true,
53
},
54
icon: resolve(app.getAppPath(), 'logo.png'),
55
show: false,
56
});
57
58
this.loadSavedState();
59
this.browserWindow.show();
60
61
this.bindListenersToWindow();
62
63
this.replayView = new ReplayView(this);
64
this.appView = new AppView(this);
65
66
this.isReady = this.load(state);
67
}
68
69
public get id() {
70
return this.browserWindow.id;
71
}
72
73
public get webContents() {
74
return this.browserWindow.webContents;
75
}
76
77
public get fullscreen() {
78
return this._fullscreen;
79
}
80
81
public set fullscreen(val: boolean) {
82
this._fullscreen = val;
83
this.fixBounds();
84
}
85
86
public sendToRenderer(channel: string, ...args: any[]) {
87
this.webContents.send(channel, ...args);
88
}
89
90
public goBack() {
91
if (this.hasBack()) {
92
return this.goToHistory(this.navCursor - 1);
93
}
94
}
95
96
public goForward() {
97
if (this.hasNext()) {
98
return this.goToHistory(this.navCursor + 1);
99
}
100
}
101
102
public hasBack() {
103
return this.navCursor > 0;
104
}
105
106
public hasNext() {
107
return this.navCursor + 1 < this.navHistory.length;
108
}
109
110
public async openAppLocation(location: IWindowLocation, navigateToHistoryIdx?: number) {
111
console.log('Navigating to %s', location);
112
113
this.replayView.detach();
114
115
this.logHistory({ location }, navigateToHistoryIdx);
116
117
this.activeView = this.appView;
118
await this.appView.load(location);
119
120
this.sendToRenderer('location:updated', {
121
location,
122
hasNext: this.hasNext(),
123
hasBack: this.hasBack(),
124
});
125
}
126
127
public async openReplayApi(replayApi: ReplayApi, navigateToHistoryIdx?: number) {
128
console.log('Navigating to Replay Api (%s)', replayApi.apiHost);
129
130
this.appView.detach();
131
132
this.logHistory({ replayMeta: replayApi.saSession }, navigateToHistoryIdx);
133
134
this.activeView = this.replayView;
135
await this.replayView.load(replayApi);
136
await this.fixBounds();
137
138
this.sendToRenderer('location:updated', {
139
saSession: replayApi.saSession,
140
hasNext: this.hasNext(),
141
hasBack: this.hasBack(),
142
});
143
}
144
145
public addRelatedSession(related: { id: string; name: string }) {
146
if (
147
!this.replayApi ||
148
this.replayApi.saSession.relatedSessions.some(x => x.id === related.id)
149
) {
150
return;
151
}
152
153
this.replayApi.saSession.relatedSessions.push(related);
154
this.sendToRenderer('location:updated', {
155
saSession: this.replayApi.saSession,
156
hasNext: this.hasNext(),
157
hasBack: this.hasBack(),
158
});
159
}
160
161
public onReplayTabChange(tab: ISessionTab) {
162
this.sendToRenderer('replay:tab', tab);
163
}
164
165
public setAddressBarUrl(url: string) {
166
this.sendToRenderer('replay:page-url', url);
167
}
168
169
public setActiveTabId(id: number) {
170
this.sendToRenderer('replay:active-tab', id);
171
}
172
173
public async loadReplayTab(id: number) {
174
await this.replayView.loadTab(id);
175
await this.fixBounds();
176
}
177
178
public replayOnFocus() {
179
this.replayView.start();
180
}
181
182
public async fixBounds() {
183
const newBounds = await this.getAvailableBounds();
184
if (this.isReplayActive) {
185
this.replayView.fixBounds(newBounds);
186
} else {
187
this.appView.fixBounds(newBounds);
188
}
189
}
190
191
public async getAvailableBounds(): Promise<Rectangle> {
192
const { width, height } = this.browserWindow.getContentBounds();
193
const toolbarContentHeight = await this.getHeaderHeight();
194
195
const bounds = {
196
x: 0,
197
y: this.fullscreen ? 0 : toolbarContentHeight + 1,
198
width,
199
height: this.fullscreen ? height : height - toolbarContentHeight,
200
};
201
if (this.isReplayActive) {
202
bounds.height -= this.replayView.toolbarHeight;
203
}
204
return bounds;
205
}
206
207
public hideMessageOverlay(messageId: string) {
208
if (messageId === ReplayView.MESSAGE_HANG_ID) {
209
this.replayView.replayApi.showUnresponsiveMessage = false;
210
}
211
}
212
213
protected async load(state: { replayApi?: ReplayApi; location?: IWindowLocation }) {
214
const { replayApi, location } = state || {};
215
await this.browserWindow.loadURL(Application.instance.getPageUrl('header'));
216
217
// resize the BrowserView's height when the toolbar height changes
218
await this.webContents.executeJavaScript(`
219
const {ipcRenderer} = require('electron');
220
const resizeObserver = new ResizeObserver(() => {
221
ipcRenderer.send('resize-height');
222
});
223
const elem = document.querySelector('.HeaderPage');
224
resizeObserver.observe(elem);
225
`);
226
227
if (replayApi) {
228
return this.openReplayApi(replayApi);
229
}
230
return this.openAppLocation(location ?? InternalLocations.Dashboard);
231
}
232
233
/////// HISTORY /////////////////////////////////////////////////////////////////////////////////////////////////////
234
235
private async goToHistory(index: number) {
236
const history = this.navHistory[index];
237
if (history.location) return this.openAppLocation(history.location, index);
238
239
const api = await ReplayApi.connect(history.replayMeta);
240
return this.openReplayApi(api, index);
241
}
242
243
private logHistory(
244
history: { replayMeta?: IReplayMeta; location?: IWindowLocation },
245
historyIdx?: number,
246
) {
247
if (historyIdx !== undefined) {
248
this.navCursor = historyIdx;
249
} else {
250
this.navHistory.length = this.navCursor + 1;
251
this.navCursor = this.navHistory.length;
252
this.navHistory.push(history);
253
}
254
}
255
256
private async getHeaderHeight() {
257
return await this.webContents.executeJavaScript(
258
`document.querySelector('.HeaderPage').offsetHeight`,
259
);
260
}
261
262
private bindListenersToWindow() {
263
this.browserWindow.on('enter-full-screen', () => {
264
this.sendToRenderer('fullscreen', true);
265
this.fixBounds();
266
});
267
268
this.browserWindow.on('leave-full-screen', () => {
269
this.sendToRenderer('fullscreen', false);
270
this.fixBounds();
271
});
272
273
this.browserWindow.on('enter-html-full-screen', () => {
274
this.fullscreen = true;
275
this.sendToRenderer('html-fullscreen', true);
276
});
277
278
this.browserWindow.on('leave-html-full-screen', () => {
279
this.fullscreen = false;
280
this.sendToRenderer('html-fullscreen', false);
281
});
282
283
this.browserWindow.on('scroll-touch-begin', () => {
284
this.sendToRenderer('scroll-touch-begin');
285
});
286
287
this.browserWindow.on('scroll-touch-end', () => {
288
this.activeView.webContents.send('scroll-touch-end');
289
this.sendToRenderer('scroll-touch-end');
290
});
291
292
this.browserWindow.on('focus', () => {
293
Window.current = this;
294
});
295
296
// Update window bounds on resize and on move when window is not maximized.
297
this.browserWindow.on('resize', () => {
298
if (!this.browserWindow.isMaximized()) {
299
this.windowState.bounds = this.browserWindow.getBounds();
300
}
301
});
302
303
this.browserWindow.on('move', () => {
304
if (!this.browserWindow.isMaximized()) {
305
this.windowState.bounds = this.browserWindow.getBounds();
306
}
307
});
308
309
this.browserWindow.on('maximize', () => this.resize());
310
this.browserWindow.on('restore', () => this.resize());
311
this.browserWindow.on('unmaximize', () => this.resize());
312
this.browserWindow.on('close', () => this.close());
313
314
this.webContents.on('context-menu', (e, params) => {
315
generateContextMenu(params, this.webContents).popup();
316
});
317
318
this.webContents.on('ipc-message', (e, message) => {
319
if (message === 'resize-height') {
320
this.fixBounds();
321
}
322
});
323
}
324
325
private loadSavedState() {
326
try {
327
const windowState = storage.windowState;
328
Object.assign(this.windowState, windowState);
329
330
if (Window.list.length > 0) {
331
const last = Window.list[Window.list.length - 1];
332
this.windowState.bounds = last.browserWindow.getBounds();
333
this.windowState.bounds.x += 10;
334
this.windowState.bounds.y += 10;
335
}
336
337
this.browserWindow.setBounds({ ...this.windowState.bounds });
338
if (this.windowState.isMaximized) this.browserWindow.maximize();
339
if (this.windowState.isFullscreen) this.browserWindow.setFullScreen(true);
340
} catch (e) {
341
storage.windowState = {};
342
storage.persistAll();
343
}
344
}
345
346
private resize() {
347
setImmediate(() => this.fixBounds());
348
}
349
350
private close() {
351
this.windowState.isMaximized = this.browserWindow.isMaximized();
352
this.windowState.isFullscreen = this.browserWindow.isFullScreen();
353
storage.windowState = this.windowState;
354
storage.persistAll();
355
this.browserWindow.setBrowserView(null);
356
this.replayView.destroy();
357
this.replayView.destroy();
358
this.appView.destroy();
359
360
Window.list = Window.list.filter(x => x.browserWindow.id !== this.browserWindow.id);
361
if (this.webContents.isDevToolsOpened) {
362
this.webContents.closeDevTools();
363
}
364
}
365
366
public static create(
367
initialLocation: { replayApi?: ReplayApi; location?: IWindowLocation } = {},
368
) {
369
const window = new Window(initialLocation);
370
this.list.push(window);
371
return window;
372
}
373
374
public static noneOpen() {
375
return this.list.filter(Boolean).length === 0;
376
}
377
}
378
379