Split electron-main into smaller chunks (#377)

* Split electron-main into smaller chunks

* Affix @types/node version and upgrade electron-store

* Iterate PR

* tidy up

* Actually run the split out code
This commit is contained in:
Michael Telatynski
2022-07-01 20:17:40 +01:00
committed by GitHub
parent b7a0402de5
commit 389f6f4334
10 changed files with 794 additions and 691 deletions

View File

@@ -21,90 +21,42 @@ limitations under the License.
import "./squirrelhooks";
import {
app,
ipcMain,
powerSaveBlocker,
BrowserWindow,
Menu,
autoUpdater,
protocol,
dialog,
desktopCapturer,
} from "electron";
import AutoLaunch from "auto-launch";
import path from "path";
import windowStateKeeper from 'electron-window-state';
import Store from 'electron-store';
import fs, { promises as afs } from "fs";
import crypto from "crypto";
import { URL } from "url";
import minimist from "minimist";
import type * as Keytar from "keytar"; // Hak dependency type
import type {
Seshat as SeshatType,
SeshatRecovery as SeshatRecoveryType,
ReindexError as ReindexErrorType,
} from "matrix-seshat"; // Hak dependency type
import "./ipc";
import "./keytar";
import "./seshat";
import "./settings";
import * as tray from "./tray";
import { buildMenuTemplate } from './vectormenu';
import webContentsHandler from './webcontents-handler';
import * as updater from './updater';
import { getProfileFromDeeplink, protocolInit, recordSSOSession } from './protocol';
import { getProfileFromDeeplink, protocolInit } from './protocol';
import { _t, AppLocalization } from './language-helper';
import Input = Electron.Input;
import IpcMainEvent = Electron.IpcMainEvent;
const argv = minimist(process.argv, {
alias: { help: "h" },
});
let keytar: typeof Keytar;
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
keytar = require('keytar');
} catch (e) {
if (e.code === "MODULE_NOT_FOUND") {
console.log("Keytar isn't installed; secure key storage is disabled.");
} else {
console.warn("Keytar unexpected error:", e);
}
}
let seshatSupported = false;
let Seshat: typeof SeshatType;
let SeshatRecovery: typeof SeshatRecoveryType;
let ReindexError: typeof ReindexErrorType;
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const seshatModule = require('matrix-seshat');
Seshat = seshatModule.Seshat;
SeshatRecovery = seshatModule.SeshatRecovery;
ReindexError = seshatModule.ReindexError;
seshatSupported = true;
} catch (e) {
if (e.code === "MODULE_NOT_FOUND") {
console.log("Seshat isn't installed, event indexing is disabled.");
} else {
console.warn("Seshat unexpected error:", e);
}
}
// Things we need throughout the file but need to be created
// async to are initialised in setupGlobals()
let asarPath: string;
let resPath: string;
let iconPath: string;
let vectorConfig: Record<string, any>;
let trayConfig: {
// eslint-disable-next-line camelcase
icon_path: string;
brand: string;
};
let launcher: AutoLaunch;
let appLocalization: AppLocalization;
if (argv["help"]) {
console.log("Options:");
console.log(" --profile-dir {path}: Path to where to store the profile.");
@@ -199,13 +151,13 @@ async function setupGlobals(): Promise<void> {
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
vectorConfig = require(asarPath + 'config.json');
global.vectorConfig = require(asarPath + 'config.json');
} catch (e) {
// it would be nice to check the error code here and bail if the config
// is unparsable, but we get MODULE_NOT_FOUND in the case of a missing
// file or invalid json, so node is just very unhelpful.
// Continue with the defaults (ie. an empty config)
vectorConfig = {};
global.vectorConfig = {};
}
try {
@@ -219,19 +171,19 @@ async function setupGlobals(): Promise<void> {
const homeserverProps = ['default_is_url', 'default_hs_url', 'default_server_name', 'default_server_config'];
if (Object.keys(localConfig).find(k => homeserverProps.includes(k))) {
// Rip out all the homeserver options from the vector config
vectorConfig = Object.keys(vectorConfig)
global.vectorConfig = Object.keys(global.vectorConfig)
.filter(k => !homeserverProps.includes(k))
.reduce((obj, key) => {obj[key] = vectorConfig[key]; return obj;}, {});
.reduce((obj, key) => {obj[key] = global.vectorConfig[key]; return obj;}, {});
}
vectorConfig = Object.assign(vectorConfig, localConfig);
global.vectorConfig = Object.assign(global.vectorConfig, localConfig);
} catch (e) {
if (e instanceof SyntaxError) {
dialog.showMessageBox({
type: "error",
title: `Your ${vectorConfig.brand || 'Element'} is misconfigured`,
message: `Your custom ${vectorConfig.brand || 'Element'} configuration contains invalid JSON. ` +
`Please correct the problem and reopen ${vectorConfig.brand || 'Element'}.`,
title: `Your ${global.vectorConfig.brand || 'Element'} is misconfigured`,
message: `Your custom ${global.vectorConfig.brand || 'Element'} configuration contains invalid JSON. ` +
`Please correct the problem and reopen ${global.vectorConfig.brand || 'Element'}.`,
detail: e.message || "",
});
}
@@ -243,14 +195,14 @@ async function setupGlobals(): Promise<void> {
// It's important to call `path.join` so we don't end up with the packaged asar in the final path.
const iconFile = `element.${process.platform === 'win32' ? 'ico' : 'png'}`;
iconPath = path.join(resPath, "img", iconFile);
trayConfig = {
global.trayConfig = {
icon_path: iconPath,
brand: vectorConfig.brand || 'Element',
brand: global.vectorConfig.brand || 'Element',
};
// launcher
launcher = new AutoLaunch({
name: vectorConfig.brand || 'Element',
global.launcher = new AutoLaunch({
name: global.vectorConfig.brand || 'Element',
isHidden: true,
mac: {
useLaunchAgent: true,
@@ -261,7 +213,7 @@ async function setupGlobals(): Promise<void> {
async function moveAutoLauncher(): Promise<void> {
// Look for an auto-launcher under 'Riot' and if we find one, port it's
// enabled/disabled-ness over to the new 'Element' launcher
if (!vectorConfig.brand || vectorConfig.brand === 'Element') {
if (!global.vectorConfig.brand || global.vectorConfig.brand === 'Element') {
const oldLauncher = new AutoLaunch({
name: 'Riot',
isHidden: true,
@@ -272,24 +224,13 @@ async function moveAutoLauncher(): Promise<void> {
const wasEnabled = await oldLauncher.isEnabled();
if (wasEnabled) {
await oldLauncher.disable();
await launcher.enable();
await global.launcher.enable();
}
}
}
const eventStorePath = path.join(app.getPath('userData'), 'EventStore');
const store = new Store<{
warnBeforeExit?: boolean;
minimizeToTray?: boolean;
spellCheckerEnabled?: boolean;
autoHideMenuBar?: boolean;
locale?: string | string[];
disableHardwareAcceleration?: boolean;
}>({ name: "electron-config" });
global.store = new Store({ name: "electron-config" });
let eventIndex: SeshatType = null;
let mainWindow: BrowserWindow = null;
global.appQuitting = false;
const exitShortcuts: Array<(input: Input, platform: string) => boolean> = [
@@ -299,12 +240,12 @@ const exitShortcuts: Array<(input: Input, platform: string) => boolean> = [
];
const warnBeforeExit = (event: Event, input: Input): void => {
const shouldWarnBeforeExit = store.get('warnBeforeExit', true);
const shouldWarnBeforeExit = global.store.get('warnBeforeExit', true);
const exitShortcutPressed =
input.type === 'keyDown' && exitShortcuts.some(shortcutFn => shortcutFn(input, process.platform));
if (shouldWarnBeforeExit && exitShortcutPressed) {
const shouldCancelCloseRequest = dialog.showMessageBoxSync(mainWindow, {
const shouldCancelCloseRequest = dialog.showMessageBoxSync(global.mainWindow, {
type: "question",
buttons: [_t("Cancel"), _t("Close Element")],
message: _t("Are you sure you want to quit?"),
@@ -318,25 +259,6 @@ const warnBeforeExit = (event: Event, input: Input): void => {
}
};
const deleteContents = async (p: string): Promise<void> => {
for (const entry of await afs.readdir(p)) {
const curPath = path.join(p, entry);
await afs.unlink(curPath);
}
};
async function randomArray(size: number): Promise<string> {
return new Promise((resolve, reject) => {
crypto.randomBytes(size, (err, buf) => {
if (err) {
reject(err);
} else {
resolve(buf.toString("base64").replace(/=+$/g, ''));
}
});
});
}
// handle uncaught errors otherwise it displays
// stack traces in popup dialogs, which is terrible (which
// it will do any time the auto update poke fails, and there's
@@ -347,510 +269,6 @@ process.on('uncaughtException', function(error: Error): void {
console.log('Unhandled exception', error);
});
let focusHandlerAttached = false;
ipcMain.on('setBadgeCount', function(_ev: IpcMainEvent, count: number): void {
if (process.platform !== 'win32') {
// only set badgeCount on Mac/Linux, the docs say that only those platforms support it but turns out Electron
// has some Windows support too, and in some Windows environments this leads to two badges rendering atop
// each other. See https://github.com/vector-im/element-web/issues/16942
app.badgeCount = count;
}
if (count === 0 && mainWindow) {
mainWindow.flashFrame(false);
}
});
ipcMain.on('loudNotification', function(): void {
if (process.platform === 'win32' && mainWindow && !mainWindow.isFocused() && !focusHandlerAttached) {
mainWindow.flashFrame(true);
mainWindow.once('focus', () => {
mainWindow.flashFrame(false);
focusHandlerAttached = false;
});
focusHandlerAttached = true;
}
});
let powerSaveBlockerId: number = null;
ipcMain.on('app_onAction', function(_ev: IpcMainEvent, payload) {
switch (payload.action) {
case 'call_state':
if (powerSaveBlockerId !== null && powerSaveBlocker.isStarted(powerSaveBlockerId)) {
if (payload.state === 'ended') {
powerSaveBlocker.stop(powerSaveBlockerId);
powerSaveBlockerId = null;
}
} else {
if (powerSaveBlockerId === null && payload.state === 'connected') {
powerSaveBlockerId = powerSaveBlocker.start('prevent-display-sleep');
}
}
break;
}
});
interface Setting {
read(): Promise<any>;
write(value: any): Promise<void>;
}
const settings: Record<string, Setting> = {
"Electron.autoLaunch": {
async read(): Promise<any> {
return launcher.isEnabled();
},
async write(value: any): Promise<void> {
if (value) {
return launcher.enable();
} else {
return launcher.disable();
}
},
},
"Electron.warnBeforeExit": {
async read(): Promise<any> {
return store.get("warnBeforeExit", true);
},
async write(value: any): Promise<void> {
store.set("warnBeforeExit", value);
},
},
"Electron.alwaysShowMenuBar": { // not supported on macOS
async read(): Promise<any> {
return !global.mainWindow.autoHideMenuBar;
},
async write(value: any): Promise<void> {
store.set('autoHideMenuBar', !value);
global.mainWindow.autoHideMenuBar = !value;
global.mainWindow.setMenuBarVisibility(value);
},
},
"Electron.showTrayIcon": { // not supported on macOS
async read(): Promise<any> {
return tray.hasTray();
},
async write(value: any): Promise<void> {
if (value) {
// Create trayIcon icon
tray.create(trayConfig);
} else {
tray.destroy();
}
store.set('minimizeToTray', value);
},
},
"Electron.enableHardwareAcceleration": {
async read(): Promise<any> {
return !store.get('disableHardwareAcceleration', false);
},
async write(value: any): Promise<void> {
store.set('disableHardwareAcceleration', !value);
},
},
};
ipcMain.on('ipcCall', async function(_ev: IpcMainEvent, payload) {
if (!mainWindow) return;
const args = payload.args || [];
let ret: any;
switch (payload.name) {
case 'getUpdateFeedUrl':
ret = autoUpdater.getFeedURL();
break;
case 'getSettingValue': {
const [settingName] = args;
const setting = settings[settingName];
ret = await setting.read();
break;
}
case 'setSettingValue': {
const [settingName, value] = args;
const setting = settings[settingName];
await setting.write(value);
break;
}
case 'setLanguage':
appLocalization.setAppLocale(args[0]);
break;
case 'getAppVersion':
ret = app.getVersion();
break;
case 'focusWindow':
if (mainWindow.isMinimized()) {
mainWindow.restore();
} else if (!mainWindow.isVisible()) {
mainWindow.show();
} else {
mainWindow.focus();
}
break;
case 'getConfig':
ret = vectorConfig;
break;
case 'navigateBack':
if (mainWindow.webContents.canGoBack()) {
mainWindow.webContents.goBack();
}
break;
case 'navigateForward':
if (mainWindow.webContents.canGoForward()) {
mainWindow.webContents.goForward();
}
break;
case 'setSpellCheckLanguages':
if (args[0] && args[0].length > 0) {
mainWindow.webContents.session.setSpellCheckerEnabled(true);
store.set("spellCheckerEnabled", true);
try {
mainWindow.webContents.session.setSpellCheckerLanguages(args[0]);
} catch (er) {
console.log("There were problems setting the spellcheck languages", er);
}
} else {
mainWindow.webContents.session.setSpellCheckerEnabled(false);
store.set("spellCheckerEnabled", false);
}
break;
case 'getSpellCheckLanguages':
if (store.get("spellCheckerEnabled", true)) {
ret = mainWindow.webContents.session.getSpellCheckerLanguages();
} else {
ret = [];
}
break;
case 'getAvailableSpellCheckLanguages':
ret = mainWindow.webContents.session.availableSpellCheckerLanguages;
break;
case 'startSSOFlow':
recordSSOSession(args[0]);
break;
case 'getPickleKey':
try {
ret = await keytar.getPassword("element.io", `${args[0]}|${args[1]}`);
// migrate from riot.im (remove once we think there will no longer be
// logins from the time of riot.im)
if (ret === null) {
ret = await keytar.getPassword("riot.im", `${args[0]}|${args[1]}`);
}
} catch (e) {
// if an error is thrown (e.g. keytar can't connect to the keychain),
// then return null, which means the default pickle key will be used
ret = null;
}
break;
case 'createPickleKey':
try {
const pickleKey = await randomArray(32);
await keytar.setPassword("element.io", `${args[0]}|${args[1]}`, pickleKey);
ret = pickleKey;
} catch (e) {
ret = null;
}
break;
case 'destroyPickleKey':
try {
await keytar.deletePassword("element.io", `${args[0]}|${args[1]}`);
// migrate from riot.im (remove once we think there will no longer be
// logins from the time of riot.im)
await keytar.deletePassword("riot.im", `${args[0]}|${args[1]}`);
} catch (e) {}
break;
case 'getDesktopCapturerSources':
ret = (await desktopCapturer.getSources(args[0])).map((source) => ({
id: source.id,
name: source.name,
thumbnailURL: source.thumbnail.toDataURL(),
}));
break;
default:
mainWindow.webContents.send('ipcReply', {
id: payload.id,
error: "Unknown IPC Call: " + payload.name,
});
return;
}
mainWindow.webContents.send('ipcReply', {
id: payload.id,
reply: ret,
});
});
const seshatDefaultPassphrase = "DEFAULT_PASSPHRASE";
async function getOrCreatePassphrase(key: string): Promise<string> {
if (keytar) {
try {
const storedPassphrase = await keytar.getPassword("element.io", key);
if (storedPassphrase !== null) {
return storedPassphrase;
} else {
const newPassphrase = await randomArray(32);
await keytar.setPassword("element.io", key, newPassphrase);
return newPassphrase;
}
} catch (e) {
console.log("Error getting the event index passphrase out of the secret store", e);
}
} else {
return seshatDefaultPassphrase;
}
}
ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> {
if (!mainWindow) return;
const sendError = (id, e) => {
const error = {
message: e.message,
};
mainWindow.webContents.send('seshatReply', {
id: id,
error: error,
});
};
const args = payload.args || [];
let ret: any;
switch (payload.name) {
case 'supportsEventIndexing':
ret = seshatSupported;
break;
case 'initEventIndex':
if (eventIndex === null) {
const userId = args[0];
const deviceId = args[1];
const passphraseKey = `seshat|${userId}|${deviceId}`;
const passphrase = await getOrCreatePassphrase(passphraseKey);
try {
await afs.mkdir(eventStorePath, { recursive: true });
eventIndex = new Seshat(eventStorePath, { passphrase });
} catch (e) {
if (e instanceof ReindexError) {
// If this is a reindex error, the index schema
// changed. Try to open the database in recovery mode,
// reindex the database and finally try to open the
// database again.
const recoveryIndex = new SeshatRecovery(eventStorePath, {
passphrase,
});
const userVersion = await recoveryIndex.getUserVersion();
// If our user version is 0 we'll delete the db
// anyways so reindexing it is a waste of time.
if (userVersion === 0) {
await recoveryIndex.shutdown();
try {
await deleteContents(eventStorePath);
} catch (e) {
}
} else {
await recoveryIndex.reindex();
}
eventIndex = new Seshat(eventStorePath, { passphrase });
} else {
sendError(payload.id, e);
return;
}
}
}
break;
case 'closeEventIndex':
if (eventIndex !== null) {
const index = eventIndex;
eventIndex = null;
try {
await index.shutdown();
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
case 'deleteEventIndex':
{
try {
await deleteContents(eventStorePath);
} catch (e) {
}
}
break;
case 'isEventIndexEmpty':
if (eventIndex === null) ret = true;
else ret = await eventIndex.isEmpty();
break;
case 'isRoomIndexed':
if (eventIndex === null) ret = false;
else ret = await eventIndex.isRoomIndexed(args[0]);
break;
case 'addEventToIndex':
try {
eventIndex.addEvent(args[0], args[1]);
} catch (e) {
sendError(payload.id, e);
return;
}
break;
case 'deleteEvent':
try {
ret = await eventIndex.deleteEvent(args[0]);
} catch (e) {
sendError(payload.id, e);
return;
}
break;
case 'commitLiveEvents':
try {
ret = await eventIndex.commit();
} catch (e) {
sendError(payload.id, e);
return;
}
break;
case 'searchEventIndex':
try {
ret = await eventIndex.search(args[0]);
} catch (e) {
sendError(payload.id, e);
return;
}
break;
case 'addHistoricEvents':
if (eventIndex === null) ret = false;
else {
try {
ret = await eventIndex.addHistoricEvents(
args[0], args[1], args[2]);
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
case 'getStats':
if (eventIndex === null) ret = 0;
else {
try {
ret = await eventIndex.getStats();
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
case 'removeCrawlerCheckpoint':
if (eventIndex === null) ret = false;
else {
try {
ret = await eventIndex.removeCrawlerCheckpoint(args[0]);
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
case 'addCrawlerCheckpoint':
if (eventIndex === null) ret = false;
else {
try {
ret = await eventIndex.addCrawlerCheckpoint(args[0]);
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
case 'loadFileEvents':
if (eventIndex === null) ret = [];
else {
try {
ret = await eventIndex.loadFileEvents(args[0]);
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
case 'loadCheckpoints':
if (eventIndex === null) ret = [];
else {
try {
ret = await eventIndex.loadCheckpoints();
} catch (e) {
ret = [];
}
}
break;
case 'setUserVersion':
if (eventIndex === null) break;
else {
try {
await eventIndex.setUserVersion(args[0]);
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
case 'getUserVersion':
if (eventIndex === null) ret = 0;
else {
try {
ret = await eventIndex.getUserVersion();
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
default:
mainWindow.webContents.send('seshatReply', {
id: payload.id,
error: "Unknown IPC Call: " + payload.name,
});
return;
}
mainWindow.webContents.send('seshatReply', {
id: payload.id,
reply: ret,
});
});
app.commandLine.appendSwitch('--enable-usermedia-screen-capturing');
if (!app.commandLine.hasSwitch('enable-features')) {
app.commandLine.appendSwitch('enable-features', 'WebRTCPipeWireCapturer');
@@ -894,7 +312,7 @@ app.enableSandbox();
app.commandLine.appendSwitch('disable-features', 'HardwareMediaKeyHandling,MediaSessionService');
// Disable hardware acceleration if the setting has been set.
if (store.get('disableHardwareAcceleration', false) === true) {
if (global.store.get('disableHardwareAcceleration', false) === true) {
console.log("Disabling hardware acceleration.");
app.disableHardwareAcceleration();
}
@@ -982,9 +400,9 @@ app.on('ready', async () => {
if (argv['no-update']) {
console.log('Auto update disabled via command line flag "--no-update"');
} else if (vectorConfig['update_base_url']) {
console.log(`Starting auto update with base URL: ${vectorConfig['update_base_url']}`);
updater.start(vectorConfig['update_base_url']);
} else if (global.vectorConfig['update_base_url']) {
console.log(`Starting auto update with base URL: ${global.vectorConfig['update_base_url']}`);
updater.start(global.vectorConfig['update_base_url']);
} else {
console.log('No update_base_url is defined: auto update is disabled');
}
@@ -996,13 +414,13 @@ app.on('ready', async () => {
});
const preloadScript = path.normalize(`${__dirname}/preload.js`);
mainWindow = global.mainWindow = new BrowserWindow({
global.mainWindow = new BrowserWindow({
// https://www.electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
backgroundColor: '#fff',
icon: iconPath,
show: false,
autoHideMenuBar: store.get('autoHideMenuBar', true),
autoHideMenuBar: global.store.get('autoHideMenuBar', true),
x: mainWindowState.x,
y: mainWindowState.y,
@@ -1016,32 +434,32 @@ app.on('ready', async () => {
webgl: true,
},
});
mainWindow.loadURL('vector://vector/webapp/');
global.mainWindow.loadURL('vector://vector/webapp/');
// Handle spellchecker
// For some reason spellCheckerEnabled isn't persisted so we have to use the store here
mainWindow.webContents.session.setSpellCheckerEnabled(store.get("spellCheckerEnabled", true));
// For some reason spellCheckerEnabled isn't persisted, so we have to use the store here
global.mainWindow.webContents.session.setSpellCheckerEnabled(global.store.get("spellCheckerEnabled", true));
// Create trayIcon icon
if (store.get('minimizeToTray', true)) tray.create(trayConfig);
if (global.store.get('minimizeToTray', true)) tray.create(global.trayConfig);
mainWindow.once('ready-to-show', () => {
mainWindowState.manage(mainWindow);
global.mainWindow.once('ready-to-show', () => {
mainWindowState.manage(global.mainWindow);
if (!argv['hidden']) {
mainWindow.show();
global.mainWindow.show();
} else {
// hide here explicitly because window manage above sometimes shows it
mainWindow.hide();
global.mainWindow.hide();
}
});
mainWindow.webContents.on('before-input-event', warnBeforeExit);
global.mainWindow.webContents.on('before-input-event', warnBeforeExit);
mainWindow.on('closed', () => {
mainWindow = global.mainWindow = null;
global.mainWindow.on('closed', () => {
global.mainWindow = null;
});
mainWindow.on('close', async (e) => {
global.mainWindow.on('close', async (e) => {
// If we are not quitting and have a tray icon then minimize to tray
if (!global.appQuitting && (tray.hasTray() || process.platform === 'darwin')) {
// On Mac, closing the window just hides it
@@ -1049,12 +467,12 @@ app.on('ready', async () => {
// behave, eg. Mail.app)
e.preventDefault();
if (mainWindow.isFullScreen()) {
mainWindow.once('leave-full-screen', () => mainWindow.hide());
if (global.mainWindow.isFullScreen()) {
global.mainWindow.once('leave-full-screen', () => global.mainWindow.hide());
mainWindow.setFullScreen(false);
global.mainWindow.setFullScreen(false);
} else {
mainWindow.hide();
global.mainWindow.hide();
}
return false;
@@ -1063,19 +481,19 @@ app.on('ready', async () => {
if (process.platform === 'win32') {
// Handle forward/backward mouse buttons in Windows
mainWindow.on('app-command', (e, cmd) => {
if (cmd === 'browser-backward' && mainWindow.webContents.canGoBack()) {
mainWindow.webContents.goBack();
} else if (cmd === 'browser-forward' && mainWindow.webContents.canGoForward()) {
mainWindow.webContents.goForward();
global.mainWindow.on('app-command', (e, cmd) => {
if (cmd === 'browser-backward' && global.mainWindow.webContents.canGoBack()) {
global.mainWindow.webContents.goBack();
} else if (cmd === 'browser-forward' && global.mainWindow.webContents.canGoForward()) {
global.mainWindow.webContents.goForward();
}
});
}
webContentsHandler(mainWindow.webContents);
webContentsHandler(global.mainWindow.webContents);
appLocalization = new AppLocalization({
store,
global.appLocalization = new AppLocalization({
store: global.store,
components: [
() => tray.initApplicationMenu(),
() => Menu.setApplicationMenu(buildMenuTemplate()),
@@ -1088,14 +506,12 @@ app.on('window-all-closed', () => {
});
app.on('activate', () => {
mainWindow.show();
global.mainWindow.show();
});
function beforeQuit(): void {
global.appQuitting = true;
if (mainWindow) {
mainWindow.webContents.send('before-quit');
}
global.mainWindow?.webContents.send('before-quit');
}
app.on('before-quit', beforeQuit);
@@ -1106,10 +522,10 @@ app.on('second-instance', (ev, commandLine, workingDirectory) => {
if (commandLine.includes('--hidden')) return;
// Someone tried to run a second instance, we should focus our window.
if (mainWindow) {
if (!mainWindow.isVisible()) mainWindow.show();
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
if (global.mainWindow) {
if (!global.mainWindow.isVisible()) global.mainWindow.show();
if (global.mainWindow.isMinimized()) global.mainWindow.restore();
global.mainWindow.focus();
}
});