-
Matiss Janis Aboltins authoredMatiss Janis Aboltins authored
index.js 9.34 KiB
/* eslint-disable import/order */
// (I have no idea why the imports are like this. Not touching them.)
const isDev = require('electron-is-dev');
const fs = require('fs');
require('module').globalPaths.push(__dirname + '/..');
// Allow unsecure in dev
if (isDev) {
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';
}
const {
app,
ipcMain,
BrowserWindow,
Menu,
dialog,
shell,
protocol,
} = require('electron');
const promiseRetry = require('promise-retry');
// This allows relative URLs to be resolved to app:// which makes
// local assets load correctly
protocol.registerSchemesAsPrivileged([
{ scheme: 'app', privileges: { standard: true } },
]);
global.fetch = require('node-fetch');
const about = require('./about');
const { getRandomPort } = require('get-port-please');
const getMenu = require('./menu');
const updater = require('./updater');
require('./security');
const { fork } = require('child_process');
const path = require('path');
require('./setRequireHook');
if (!isDev || !process.env.ACTUAL_DOCUMENT_DIR) {
process.env.ACTUAL_DOCUMENT_DIR = app.getPath('documents');
}
if (!isDev || !process.env.ACTUAL_DATA_DIR) {
process.env.ACTUAL_DATA_DIR = app.getPath('userData');
}
// eslint-disable-next-line import/extensions
const WindowState = require('./window-state.js');
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let clientWin;
let serverWin; // eslint-disable-line @typescript-eslint/no-unused-vars
let serverProcess;
let serverSocket;
let IS_QUITTING = false;
updater.onEvent((type, data) => {
// Notify both the app and the about window
if (clientWin) {
clientWin.webContents.send(type, data);
}
if (about.getWindow()) {
about.getWindow().webContents.send(type, data);
}
});
if (isDev) {
process.traceProcessWarnings = true;
}
function createBackgroundProcess(socketName) {
serverProcess = fork(
__dirname + '/server.js',
['--subprocess', app.getVersion(), socketName],
isDev ? { execArgv: ['--inspect'] } : undefined,
);
serverProcess.on('message', msg => {
switch (msg.type) {
case 'captureEvent':
case 'captureBreadcrumb':
break;
case 'shouldAutoUpdate':
if (msg.flag) {
updater.start();
} else {
updater.stop();
}
break;
default:
console.log('Unknown server message: ' + msg.type);
}
});
}
async function createWindow() {
const windowState = await WindowState.get();
// Create the browser window.
const win = new BrowserWindow({
x: windowState.x,
y: windowState.y,
width: windowState.width,
height: windowState.height,
title: 'Actual',
webPreferences: {
nodeIntegration: false,
nodeIntegrationInWorker: false,
nodeIntegrationInSubFrames: false,
contextIsolation: true,
enableRemoteModule: false,
preload: __dirname + '/preload.js',
},
});
win.setBackgroundColor('#E8ECF0');
if (isDev) {
win.webContents.openDevTools();
}
const unlistenToState = WindowState.listen(win, windowState);
if (isDev) {
win.loadURL(`file://${__dirname}/loading.html`);
// Wait for the development server to start
setTimeout(() => {
promiseRetry(retry => win.loadURL('http://localhost:3001/').catch(retry));
}, 3000);
} else {
win.loadURL(`app://actual/`);
}
win.on('close', () => {
// We don't want to close the budget on exit because that will
// clear the state which re-opens the last budget automatically on
// startup
if (!IS_QUITTING) {
clientWin.webContents.executeJavaScript('__actionsForMenu.closeBudget()');
}
});
win.on('closed', () => {
clientWin = null;
updateMenu(false);
unlistenToState();
});
win.on('unresponsive', () => {
console.log(
'browser window went unresponsive (maybe because of a modal though)',
);
});
win.on('focus', async () => {
let url = clientWin.webContents.getURL();
if (url.includes('app://') || url.includes('localhost:')) {
clientWin.webContents.executeJavaScript('__actionsForMenu.focused()');
}
});
win.webContents.on('did-finish-load', () => {
win.webContents.send('set-socket', { name: serverSocket });
});
// hit when middle-clicking buttons or <a href/> with a target set to _blank
// always deny, optionally redirect to browser
win.webContents.setWindowOpenHandler(({ url }) => {
if (isExternalUrl(url)) {
shell.openExternal(url);
}
return { action: 'deny' };
});
// hit when clicking <a href/> with no target
// optionally redirect to browser
win.webContents.on('will-navigate', (event, url) => {
if (isExternalUrl(url)) {
shell.openExternal(url);
event.preventDefault();
}
});
if (process.platform === 'win32') {
Menu.setApplicationMenu(null);
win.setMenu(getMenu(isDev, createWindow));
} else {
Menu.setApplicationMenu(getMenu(isDev, createWindow));
}
clientWin = win;
}
function isExternalUrl(url) {
return !url.includes('localhost:') && !url.includes('app://');
}
function updateMenu(isBudgetOpen) {
const menu = getMenu(isDev, createWindow);
const file = menu.items.filter(item => item.label === 'File')[0];
const fileItems = file.submenu.items;
fileItems
.filter(item => item.label === 'Load Backup...')
.map(item => (item.enabled = isBudgetOpen));
let tools = menu.items.filter(item => item.label === 'Tools')[0];
tools.submenu.items.forEach(item => {
item.enabled = isBudgetOpen;
});
const edit = menu.items.filter(item => item.label === 'Edit')[0];
const editItems = edit.submenu.items;
editItems
.filter(item => item.label === 'Undo' || item.label === 'Redo')
.map(item => (item.enabled = isBudgetOpen));
if (process.platform === 'win32') {
if (clientWin) {
clientWin.setMenu(menu);
}
} else {
Menu.setApplicationMenu(menu);
}
}
app.setAppUserModelId('com.shiftreset.actual');
app.on('ready', async () => {
serverSocket = await getRandomPort();
// Install an `app://` protocol that always returns the base HTML
// file no matter what URL it is. This allows us to use react-router
// on the frontend
protocol.registerFileProtocol('app', (request, callback) => {
if (request.method !== 'GET') {
callback({ error: -322 }); // METHOD_NOT_SUPPORTED from chromium/src/net/base/net_error_list.h
return null;
}
const parsedUrl = new URL(request.url);
if (parsedUrl.protocol !== 'app:') {
callback({ error: -302 }); // UNKNOWN_URL_SCHEME
return;
}
if (parsedUrl.host !== 'actual') {
callback({ error: -105 }); // NAME_NOT_RESOLVED
return;
}
const pathname = parsedUrl.pathname;
if (pathname.startsWith('/static')) {
callback({
path: path.normalize(`${__dirname}/client-build${pathname}`),
});
} else {
callback({
path: path.normalize(`${__dirname}/client-build/index.html`),
});
}
});
if (process.argv[1] !== '--server') {
await createWindow();
}
// This is mainly to aid debugging Sentry errors - it will add a
// breadcrumb
require('electron').powerMonitor.on('suspend', () => {
console.log('Suspending', new Date());
});
createBackgroundProcess(serverSocket);
});
app.on('window-all-closed', () => {
// On macOS, closing all windows shouldn't exit the process
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('before-quit', () => {
IS_QUITTING = true;
if (serverProcess) {
serverProcess.kill();
serverProcess = null;
}
});
app.on('activate', () => {
if (clientWin === null) {
createWindow();
}
});
ipcMain.on('get-bootstrap-data', event => {
event.returnValue = {
version: app.getVersion(),
isDev,
};
});
ipcMain.handle('relaunch', () => {
app.relaunch();
app.exit();
});
ipcMain.handle('open-file-dialog', (event, { filters, properties }) => {
return dialog.showOpenDialogSync({
properties: properties || ['openFile'],
filters,
});
});
ipcMain.handle(
'save-file-dialog',
(event, { title, defaultPath, fileContents }) => {
let fileLocation = dialog.showSaveDialogSync({ title, defaultPath });
return new Promise((resolve, reject) => {
if (fileLocation) {
fs.writeFile(fileLocation, fileContents, error => {
return reject(error);
});
}
resolve();
});
},
);
ipcMain.handle('open-external-url', (event, url) => {
shell.openExternal(url);
});
ipcMain.on('show-about', () => {
about.openAboutWindow();
});
ipcMain.on('screenshot', () => {
if (isDev) {
let width = 1100;
// This is for the main screenshot inside the frame
clientWin.setSize(width, Math.floor(width * (427 / 623)));
// clientWin.setSize(width, Math.floor(width * (495 / 700)));
}
});
ipcMain.on('check-for-update', () => {
// If the updater is in the middle of an update already, send the
// about window the current status
if (updater.isChecking()) {
// This should always come from the about window so we can
// guarantee that it exists. If we ever see an error here
// something is wrong
about.getWindow().webContents.send(updater.getLastEvent());
} else {
updater.check();
}
});
ipcMain.on('apply-update', () => {
updater.apply();
});
ipcMain.on('update-menu', (event, isBudgetOpen) => {
updateMenu(isBudgetOpen);
});