diff options
author | Markus Hatvan <markus_hatvan@aon.at> | 2021-10-10 10:42:20 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-10-10 10:42:20 +0200 |
commit | a673c2b577a31437c91a0b498e69b0753d62aa58 (patch) | |
tree | 50d52df3ff9d78828ca06a24ef438e31b062ca56 /src/index.ts | |
parent | remove unused 'conventional-changelog' npm package. Change the url for the ch... (diff) | |
download | ferdium-app-a673c2b577a31437c91a0b498e69b0753d62aa58.tar.gz ferdium-app-a673c2b577a31437c91a0b498e69b0753d62aa58.tar.zst ferdium-app-a673c2b577a31437c91a0b498e69b0753d62aa58.zip |
chore: convert index file to TS (#2049)
Diffstat (limited to 'src/index.ts')
-rw-r--r-- | src/index.ts | 693 |
1 files changed, 693 insertions, 0 deletions
diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 000000000..7beaa86f6 --- /dev/null +++ b/src/index.ts | |||
@@ -0,0 +1,693 @@ | |||
1 | /* eslint-disable import/first */ | ||
2 | |||
3 | import { | ||
4 | app, | ||
5 | BrowserWindow, | ||
6 | globalShortcut, | ||
7 | ipcMain, | ||
8 | session, | ||
9 | dialog, | ||
10 | } from 'electron'; | ||
11 | |||
12 | import { emptyDirSync, ensureFileSync } from 'fs-extra'; | ||
13 | import { join } from 'path'; | ||
14 | import windowStateKeeper from 'electron-window-state'; | ||
15 | import ms from 'ms'; | ||
16 | import { initializeRemote } from './electron-util'; | ||
17 | import { enforceMacOSAppLocation } from './enforce-macos-app-location'; | ||
18 | |||
19 | initializeRemote(); | ||
20 | |||
21 | import { DEFAULT_APP_SETTINGS, DEFAULT_WINDOW_OPTIONS } from './config'; | ||
22 | |||
23 | import { isMac, isWindows, isLinux, altKey } from './environment'; | ||
24 | import { | ||
25 | isDevMode, | ||
26 | aboutAppDetails, | ||
27 | userDataRecipesPath, | ||
28 | userDataPath, | ||
29 | } from './environment-remote'; | ||
30 | import { ifUndefinedBoolean } from './jsUtils'; | ||
31 | |||
32 | import { mainIpcHandler as basicAuthHandler } from './features/basicAuth'; | ||
33 | import ipcApi from './electron/ipc-api'; | ||
34 | import Tray from './lib/Tray'; | ||
35 | import DBus from './lib/DBus'; | ||
36 | import Settings from './electron/Settings'; | ||
37 | import handleDeepLink from './electron/deepLinking'; | ||
38 | import { isPositionValid } from './electron/windowUtils'; | ||
39 | // @ts-expect-error Cannot find module './package.json' or its corresponding type declarations. | ||
40 | import { appId } from './package.json'; | ||
41 | import './electron/exception'; | ||
42 | |||
43 | import { asarPath } from './helpers/asar-helpers'; | ||
44 | import { openExternalUrl } from './helpers/url-helpers'; | ||
45 | import userAgent from './helpers/userAgent-helpers'; | ||
46 | |||
47 | const debug = require('debug')('Ferdi:App'); | ||
48 | |||
49 | // Globally set useragent to fix user agent override in service workers | ||
50 | debug('Set userAgent to ', userAgent()); | ||
51 | app.userAgentFallback = userAgent(); | ||
52 | |||
53 | // Keep a global reference of the window object, if you don't, the window will | ||
54 | // be closed automatically when the JavaScript object is garbage collected. | ||
55 | let mainWindow: BrowserWindow | undefined; | ||
56 | let willQuitApp = false; | ||
57 | |||
58 | // Register methods to be called once the window has been loaded. | ||
59 | let onDidLoadFns: any[] | null = []; | ||
60 | |||
61 | function onDidLoad(fn: { | ||
62 | (window: BrowserWindow): void; | ||
63 | (window: BrowserWindow): void; | ||
64 | (window: BrowserWindow): void; | ||
65 | (arg0: BrowserWindow): void; | ||
66 | }) { | ||
67 | if (onDidLoadFns) { | ||
68 | onDidLoadFns.push(fn); | ||
69 | } else if (mainWindow) { | ||
70 | fn(mainWindow); | ||
71 | } | ||
72 | } | ||
73 | |||
74 | // Ensure that the recipe directory exists | ||
75 | emptyDirSync(userDataRecipesPath('temp')); | ||
76 | ensureFileSync(userDataPath('window-state.json')); | ||
77 | |||
78 | // Set App ID for Windows | ||
79 | if (isWindows) { | ||
80 | app.setAppUserModelId(appId); | ||
81 | } | ||
82 | |||
83 | // Initialize Settings | ||
84 | const settings = new Settings('app', DEFAULT_APP_SETTINGS); | ||
85 | const proxySettings = new Settings('proxy'); | ||
86 | |||
87 | const retrieveSettingValue = (key: string, defaultValue: boolean) => | ||
88 | ifUndefinedBoolean(settings.get(key), defaultValue); | ||
89 | |||
90 | if (retrieveSettingValue('sentry', DEFAULT_APP_SETTINGS.sentry)) { | ||
91 | // eslint-disable-next-line global-require | ||
92 | require('./sentry'); | ||
93 | } | ||
94 | |||
95 | const liftSingleInstanceLock = retrieveSettingValue( | ||
96 | 'liftSingleInstanceLock', | ||
97 | false, | ||
98 | ); | ||
99 | |||
100 | // Force single window | ||
101 | const gotTheLock = liftSingleInstanceLock | ||
102 | ? true | ||
103 | : app.requestSingleInstanceLock(); | ||
104 | if (!gotTheLock) { | ||
105 | app.quit(); | ||
106 | } else { | ||
107 | app.on('second-instance', (_event, argv) => { | ||
108 | // Someone tried to run a second instance, we should focus our window. | ||
109 | if (mainWindow) { | ||
110 | if (!mainWindow.isVisible()) { | ||
111 | mainWindow.show(); | ||
112 | } | ||
113 | if (mainWindow.isMinimized()) { | ||
114 | mainWindow.restore(); | ||
115 | } | ||
116 | mainWindow.focus(); | ||
117 | |||
118 | if (isWindows) { | ||
119 | onDidLoad((window: BrowserWindow) => { | ||
120 | // Keep only command line / deep linked arguments | ||
121 | const url = argv.slice(1); | ||
122 | if (url) { | ||
123 | handleDeepLink(window, url.toString()); | ||
124 | } | ||
125 | |||
126 | if (argv.includes('--reset-window')) { | ||
127 | // Needs to be delayed to not interfere with mainWindow.restore(); | ||
128 | setTimeout(() => { | ||
129 | debug('Resetting windows via Task'); | ||
130 | window.setPosition( | ||
131 | DEFAULT_WINDOW_OPTIONS.x + 100, | ||
132 | DEFAULT_WINDOW_OPTIONS.y + 100, | ||
133 | ); | ||
134 | window.setSize( | ||
135 | DEFAULT_WINDOW_OPTIONS.width, | ||
136 | DEFAULT_WINDOW_OPTIONS.height, | ||
137 | ); | ||
138 | }, 1); | ||
139 | } else if (argv.includes('--quit')) { | ||
140 | // Needs to be delayed to not interfere with mainWindow.restore(); | ||
141 | setTimeout(() => { | ||
142 | debug('Quitting Ferdi via Task'); | ||
143 | app.quit(); | ||
144 | }, 1); | ||
145 | } | ||
146 | }); | ||
147 | } | ||
148 | } | ||
149 | }); | ||
150 | } | ||
151 | |||
152 | // Fix Unity indicator issue | ||
153 | // https://github.com/electron/electron/issues/9046 | ||
154 | if ( | ||
155 | isLinux && | ||
156 | process.env.XDG_CURRENT_DESKTOP && | ||
157 | ['Pantheon', 'Unity:Unity7'].includes(process.env.XDG_CURRENT_DESKTOP) | ||
158 | ) { | ||
159 | process.env.XDG_CURRENT_DESKTOP = 'Unity'; | ||
160 | } | ||
161 | |||
162 | // Disable GPU acceleration | ||
163 | if (!retrieveSettingValue('enableGPUAcceleration', false)) { | ||
164 | debug('Disable GPU Acceleration'); | ||
165 | app.disableHardwareAcceleration(); | ||
166 | } | ||
167 | |||
168 | app.setAboutPanelOptions({ | ||
169 | applicationVersion: aboutAppDetails(), | ||
170 | version: '', | ||
171 | }); | ||
172 | |||
173 | const createWindow = () => { | ||
174 | // Remember window size | ||
175 | const mainWindowState = windowStateKeeper({ | ||
176 | defaultWidth: DEFAULT_WINDOW_OPTIONS.width, | ||
177 | defaultHeight: DEFAULT_WINDOW_OPTIONS.height, | ||
178 | maximize: true, // Automatically maximizes the window, if it was last closed maximized | ||
179 | fullScreen: true, // Automatically restores the window to full screen, if it was last closed full screen | ||
180 | }); | ||
181 | |||
182 | let posX = mainWindowState.x || DEFAULT_WINDOW_OPTIONS.x; | ||
183 | let posY = mainWindowState.y || DEFAULT_WINDOW_OPTIONS.y; | ||
184 | |||
185 | if (!isPositionValid({ x: posX, y: posY })) { | ||
186 | debug('Window is out of screen bounds, resetting window'); | ||
187 | posX = DEFAULT_WINDOW_OPTIONS.x; | ||
188 | posY = DEFAULT_WINDOW_OPTIONS.y; | ||
189 | } | ||
190 | |||
191 | // Create the browser window. | ||
192 | const backgroundColor = retrieveSettingValue('darkMode', false) | ||
193 | ? '#1E1E1E' | ||
194 | : settings.get('accentColor'); | ||
195 | |||
196 | mainWindow = new BrowserWindow({ | ||
197 | x: posX, | ||
198 | y: posY, | ||
199 | width: mainWindowState.width, | ||
200 | height: mainWindowState.height, | ||
201 | minWidth: 600, | ||
202 | minHeight: 500, | ||
203 | show: false, | ||
204 | titleBarStyle: isMac ? 'hidden' : 'default', | ||
205 | frame: isLinux, | ||
206 | backgroundColor, | ||
207 | webPreferences: { | ||
208 | spellcheck: retrieveSettingValue( | ||
209 | 'enableSpellchecking', | ||
210 | DEFAULT_APP_SETTINGS.enableSpellchecking, | ||
211 | ), | ||
212 | nodeIntegration: true, | ||
213 | contextIsolation: false, | ||
214 | webviewTag: true, | ||
215 | preload: join(__dirname, 'sentry.js'), | ||
216 | // @ts-expect-error Object literal may only specify known properties, and 'enableRemoteModule' does not exist in type 'WebPreferences'. | ||
217 | enableRemoteModule: true, | ||
218 | }, | ||
219 | }); | ||
220 | |||
221 | app.on('web-contents-created', (_e, contents) => { | ||
222 | if (contents.getType() === 'webview') { | ||
223 | contents.on('new-window', event => { | ||
224 | event.preventDefault(); | ||
225 | }); | ||
226 | } | ||
227 | }); | ||
228 | |||
229 | mainWindow.webContents.on('did-finish-load', () => { | ||
230 | const fns = onDidLoadFns; | ||
231 | onDidLoadFns = null; | ||
232 | |||
233 | if (!fns) return; | ||
234 | |||
235 | for (const fn of fns) { | ||
236 | fn(mainWindow); | ||
237 | } | ||
238 | }); | ||
239 | |||
240 | // Initialize System Tray | ||
241 | const trayIcon: Tray = new Tray(); | ||
242 | |||
243 | // Initialize DBus interface | ||
244 | const dbus = new DBus(trayIcon); | ||
245 | |||
246 | // Initialize ipcApi | ||
247 | ipcApi({ | ||
248 | mainWindow, | ||
249 | settings: { | ||
250 | app: settings, | ||
251 | proxy: proxySettings, | ||
252 | }, | ||
253 | trayIcon, | ||
254 | }); | ||
255 | |||
256 | // Connect to the DBus after ipcApi took care of the System Tray | ||
257 | dbus.start(); | ||
258 | |||
259 | // Manage Window State | ||
260 | mainWindowState.manage(mainWindow); | ||
261 | |||
262 | // and load the index.html of the app. | ||
263 | mainWindow.loadURL(`file://${__dirname}/index.html`); | ||
264 | |||
265 | // Open the DevTools. | ||
266 | if (isDevMode || process.argv.includes('--devtools')) { | ||
267 | mainWindow.webContents.openDevTools(); | ||
268 | } | ||
269 | |||
270 | // Windows deep linking handling on app launch | ||
271 | if (isWindows) { | ||
272 | onDidLoad((window: BrowserWindow) => { | ||
273 | const url = process.argv.slice(1); | ||
274 | if (url) { | ||
275 | handleDeepLink(window, url.toString()); | ||
276 | } | ||
277 | }); | ||
278 | } | ||
279 | |||
280 | // Emitted when the window is closed. | ||
281 | mainWindow.on('close', e => { | ||
282 | debug('Window: close window'); | ||
283 | // Dereference the window object, usually you would store windows | ||
284 | // in an array if your app supports multi windows, this is the time | ||
285 | // when you should delete the corresponding element. | ||
286 | if ( | ||
287 | !willQuitApp && | ||
288 | retrieveSettingValue( | ||
289 | 'runInBackground', | ||
290 | DEFAULT_APP_SETTINGS.runInBackground, | ||
291 | ) | ||
292 | ) { | ||
293 | e.preventDefault(); | ||
294 | if (isWindows) { | ||
295 | debug('Window: minimize'); | ||
296 | mainWindow?.minimize(); | ||
297 | |||
298 | if ( | ||
299 | retrieveSettingValue( | ||
300 | 'closeToSystemTray', | ||
301 | DEFAULT_APP_SETTINGS.closeToSystemTray, | ||
302 | ) | ||
303 | ) { | ||
304 | debug('Skip taskbar: true'); | ||
305 | mainWindow?.setSkipTaskbar(true); | ||
306 | } | ||
307 | } else if (isMac && mainWindow?.isFullScreen()) { | ||
308 | debug('Window: leaveFullScreen and hide'); | ||
309 | mainWindow.once('show', () => mainWindow?.setFullScreen(true)); | ||
310 | mainWindow.once('leave-full-screen', () => mainWindow?.hide()); | ||
311 | mainWindow.setFullScreen(false); | ||
312 | } else { | ||
313 | debug('Window: hide'); | ||
314 | mainWindow?.hide(); | ||
315 | } | ||
316 | } else { | ||
317 | dbus.stop(); | ||
318 | app.quit(); | ||
319 | } | ||
320 | }); | ||
321 | |||
322 | // For Windows we need to store a flag to properly restore the window | ||
323 | // if the window was maximized before minimizing it so system tray | ||
324 | mainWindow.on('minimize', () => { | ||
325 | // @ts-expect-error Property 'wasMaximized' does not exist on type 'App'. | ||
326 | app.wasMaximized = app.isMaximized; | ||
327 | |||
328 | if ( | ||
329 | retrieveSettingValue( | ||
330 | 'minimizeToSystemTray', | ||
331 | DEFAULT_APP_SETTINGS.minimizeToSystemTray, | ||
332 | ) | ||
333 | ) { | ||
334 | debug('Skip taskbar: true'); | ||
335 | mainWindow?.setSkipTaskbar(true); | ||
336 | trayIcon.show(); | ||
337 | } | ||
338 | }); | ||
339 | |||
340 | mainWindow.on('maximize', () => { | ||
341 | debug('Window: maximize'); | ||
342 | // @ts-expect-error Property 'isMaximized' does not exist on type 'App'. | ||
343 | app.isMaximized = true; | ||
344 | }); | ||
345 | |||
346 | mainWindow.on('unmaximize', () => { | ||
347 | debug('Window: unmaximize'); | ||
348 | // @ts-expect-error Property 'isMaximized' does not exist on type 'App'. | ||
349 | app.isMaximized = false; | ||
350 | }); | ||
351 | |||
352 | mainWindow.on('restore', () => { | ||
353 | debug('Window: restore'); | ||
354 | mainWindow?.setSkipTaskbar(false); | ||
355 | |||
356 | // @ts-expect-error Property 'wasMaximized' does not exist on type 'App'. | ||
357 | if (app.wasMaximized) { | ||
358 | debug('Window: was maximized before, maximize window'); | ||
359 | mainWindow?.maximize(); | ||
360 | } | ||
361 | |||
362 | if ( | ||
363 | !retrieveSettingValue( | ||
364 | 'enableSystemTray', | ||
365 | DEFAULT_APP_SETTINGS.enableSystemTray, | ||
366 | ) | ||
367 | ) { | ||
368 | debug('Tray: hiding tray icon'); | ||
369 | trayIcon.hide(); | ||
370 | } | ||
371 | }); | ||
372 | |||
373 | if (isMac) { | ||
374 | // eslint-disable-next-line global-require | ||
375 | const { askFormacOSPermissions } = require('./electron/macOSPermissions'); | ||
376 | setTimeout(() => askFormacOSPermissions(mainWindow), ms('30s')); | ||
377 | } | ||
378 | |||
379 | mainWindow.on('show', () => { | ||
380 | debug('Skip taskbar: true'); | ||
381 | mainWindow?.setSkipTaskbar(false); | ||
382 | }); | ||
383 | |||
384 | // @ts-expect-error Property 'isMaximized' does not exist on type 'App'. | ||
385 | app.isMaximized = mainWindow.isMaximized(); | ||
386 | |||
387 | mainWindow.webContents.on('new-window', (e, url) => { | ||
388 | e.preventDefault(); | ||
389 | openExternalUrl(url); | ||
390 | }); | ||
391 | |||
392 | if ( | ||
393 | retrieveSettingValue('startMinimized', DEFAULT_APP_SETTINGS.startMinimized) | ||
394 | ) { | ||
395 | mainWindow.hide(); | ||
396 | } else { | ||
397 | mainWindow.show(); | ||
398 | } | ||
399 | |||
400 | app.whenReady().then(() => { | ||
401 | if ( | ||
402 | retrieveSettingValue( | ||
403 | 'enableGlobalHideShortcut', | ||
404 | DEFAULT_APP_SETTINGS.enableGlobalHideShortcut, | ||
405 | ) | ||
406 | ) { | ||
407 | // Toggle the window on 'Alt+X' | ||
408 | globalShortcut.register(`${altKey()}+X`, () => { | ||
409 | trayIcon.trayMenuTemplate[0].click(); | ||
410 | }); | ||
411 | } | ||
412 | }); | ||
413 | }; | ||
414 | |||
415 | // Allow passing command line parameters/switches to electron | ||
416 | // https://electronjs.org/docs/api/chrome-command-line-switches | ||
417 | // used for Kerberos support | ||
418 | // Usage e.g. MACOS | ||
419 | // $ Ferdi.app/Contents/MacOS/Ferdi --auth-server-whitelist *.mydomain.com --auth-negotiate-delegate-whitelist *.mydomain.com | ||
420 | const argv = require('minimist')(process.argv.slice(1)); | ||
421 | |||
422 | if (argv['auth-server-whitelist']) { | ||
423 | app.commandLine.appendSwitch( | ||
424 | 'auth-server-whitelist', | ||
425 | argv['auth-server-whitelist'], | ||
426 | ); | ||
427 | } | ||
428 | if (argv['auth-negotiate-delegate-whitelist']) { | ||
429 | app.commandLine.appendSwitch( | ||
430 | 'auth-negotiate-delegate-whitelist', | ||
431 | argv['auth-negotiate-delegate-whitelist'], | ||
432 | ); | ||
433 | } | ||
434 | |||
435 | // Disable Chromium's poor MPRIS implementation | ||
436 | // and apply workaround for https://github.com/electron/electron/pull/26432 | ||
437 | app.commandLine.appendSwitch( | ||
438 | 'disable-features', | ||
439 | 'HardwareMediaKeyHandling,MediaSessionService,CrossOriginOpenerPolicy', | ||
440 | ); | ||
441 | |||
442 | // This method will be called when Electron has finished | ||
443 | // initialization and is ready to create browser windows. | ||
444 | // Some APIs can only be used after this event occurs. | ||
445 | app.on('ready', () => { | ||
446 | // force app to live in /Applications | ||
447 | enforceMacOSAppLocation(); | ||
448 | |||
449 | // Register App URL | ||
450 | const protocolClient = isDevMode ? 'ferdi-dev' : 'ferdi'; | ||
451 | if (!app.isDefaultProtocolClient(protocolClient)) { | ||
452 | app.setAsDefaultProtocolClient(protocolClient); | ||
453 | } | ||
454 | |||
455 | if (isWindows) { | ||
456 | const extraArgs = isDevMode ? `${__dirname} ` : ''; | ||
457 | const iconPath = asarPath( | ||
458 | join( | ||
459 | isDevMode ? `${__dirname}../src/` : __dirname, | ||
460 | 'assets/images/taskbar/win32/display.ico', | ||
461 | ), | ||
462 | ); | ||
463 | app.setUserTasks([ | ||
464 | { | ||
465 | program: process.execPath, | ||
466 | arguments: `${extraArgs}--reset-window`, | ||
467 | iconPath, | ||
468 | iconIndex: 0, | ||
469 | title: 'Move Ferdi to Current Display', | ||
470 | description: 'Restore the position and size of Ferdi', | ||
471 | }, | ||
472 | { | ||
473 | program: process.execPath, | ||
474 | arguments: `${extraArgs}--quit`, | ||
475 | iconPath, | ||
476 | iconIndex: 0, | ||
477 | title: 'Quit Ferdi', | ||
478 | description: '', | ||
479 | }, | ||
480 | ]); | ||
481 | } | ||
482 | |||
483 | // eslint-disable-next-line global-require | ||
484 | require('electron-react-titlebar/main').initialize(); | ||
485 | |||
486 | createWindow(); | ||
487 | }); | ||
488 | |||
489 | // This is the worst possible implementation as the webview.webContents based callback doesn't work 🖕 | ||
490 | // TODO: rewrite to handle multiple login calls | ||
491 | const noop = () => null; | ||
492 | let authCallback = noop; | ||
493 | |||
494 | app.on('login', (event, _webContents, _request, authInfo, callback) => { | ||
495 | // @ts-expect-error Type '(username?: string | undefined, password?: string | undefined) => void' is not assignable to type '() => null'. | ||
496 | authCallback = callback; | ||
497 | debug('browser login event', authInfo); | ||
498 | event.preventDefault(); | ||
499 | |||
500 | if (!authInfo.isProxy && authInfo.scheme === 'basic') { | ||
501 | debug('basic auth handler', authInfo); | ||
502 | basicAuthHandler(mainWindow, authInfo); | ||
503 | } | ||
504 | }); | ||
505 | |||
506 | // TODO: evaluate if we need to store the authCallback for every service | ||
507 | ipcMain.on('feature-basic-auth-credentials', (_e, { user, password }) => { | ||
508 | debug('Received basic auth credentials', user, '********'); | ||
509 | |||
510 | // @ts-expect-error Expected 0 arguments, but got 2. | ||
511 | authCallback(user, password); | ||
512 | authCallback = noop; | ||
513 | }); | ||
514 | |||
515 | ipcMain.on('open-browser-window', (_e, { url, serviceId }) => { | ||
516 | const serviceSession = session.fromPartition(`persist:service-${serviceId}`); | ||
517 | const child = new BrowserWindow({ | ||
518 | parent: mainWindow, | ||
519 | webPreferences: { | ||
520 | session: serviceSession, | ||
521 | // TODO: Aren't these needed here? | ||
522 | // contextIsolation: false, | ||
523 | // enableRemoteModule: true, | ||
524 | }, | ||
525 | }); | ||
526 | child.show(); | ||
527 | child.loadURL(url); | ||
528 | debug('Received open-browser-window', url); | ||
529 | }); | ||
530 | |||
531 | ipcMain.on( | ||
532 | 'modifyRequestHeaders', | ||
533 | (_e, { modifiedRequestHeaders, serviceId }) => { | ||
534 | debug( | ||
535 | `Received modifyRequestHeaders ${modifiedRequestHeaders} for serviceId ${serviceId}`, | ||
536 | ); | ||
537 | for (const headerFilterSet of modifiedRequestHeaders) { | ||
538 | const { headers, requestFilters } = headerFilterSet; | ||
539 | session | ||
540 | .fromPartition(`persist:service-${serviceId}`) | ||
541 | .webRequest.onBeforeSendHeaders(requestFilters, (details, callback) => { | ||
542 | for (const key in headers) { | ||
543 | if (Object.prototype.hasOwnProperty.call(headers, key)) { | ||
544 | const value = headers[key]; | ||
545 | details.requestHeaders[key] = value; | ||
546 | } | ||
547 | } | ||
548 | callback({ requestHeaders: details.requestHeaders }); | ||
549 | }); | ||
550 | } | ||
551 | }, | ||
552 | ); | ||
553 | |||
554 | ipcMain.on('knownCertificateHosts', (_e, { knownHosts, serviceId }) => { | ||
555 | debug( | ||
556 | `Received knownCertificateHosts ${knownHosts} for serviceId ${serviceId}`, | ||
557 | ); | ||
558 | session | ||
559 | .fromPartition(`persist:service-${serviceId}`) | ||
560 | .setCertificateVerifyProc((request, callback) => { | ||
561 | // To know more about these callbacks: https://www.electronjs.org/docs/api/session#sessetcertificateverifyprocproc | ||
562 | const { hostname } = request; | ||
563 | if ( | ||
564 | knownHosts.find((item: string | string[]) => item.includes(hostname)) | ||
565 | .length > 0 | ||
566 | ) { | ||
567 | callback(0); | ||
568 | } else { | ||
569 | callback(-2); | ||
570 | } | ||
571 | }); | ||
572 | }); | ||
573 | |||
574 | ipcMain.on('feature-basic-auth-cancel', () => { | ||
575 | debug('Cancel basic auth'); | ||
576 | |||
577 | // @ts-expect-error Expected 0 arguments, but got 2. | ||
578 | authCallback(null); | ||
579 | authCallback = noop; | ||
580 | }); | ||
581 | |||
582 | // Handle synchronous messages from service webviews. | ||
583 | |||
584 | ipcMain.on('find-in-page', (e, text, options) => { | ||
585 | const { sender: webContents } = e; | ||
586 | if (webContents !== mainWindow?.webContents && typeof text === 'string') { | ||
587 | const sanitizedOptions = {}; | ||
588 | for (const option of ['forward', 'findNext', 'matchCase']) { | ||
589 | if (option in options) { | ||
590 | sanitizedOptions[option] = !!options[option]; | ||
591 | } | ||
592 | } | ||
593 | const requestId = webContents.findInPage(text, sanitizedOptions); | ||
594 | debug('Find in page', text, options, requestId); | ||
595 | e.returnValue = requestId; | ||
596 | } else { | ||
597 | e.returnValue = null; | ||
598 | } | ||
599 | }); | ||
600 | |||
601 | ipcMain.on('stop-find-in-page', (e, action) => { | ||
602 | const { sender: webContents } = e; | ||
603 | if (webContents !== mainWindow?.webContents) { | ||
604 | const validActions = [ | ||
605 | 'clearSelection', | ||
606 | 'keepSelection', | ||
607 | 'activateSelection', | ||
608 | ]; | ||
609 | if (validActions.includes(action)) { | ||
610 | webContents.stopFindInPage(action); | ||
611 | } | ||
612 | } | ||
613 | e.returnValue = null; | ||
614 | }); | ||
615 | |||
616 | ipcMain.on('set-spellchecker-locales', (_e, { locale, serviceId }) => { | ||
617 | if (serviceId === undefined) { | ||
618 | return; | ||
619 | } | ||
620 | |||
621 | const serviceSession = session.fromPartition(`persist:service-${serviceId}`); | ||
622 | const [defaultLocale] = serviceSession.getSpellCheckerLanguages(); | ||
623 | debug(`Spellchecker default locale is: ${defaultLocale}`); | ||
624 | |||
625 | const locales = [locale, defaultLocale, DEFAULT_APP_SETTINGS.fallbackLocale]; | ||
626 | debug(`Setting spellchecker locales to: ${locales}`); | ||
627 | serviceSession.setSpellCheckerLanguages(locales); | ||
628 | }); | ||
629 | |||
630 | // Quit when all windows are closed. | ||
631 | app.on('window-all-closed', () => { | ||
632 | // On OS X it is common for applications and their menu bar | ||
633 | // to stay active until the user quits explicitly with Cmd + Q | ||
634 | if ( | ||
635 | retrieveSettingValue( | ||
636 | 'runInBackground', | ||
637 | DEFAULT_APP_SETTINGS.runInBackground, | ||
638 | ) | ||
639 | ) { | ||
640 | debug('Window: all windows closed, quit app'); | ||
641 | app.quit(); | ||
642 | } else { | ||
643 | debug("Window: don't quit app"); | ||
644 | } | ||
645 | }); | ||
646 | |||
647 | app.on('before-quit', event => { | ||
648 | const yesButtonIndex = 0; | ||
649 | let selection = yesButtonIndex; | ||
650 | if ( | ||
651 | retrieveSettingValue('confirmOnQuit', DEFAULT_APP_SETTINGS.confirmOnQuit) | ||
652 | ) { | ||
653 | selection = dialog.showMessageBoxSync(mainWindow!, { | ||
654 | type: 'question', | ||
655 | message: 'Quit', | ||
656 | detail: 'Do you really want to quit Ferdi?', | ||
657 | buttons: ['Yes', 'No'], | ||
658 | }); | ||
659 | } | ||
660 | if (selection === yesButtonIndex) { | ||
661 | willQuitApp = true; | ||
662 | } else { | ||
663 | event.preventDefault(); | ||
664 | } | ||
665 | }); | ||
666 | |||
667 | app.on('activate', () => { | ||
668 | // On OS X it's common to re-create a window in the app when the | ||
669 | // dock icon is clicked and there are no other windows open. | ||
670 | if (mainWindow === null) { | ||
671 | createWindow(); | ||
672 | } else { | ||
673 | mainWindow?.show(); | ||
674 | } | ||
675 | }); | ||
676 | |||
677 | app.on('web-contents-created', (_createdEvent, contents) => { | ||
678 | contents.on('new-window', (event, _url, _frameNme, disposition) => { | ||
679 | if (disposition === 'foreground-tab') event.preventDefault(); | ||
680 | }); | ||
681 | }); | ||
682 | |||
683 | app.on('will-finish-launching', () => { | ||
684 | // Protocol handler for macOS | ||
685 | app.on('open-url', (event, url) => { | ||
686 | event.preventDefault(); | ||
687 | |||
688 | onDidLoad((window: BrowserWindow) => { | ||
689 | debug('open-url event', url); | ||
690 | handleDeepLink(window, url); | ||
691 | }); | ||
692 | }); | ||
693 | }); | ||