// Built-in modules
const { spawn } = require('child_process');
const path = require('path');
// Electron modules
const { app, BrowserWindow, ipcMain } = require('electron');
// Extra modules
const getPort = require('get-port');
const isDevMode = require('electron-is-dev');
const { get } = require('axios');
/**
* @description - Shuts down Electron & Flask.
* @param {number} port - Port that Flask server is running on.
*/
const shutdown = (port) => {
get(`http://localhost:${port}/quit`)
.then(app.quit)
.catch(app.quit);
};
/**
* @namespace BrowserWindow
* @description - Electron browser windows.
* @tutorial - https://www.electronjs.org/docs/api/browser-window
*/
const browserWindows = {};
/**
* @description - Creates main window.
* @param {number} port - Port that Flask server is running on.
*
* @memberof BrowserWindow
*/
const createMainWindow = (port) => {
const { loadingWindow, mainWindow } = browserWindows;
/**
* @description - Function to use custom JavaSCript in the DOM.
* @param {string} command - JavaScript to execute in DOM.
* @param {function} callback - Callback to execute here once complete.
* @returns {Promise}
*/
const executeOnWindow = (command, callback) => {
return mainWindow.webContents.executeJavaScript(command)
.then(callback)
.catch(console.error);
};
/**
* If in developer mode, show a loading window while
* the app and developer server compile.
*/
if (isDevMode) {
mainWindow.loadURL('http://localhost:3000');
mainWindow.hide();
/**
* Hide loading window and show main window
* once the main window is ready.
*/
mainWindow.webContents.on('did-finish-load', () => {
mainWindow.webContents.openDevTools({ mode: 'undocked' });
/**
* Checks page for errors that may have occurred
* during the hot-loading process.
*/
const isPageLoaded = `
var isBodyFull = document.body.innerHTML !== "";
var isHeadFull = document.head.innerHTML !== "";
var isLoadSuccess = isBodyFull && isHeadFull;
isLoadSuccess || Boolean(location.reload());
`;
/**
* @description Updates windows if page is loaded
* @param {*} isLoaded
*/
const handleLoad = (isLoaded) => {
if (isLoaded) {
/**
* Keep show() & hide() in this order to prevent
* unresponsive behavior during page load.
*/
mainWindow.show();
loadingWindow.hide();
}
};
/**
* Checks if the page has been populated with
* React project. if so, shows the main page.
*/
executeOnWindow(isPageLoaded, handleLoad);
});
}
/**
* If using in production, the built version of the
* React project will be used instead of localhost.
*/
else mainWindow.loadFile(path.join(__dirname, 'build/index.html'));
/**
* @description - Controls the opacity of title bar on focus/blur.
* @param {number} value - Opacity to set for title bar.
*/
const setTitleOpacity = (value) => `
if(document.readyState === 'complete') {
const titleBar = document.getElementById('electron-window-title-text');
const titleButtons = document.getElementById('electron-window-title-buttons');
if(titleBar) titleBar.style.opacity = ${value};
if(titleButtons) titleButtons.style.opacity = ${value};
}
`;
mainWindow.on('focus', () => executeOnWindow(setTitleOpacity(1)));
mainWindow.on('blur', () => executeOnWindow(setTitleOpacity(0.5)));
/**
* Listen and respond to ipcRenderer events on the frontend.
* @see `src\utils\services.js`
*/
ipcMain.on('app-maximize', (_event, _arg) => mainWindow.maximize());
ipcMain.on('app-minimize', (_event, _arg) => mainWindow.minimize());
ipcMain.on('app-quit', (_event, _arg) => shutdown(port));
ipcMain.on('app-unmaximize', (_event, _arg) => mainWindow.unmaximize());
ipcMain.on('get-port-number', (event, _arg) => {
event.returnValue = port;
});
};
/**
* @description - Creates loading window to show while build is created.
* @memberof BrowserWindow
*/
const createLoadingWindow = () => {
return new Promise((resolve, reject) => {
const { loadingWindow } = browserWindows;
// Variants of developer loading screen
const loaderConfig = {
react: 'utilities/loaders/react/index.html',
redux: 'utilities/loaders/redux/index.html'
};
try {
loadingWindow.loadFile(path.join(__dirname, loaderConfig.redux));
loadingWindow.webContents.on('did-finish-load', () => {
loadingWindow.show();
resolve();
});
} catch (error) {
console.error(error);
reject();
}
});
};
/**
* @description - Installs developer extensions.
* @returns {Promise}
*/
const installExtensions = async () => {
const isForceDownload = Boolean(process.env.UPGRADE_EXTENSIONS);
const installer = require('electron-devtools-installer');
const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS']
.map((extension) => installer.default(installer[extension], isForceDownload));
return Promise
.allSettled(extensions)
.catch(console.error);
};
/**
* This method will be called when Electron has finished
* initialization and is ready to create browser windows.
* Some APIs can only be used after this event occurs.
*/
app.whenReady().then(async () => {
/**
* Method to set port in range of 3001-3999,
* based on availability.
*/
const port = await getPort({
port: getPort.makeRange(3001, 3999)
});
/**
* Assigns the main browser window on the
* browserWindows object.
*/
browserWindows.mainWindow = new BrowserWindow({
frame: false,
webPreferences: {
contextIsolation: false,
enableRemoteModule: true,
nodeIntegration: true,
preload: path.join(app.getAppPath(), 'preload.js')
}
});
/**
* If not using in production, use the loading window
* and run Flask in shell.
*/
if (isDevMode) {
await installExtensions(); // React, Redux devTools
browserWindows.loadingWindow = new BrowserWindow({ frame: false });
createLoadingWindow().then(() => createMainWindow(port));
spawn(`python app.py ${port}`, { detached: true, shell: true, stdio: 'inherit' });
}
/**
* If using in production, use the main window
* and run bundled app (dmg, elf, or exe) file.
*/
else {
createMainWindow(port);
// Dynamic script assignment for starting Flask in production
const runFlask = {
darwin: `open -gj "${path.join(app.getAppPath(), 'resources', 'app.app')}" --args`,
linux: './resources/app/app',
win32: 'start ./resources/app/app.exe'
}[process.platform];
spawn(`${runFlask} ${port}`, { detached: false, shell: true, stdio: 'pipe' });
}
app.on('activate', () => {
/**
* On macOS it's common to re-create a window in the app when the
* dock icon is clicked and there are no other windows open.
*/
if (BrowserWindow.getAllWindows().length === 0) createMainWindow(port);
});
/**
* Ensures that only a single instance of the app
* can run, this correlates with the "name" property
* used in `package.json`.
*/
const initialInstance = app.requestSingleInstanceLock();
if (!initialInstance) app.quit();
else {
app.on('second-instance', () => {
if (browserWindows.mainWindow?.isMinimized()) browserWindows.mainWindow?.restore();
browserWindows.mainWindow?.focus();
});
}
/**
* Quit when all windows are closed, except on macOS. There, it's common
* for applications and their menu bar to stay active until the user quits
* explicitly with Cmd + Q.
*/
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
shutdown(port);
}
});
});