import { app, BrowserWindow, Event, LoadFileOptions, LoadURLOptions, NewWindowWebContentsEvent } from 'electron'; import os from 'os'; import path from 'path'; import { isDev } from '../../core/env'; import { completeContentSecurityPolicy, setWindowCsp } from '../session/session-util'; import type { AppWindowInterface } from './app-window-interface'; import { WindowClosedError } from './window-closed-error'; import BrowserWindowConstructorOptions = Electron.BrowserWindowConstructorOptions; let defaultOptions: BrowserWindowConstructorOptions = { width: 1600, height: 900, webPreferences: { enableRemoteModule: false, nodeIntegration: false, contextIsolation: true, devTools: isDev(), }, }; switch (os.platform()) { case 'win32': defaultOptions = { ...defaultOptions, ...{ icon: path.resolve(app.getAppPath(), 'resources', 'icon.ico'), }, }; break; default: break; } export abstract class AppWindow implements AppWindowInterface { protected static default = {}; protected _window: BrowserWindow | null = null; protected readonly logger: LoggerInterface; protected options: BrowserWindowConstructorOptions; protected uri: string; protected abstract loadOptions: LoadFileOptions | LoadURLOptions; protected constructor(logger: LoggerInterface, uri: string, options: BrowserWindowConstructorOptions = {}) { this.logger = logger; this.options = { ...defaultOptions, ...options }; this.uri = uri; } public get window(): BrowserWindow | null { return this._window; } public getWindow(): BrowserWindow { if (!this._window) { throw new WindowClosedError(); } return this._window; } public open(): Promise { this._window = new BrowserWindow(this.options); setWindowCsp(this._window, completeContentSecurityPolicy(this.getCsp())); this._window.on('closed', () => { this.onClosed(); this._window = null; }); if (isDev()) { this._window.webContents.openDevTools(); } this._window.webContents.on('will-navigate', (...args) => this.onWillNavigate(...args)); this._window.webContents.on('new-window', (...args) => this.onNewWindow(args[0])); return this.load(this._window); } public close(force: boolean = false): void { if (force) { this._window?.destroy(); } else { this._window?.close(); } } public isClosed(): boolean { return !this._window; } public askForUserInteraction(): void { if (!this.getWindow().isFocused()) { this.getWindow().on('focus', () => { this.getWindow().flashFrame(false); }); this.getWindow().flashFrame(true); } } protected getCsp(): Session.ContentSecurityPolicy { return {}; } // eslint-disable-next-line @typescript-eslint/no-unused-vars -- it is used in child classes protected onWillNavigate(event: Event, navigationUrl: string): void { event.preventDefault(); } protected onNewWindow(event: NewWindowWebContentsEvent): void { event.preventDefault(); } protected onClosed(): void { // do nothing by default } protected getInnerHtml(selector: string): Promise { return new Promise((resolve) => { this.getWindow() .webContents.executeJavaScript(`document.querySelector('${selector}').innerHTML`) .then((innerHtml) => { resolve(innerHtml); }) .catch(() => { void this.logger.warning(`Could not get the inner HTML of an element with the selector '${selector}'.`); resolve(''); }); }); } protected getTime(selector: string): Promise { return new Promise((resolve) => { this.getWindow() .webContents.executeJavaScript(`Date(document.querySelector('${selector}').dateTime).getTime()`) .then((time) => { resolve(time); }) .catch(() => { void this.logger.warning( `Could not get the of the presumed HTMLTimeElement with the selector '${selector}'.`, ); resolve(undefined); }); }); } protected abstract load(window: BrowserWindow): Promise; }