RenaiApp/src/main/modules/app-window/app-window.ts

152 lines
4.1 KiB
TypeScript

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<void> {
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', this.onWillNavigate);
this._window.webContents.on('new-window', this.onNewWindow);
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 {}
protected getInnerHtml(selector: string): Promise<string> {
return new Promise<string>((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<number | undefined> {
return new Promise<number | undefined>((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<void>;
}