import * as path from 'path'; import * as url from 'url'; import { app, BrowserWindow, screen, ipcMain, IpcMainEvent } from 'electron'; import windowStateKeeper from 'electron-window-state'; import { EventEmitter } from 'events'; import log from 'electron-log'; import { registerWindowStateChangedEvents } from '../lib/window-state'; import { ElectronAppChannel, ElectronBrowserWindowChannel, ElectronWebContentsChannel } from '@ucap-webmessenger/electron-core'; import { now } from '../util/now'; import { Storage } from '../lib/storage'; export class AppWindow { private window: BrowserWindow | null = null; private eventEmitter = new EventEmitter(); // tslint:disable-next-line: variable-name private _loadTime: number | null = null; // tslint:disable-next-line: variable-name private _rendererReadyTime: number | null = null; private minWidth = 700; private minHeight = 600; private defaultWidth = 1160; private defaultHeight = 800; public constructor(private appIconPath: string, appStorage: Storage) { const savedWindowState = windowStateKeeper({ defaultWidth: this.defaultWidth, defaultHeight: this.defaultHeight }); const windowOptions: Electron.BrowserWindowConstructorOptions = { x: savedWindowState.x, y: savedWindowState.y, width: savedWindowState.width, height: savedWindowState.height, minWidth: this.minWidth, minHeight: this.minHeight, center: true, // This fixes subpixel aliasing on Windows // See https://github.com/atom/atom/commit/683bef5b9d133cb194b476938c77cc07fd05b972 backgroundColor: '#fff', webPreferences: { // Disable auxclick event // See https://developers.google.com/web/updates/2016/10/auxclick disableBlinkFeatures: 'Auxclick', // Enable, among other things, the ResizeObserver experimentalFeatures: true, nodeIntegration: true }, acceptFirstMouse: true, icon: this.appIconPath, show: false }; if (__DARWIN__) { windowOptions.titleBarStyle = 'hidden'; } else if (__WIN32__) { windowOptions.frame = false; } else if (__LINUX__) { windowOptions.frame = false; } this.window = new BrowserWindow(windowOptions); savedWindowState.manage(this.window); let quitting = true; app.on(ElectronAppChannel.BeforeQuit, () => { quitting = true; }); ipcMain.on(ElectronAppChannel.WillQuit, (event: IpcMainEvent) => { quitting = true; event.returnValue = true; }); // on macOS, when the user closes the window we really just hide it. This // lets us activate quickly and keep all our interesting logic in the // renderer. if (__DARWIN__) { this.window.on(ElectronBrowserWindowChannel.Close, e => { if (!quitting) { e.preventDefault(); } }); } else if (__WIN32__) { this.window.on(ElectronBrowserWindowChannel.Minimize, e => { if (!quitting) { e.preventDefault(); this.window.minimize(); } }); this.window.on(ElectronBrowserWindowChannel.Close, e => { // if (!quitting) { e.preventDefault(); this.window.hide(); // } }); this.window.on(ElectronBrowserWindowChannel.Focus, e => { this.window.flashFrame(false); }); } if (__WIN32__) { // workaround for known issue with fullscreen-ing the app and restoring // is that some Chromium API reports the incorrect bounds, so that it // will leave a small space at the top of the screen on every other // maximize // // adapted from https://github.com/electron/electron/issues/12971#issuecomment-403956396 // // can be tidied up once https://github.com/electron/electron/issues/12971 // has been confirmed as resolved this.window.once(ElectronBrowserWindowChannel.ReadyToShow, () => { this.window.close(); setTimeout(() => { if (appStorage.startupHideWindow) { this.window.close(); } else { this.window.show(); } }, 500); this.window.on(ElectronBrowserWindowChannel.Unmaximize, () => { setTimeout(() => { const bounds = this.window.getBounds(); bounds.width += 1; this.window.setBounds(bounds); bounds.width -= 1; this.window.setBounds(bounds); }, 5); }); }); } } public load(hashUrl?: string): void { let startLoad = 0; this.window.webContents.once( ElectronWebContentsChannel.DidStartLoading, () => { this._rendererReadyTime = null; this._loadTime = null; startLoad = now(); } ); this.window.webContents.once( ElectronWebContentsChannel.DidFinishLoad, () => { this.window.webContents.setVisualZoomLevelLimits(1, 1); if (process.env.NODE_ENV === 'development') { this.window.webContents.openDevTools(); } this._loadTime = now() - startLoad; } ); this.window.webContents.on( ElectronWebContentsChannel.DidFailLoad, ( event: Event, errorCode: number, errorDescription: string, validatedURL: string, isMainFrame: boolean ) => { if ('ERR_FILE_NOT_FOUND' === errorDescription) { this.load(url.parse(validatedURL).hash); return; } log.error( ElectronWebContentsChannel.DidFailLoad, event, errorCode, errorDescription, validatedURL, isMainFrame ); this.window.webContents.openDevTools(); } ); registerWindowStateChangedEvents(this.window); if (__DEV__) { this.window.loadURL('http://localhost:4200'); } else { this.window.loadURL( url.format({ pathname: path.join( __dirname, '..', 'ucap-webmessenger-app/index.html' ), protocol: 'file:', slashes: true, hash: hashUrl }) ); } } /** Is the page loaded and has the renderer signalled it's ready? */ private get rendererLoaded(): boolean { return !!this.loadTime && !!this.rendererReadyTime; } public onClose(fn: () => void) { this.window.on(ElectronBrowserWindowChannel.Closed, fn); } /** * Register a function to call when the window is done loading. At that point * the page has loaded and the renderer has signalled that it is ready. */ public onDidLoad(fn: () => void): EventEmitter { return this.eventEmitter.on('did-load', fn); } public isMinimized() { return this.window.isMinimized(); } /** Is the window currently visible? */ public isVisible() { return this.window.isVisible(); } public restore() { this.window.restore(); } public focus() { this.window.focus(); } /** Show the window. */ public show() { this.window.show(); } public hide() { this.window.hide(); } /** * Get the time (in milliseconds) spent loading the page. * * This will be `null` until `onDidLoad` is called. */ public get loadTime(): number | null { return this._loadTime; } /** * Get the time (in milliseconds) elapsed from the renderer being loaded to it * signaling it was ready. * * This will be `null` until `onDidLoad` is called. */ public get rendererReadyTime(): number | null { return this._rendererReadyTime; } public destroy() { this.window.destroy(); } public get browserWindow(): BrowserWindow | null { return this.window; } }