390 lines
9.5 KiB
JavaScript
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, (width * (427 / 623)) | 0);
|
||
|
// clientWin.setSize(width, (width * (495 / 700)) | 0);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
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);
|
||
|
});
|