actual/packages/desktop-electron/index.js
2022-11-10 12:33:19 -05:00

390 lines
9.5 KiB
JavaScript

const isDev = require('electron-is-dev');
require('module').globalPaths.push(__dirname + '/..');
const {
app,
ipcMain,
BrowserWindow,
Menu,
dialog,
shell,
protocol
} = require('electron');
// 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 SentryClient = require('@sentry/electron');
const findOpenSocket = require('./findOpenSocket');
const updater = require('./updater');
const about = require('./about');
const { SentryMetricIntegration } = require('@jlongster/sentry-metrics-actual');
require('./security');
if (!isDev) {
// Install sentry
SentryClient.init({
dsn:
'https://f2fa901455894dc8bf28210ef1247e2d:b9e69eb21d9740539b3ff593f7346396@sentry.io/261029',
release: app.getVersion(),
enableUnresponsive: false,
ignoreErrors: ['PostError', 'HTTPError', 'ResizeObserver loop'],
integrations: [
new SentryMetricIntegration({
url: 'https://sync.actualbudget.com/metrics',
metric: 'app-errors',
dimensions: { platform: 'desktop' },
headers: { Origin: 'app://actual' }
})
]
});
}
const { fork } = require('child_process');
const path = require('path');
const getMenu = require('./menu');
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');
}
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;
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) {
const SentryClient = require('@sentry/electron');
serverProcess = fork(__dirname + '/server.js', [
'--subprocess',
app.getVersion(),
socketName
]);
serverProcess.on('message', msg => {
switch (msg.type) {
case 'captureEvent':
let event = msg.event;
SentryClient.captureEvent(event);
break;
case 'captureBreadcrumb':
SentryClient.addBreadcrumb(msg.breadcrumb);
break;
case 'shouldAutoUpdate':
if (msg.flag) {
updater.start();
} else {
updater.stop();
}
break;
default:
console.log('Unknown server message: ' + msg.type);
}
});
}
function createBackgroundWindow(socketName) {
const win = new BrowserWindow({
show: true,
title: 'Actual Server',
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
});
win.loadURL(`file://${__dirname}/server.html`);
win.webContents.on('did-finish-load', () => {
win.webContents.send('set-socket', { name: socketName });
});
win.on('closed', () => {
serverWin = null;
});
serverWin = win;
}
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',
titleBarStyle: 'hiddenInset',
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: __dirname + '/preload.js'
}
});
win.setBackgroundColor('#E8ECF0');
const unlistenToState = WindowState.listen(win, windowState);
if (isDev) {
win.loadURL(`file://${__dirname}/loading.html`);
// Wait for the development server to start
setTimeout(() => {
win.loadURL('http://localhost:3001/');
}, 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 });
});
if (process.platform === 'win32') {
Menu.setApplicationMenu(null);
win.setMenu(getMenu(isDev, createWindow));
} else {
Menu.setApplicationMenu(getMenu(isDev, createWindow));
}
clientWin = win;
}
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 === 'Start Tutorial' ||
item.label === 'Manage Payees...' ||
item.label === 'Manage Rules...' ||
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 findOpenSocket();
// 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') || pathname.startsWith('/Inter')) {
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());
});
if (isDev) {
createBackgroundWindow(serverSocket);
} else {
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('get-version', () => {
return app.getVersion();
});
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 }) => {
return dialog.showSaveDialogSync({ title, defaultPath });
});
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);
});