main.js

  1. // Built-in modules
  2. const { spawn } = require('child_process');
  3. const path = require('path');
  4. const fs = require('fs');
  5. const util = require('util');
  6. // Electron modules
  7. const { app, BrowserWindow, ipcMain } = require('electron');
  8. // Extra modules
  9. const getPort = require('get-port');
  10. const isDevMode = require('electron-is-dev');
  11. const { get } = require('axios');
  12. // Set path to error log
  13. const errorLogPath = path.join(
  14. process.env.APPDATA,
  15. 'MKVToolNix Batch Tool',
  16. 'error.log'
  17. );
  18. // Prepare file for writing logs
  19. const errorLogFile = fs.createWriteStream(errorLogPath, { flags: 'a' });
  20. // Update console.error so stdout is saved to log file
  21. console.error = (...args) => {
  22. const now = new Date();
  23. const message = util.format(...args);
  24. // Ensure message configuration is same as Flask's
  25. const timestamp = now.toLocaleString('en-US', {
  26. year: 'numeric',
  27. month: '2-digit',
  28. day: '2-digit',
  29. hour: '2-digit',
  30. minute: '2-digit',
  31. second: '2-digit',
  32. hour12: false
  33. }).replace(',', '');
  34. const formattedMessage = `${timestamp} - MKVToolNix Batch Tool - ERROR - ${message}\n`;
  35. // Write stdout to error.log file
  36. errorLogFile.write(formattedMessage);
  37. process.stdout.write(formattedMessage);
  38. };
  39. /**
  40. * @description - Shuts down Electron & Flask.
  41. * @param {number} port - Port that Flask server is running on.
  42. */
  43. const shutdown = (port) => {
  44. get(`http://localhost:${port}/quit`).then(app.quit).catch(app.quit);
  45. };
  46. /**
  47. * @namespace BrowserWindow
  48. * @description - Electron browser windows.
  49. * @tutorial - https://www.electronjs.org/docs/api/browser-window
  50. */
  51. const browserWindows = {};
  52. /**
  53. * @description - Creates main window.
  54. * @param {number} port - Port that Flask server is running on.
  55. *
  56. * @memberof BrowserWindow
  57. */
  58. const createMainWindow = (port) => {
  59. const { loadingWindow, mainWindow } = browserWindows;
  60. /**
  61. * @description - Function to use custom JavaSCript in the DOM.
  62. * @param {string} command - JavaScript to execute in DOM.
  63. * @param {function} callback - Callback to execute here once complete.
  64. * @returns {promise}
  65. */
  66. const executeOnWindow = (command, callback) => {
  67. return mainWindow.webContents
  68. .executeJavaScript(command)
  69. .then(callback)
  70. .catch(console.error);
  71. };
  72. /**
  73. * If in developer mode, show a loading window while
  74. * the app and developer server compile.
  75. */
  76. if (isDevMode) {
  77. mainWindow.loadURL('http://localhost:3000');
  78. mainWindow.hide();
  79. /**
  80. * Opening devTools, must be done before dom-ready
  81. * to avoid occasional error from the webContents
  82. * object being destroyed.
  83. */
  84. mainWindow.webContents.openDevTools({ mode: 'undocked' });
  85. /**
  86. * Hide loading window and show main window
  87. * once the main window is ready.
  88. */
  89. mainWindow.webContents.on('did-finish-load', () => {
  90. /**
  91. * Checks page for errors that may have occurred
  92. * during the hot-loading process.
  93. */
  94. const isPageLoaded = `
  95. var isBodyFull = document.body.innerHTML !== "";
  96. var isHeadFull = document.head.innerHTML !== "";
  97. var isLoadSuccess = isBodyFull && isHeadFull;
  98. isLoadSuccess || Boolean(location.reload());
  99. `;
  100. /**
  101. * @description Updates windows if page is loaded
  102. * @param {*} isLoaded
  103. */
  104. const handleLoad = (isLoaded) => {
  105. if (isLoaded) {
  106. /**
  107. * Keep show() & hide() in this order to prevent
  108. * unresponsive behavior during page load.
  109. */
  110. mainWindow.show();
  111. loadingWindow.destroy();
  112. }
  113. };
  114. /**
  115. * Checks if the page has been populated with
  116. * React project. if so, shows the main page.
  117. */
  118. executeOnWindow(isPageLoaded, handleLoad);
  119. });
  120. } else mainWindow.loadFile(path.join(__dirname, 'build/index.html'));
  121. /**
  122. * If using in production, the built version of the
  123. * React project will be used instead of localhost.
  124. */
  125. /**
  126. * @description - Controls the opacity of title bar on focus/blur.
  127. * @param {number} value - Opacity to set for title bar.
  128. */
  129. const setTitleOpacity = (value) => `
  130. if(document.readyState === 'complete') {
  131. const titleBar = document.getElementById('electron-window-title-text');
  132. const titleButtons = document.getElementById('electron-window-title-buttons');
  133. if(titleBar) titleBar.style.opacity = ${value};
  134. if(titleButtons) titleButtons.style.opacity = ${value};
  135. }
  136. `;
  137. mainWindow.on('focus', () => executeOnWindow(setTitleOpacity(1)));
  138. mainWindow.on('blur', () => executeOnWindow(setTitleOpacity(0.5)));
  139. /**
  140. * Listen and respond to ipcRenderer events on the frontend.
  141. * @see `src\utils\services.js`
  142. */
  143. ipcMain.on('app-maximize', mainWindow.maximize);
  144. ipcMain.on('app-minimize', mainWindow.minimize);
  145. ipcMain.on('app-quit', () => shutdown(port));
  146. ipcMain.on('app-unmaximize', mainWindow.unmaximize);
  147. ipcMain.on('get-port-number', (event) => {
  148. event.returnValue = port;
  149. });
  150. ipcMain.on('app-restart', (_event, options) => {
  151. // Determine if debug or regular app is used
  152. const appPath = options.detached
  153. ? 'app.debug/app.debug.exe'
  154. : 'app/app.exe';
  155. // Determines if .py or .exe is used
  156. const script = isDevMode
  157. ? 'python app.py'
  158. : `start ./resources/${appPath}`;
  159. // Quit Flask, then restart in desired mode
  160. get(`http://localhost:${port}/quit`)
  161. .then(() => {
  162. spawn(`${script} ${port}`, options);
  163. if (options.detached) {
  164. mainWindow.webContents.openDevTools({ mode: 'undocked' });
  165. } else {
  166. mainWindow.webContents.closeDevTools();
  167. }
  168. })
  169. .catch(console.error);
  170. });
  171. };
  172. /**
  173. * @description - Creates loading window to show while build is created.
  174. * @memberof BrowserWindow
  175. */
  176. const createLoadingWindow = () => {
  177. return new Promise((resolve, reject) => {
  178. const { loadingWindow } = browserWindows;
  179. const loaderConfig = {
  180. react: 'utilities/loaders/react/index.html',
  181. redux: 'utilities/loaders/redux/index.html'
  182. };
  183. try {
  184. loadingWindow.loadFile(path.join(__dirname, loaderConfig.react));
  185. loadingWindow.webContents.on('did-finish-load', () => {
  186. resolve(loadingWindow.show());
  187. });
  188. } catch (error) {
  189. reject(console.error(error));
  190. }
  191. });
  192. };
  193. /**
  194. * This method will be called when Electron has finished
  195. * initialization and is ready to create browser windows.
  196. * Some APIs can only be used after this event occurs.
  197. */
  198. app.whenReady().then(async () => {
  199. /**
  200. * Method to set port in range of 3001-3999,
  201. * based on availability.
  202. */
  203. const port = await getPort({
  204. port: getPort.makeRange(3001, 3999)
  205. });
  206. /**
  207. * Assigns the main browser window on the
  208. * browserWindows object.
  209. */
  210. browserWindows.mainWindow = new BrowserWindow({
  211. frame: false,
  212. height: 370,
  213. maxHeight: 370,
  214. minHeight: 370,
  215. width: 675,
  216. minWidth: 675,
  217. maxWidth: 675,
  218. webPreferences: {
  219. contextIsolation: false,
  220. enableRemoteModule: true,
  221. nodeIntegration: true,
  222. preload: path.join(__dirname, 'preload.js')
  223. }
  224. });
  225. /**
  226. * If not using in production, use the loading window
  227. * and run Flask in shell.
  228. */
  229. if (isDevMode) {
  230. (browserWindows.loadingWindow = new BrowserWindow({ frame: false }));
  231. createLoadingWindow().then(() => createMainWindow(port));
  232. spawn(`python app.py ${port}`, {
  233. detached: true,
  234. shell: true,
  235. stdio: 'inherit'
  236. });
  237. } else {
  238. /**
  239. * If using in production, use the main window
  240. * and run bundled app.exe file.
  241. */
  242. createMainWindow(port);
  243. spawn(`start ./resources/app/app.exe ${port}`, {
  244. detached: false,
  245. shell: true,
  246. stdio: 'pipe'
  247. });
  248. }
  249. app.on('activate', () => {
  250. /**
  251. * On macOS it's common to re-create a window in the app when the
  252. * dock icon is clicked and there are no other windows open.
  253. */
  254. if (BrowserWindow.getAllWindows().length === 0) createMainWindow(port);
  255. });
  256. /**
  257. * Ensures that only a single instance of the app
  258. * can run, this correlates with the "name" property
  259. * used in `package.json`.
  260. */
  261. const initialInstance = app.requestSingleInstanceLock();
  262. if (!initialInstance) app.quit();
  263. else {
  264. app.on('second-instance', () => {
  265. if (browserWindows.mainWindow?.isMinimized()) { browserWindows.mainWindow?.restore(); }
  266. browserWindows.mainWindow?.focus();
  267. });
  268. }
  269. /**
  270. * Quit when all windows are closed, except on macOS. There, it's common
  271. * for applications and their menu bar to stay active until the user quits
  272. * explicitly with Cmd + Q.
  273. */
  274. app.on('window-all-closed', () => {
  275. if (process.platform !== 'darwin') shutdown(port);
  276. });
  277. });