/* * Copyright (C) 2022 Kristóf Marussy * * This file is part of Sophie. * * Sophie is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * * SPDX-License-Identifier: AGPL-3.0-only */ import { Action, MainToRendererIpcMessage, RendererToMainIpcMessage, } from '@sophie/shared'; import { BrowserWindow, ipcMain, IpcMainEvent } from 'electron'; import type { IJsonPatch } from 'mobx-state-tree'; import type MainStore from '../../../stores/MainStore'; import { getLogger } from '../../../utils/log'; import RendererBridge from '../RendererBridge'; import type { MainWindow, ServiceView } from '../types'; import ElectronServiceView from './ElectronServiceView'; import type ElectronViewFactory from './ElectronViewFactory'; import { openDevToolsWhenReady } from './devTools'; import lockWebContentsToFile from './lockWebContentsToFile'; const log = getLogger('ElectronMainWindow'); export default class ElectronMainWindow implements MainWindow { private readonly browserWindow: BrowserWindow; private readonly bridge: RendererBridge; private readonly dispatchActionHandler = ( event: IpcMainEvent, rawAction: unknown, ): void => { const { id } = event.sender; if (id !== this.browserWindow.webContents.id) { log.warn( 'Unexpected', RendererToMainIpcMessage.DispatchAction, 'from webContents', id, ); return; } try { const action = Action.parse(rawAction); this.store.dispatch(action); } catch (error) { log.error('Error while dispatching renderer action', rawAction, error); } }; constructor( private readonly store: MainStore, private readonly parent: ElectronViewFactory, ) { this.browserWindow = new BrowserWindow({ show: false, autoHideMenuBar: true, darkTheme: store.shared.shouldUseDarkColors, webPreferences: { sandbox: true, devTools: parent.devMode, preload: parent.resources.getPath('preload', 'index.cjs'), }, }); const { webContents } = this.browserWindow; ipcMain.handle(RendererToMainIpcMessage.GetSharedStoreSnapshot, (event) => { const { id } = event.sender; if (id !== webContents.id) { log.warn( 'Unexpected', RendererToMainIpcMessage.GetSharedStoreSnapshot, 'from webContents', id, ); throw new Error('Invalid IPC call'); } return this.bridge.snapshot; }); this.bridge = new RendererBridge(store, (patch) => { webContents.send(MainToRendererIpcMessage.SharedStorePatch, patch); }); ipcMain.on( RendererToMainIpcMessage.DispatchAction, this.dispatchActionHandler, ); webContents.userAgent = parent.userAgents.mainWindowUserAgent; this.browserWindow.on('ready-to-show', () => this.browserWindow.show()); this.browserWindow.on('close', () => this.dispose()); if (parent.devMode) { openDevToolsWhenReady(this.browserWindow); } } bringToForeground(): void { if (!this.browserWindow.isVisible()) { this.browserWindow.show(); } if (this.browserWindow.isMinimized()) { this.browserWindow.restore(); } this.browserWindow.focus(); } setServiceView(serviceView: ServiceView | undefined) { if (serviceView === undefined) { // eslint-disable-next-line unicorn/no-null -- Electron API requires passing `null`. this.browserWindow.setBrowserView(null); return; } if (serviceView instanceof ElectronServiceView) { this.browserWindow.setBrowserView(serviceView.browserView); serviceView.browserView.setBackgroundColor('#fff'); return; } throw new TypeError( 'Unexpected ServiceView with no underlying BrowserView', ); } dispose() { this.bridge.dispose(); this.browserWindow.destroy(); ipcMain.removeHandler(RendererToMainIpcMessage.GetSharedStoreSnapshot); ipcMain.removeListener( RendererToMainIpcMessage.DispatchAction, this.dispatchActionHandler, ); } loadInterface(): Promise { return lockWebContentsToFile( this.parent.resources, 'index.html', this.browserWindow.webContents, ); } sendSharedStorePatch(patch: IJsonPatch[]): void { this.browserWindow.webContents.send( MainToRendererIpcMessage.SharedStorePatch, patch, ); } }