// Requirements const { app, BrowserWindow, ipcMain, Menu, shell } = require('electron') const autoUpdater = require('electron-updater').autoUpdater const ejse = require('ejs-electron') const fs = require('fs-extra') const path = require('path') const semver = require('semver') const { pathToFileURL } = require('url') const { AZURE_CLIENT_ID, MSFT_OPCODE, MSFT_REPLY_TYPE, MSFT_ERROR, SHELL_OPCODE } = require('./app/assets/js/ipcconstants') const { Type } = require('helios-distribution-types') const { totalmem, freemem, tmpdir } = require('node:os') const { prerelease } = require('semver') const { addMojangAccount, addMicrosoftAccount, removeMojangAccount, removeMicrosoftAccount, validateSelected } = require('./app/assets/js/main/authmanager') const ConfigManager = require('./app/assets/js/main/configmanager') const { DistroAPI } = require('./app/assets/js/main/distromanager') // eslint-disable-next-line no-unused-vars const { HeliosDistribution } = require('helios-core/common') const { LoggerUtil } = require('helios-core') const { getLang, setupLanguage, queryEJS } = require('./app/assets/js/main/langloader') const logger = LoggerUtil.getLogger('Preloader') // Log deprecation and process warnings. process.traceProcessWarnings = true process.traceDeprecation = true // Load ConfigManager ConfigManager.load() // Yuck! // TODO Fix this DistroAPI['commonDir'] = ConfigManager.getCommonDirectory() DistroAPI['instanceDir'] = ConfigManager.getInstanceDirectory() // Setup Lang setupLanguage() /** * * @param {HeliosDistribution} data */ function onDistroLoad(data){ if(data != null){ // Resolve the selected server if its value has yet to be set. if(ConfigManager.getSelectedServer() == null || data.getServerById(ConfigManager.getSelectedServer()) == null){ logger.info('Determining default selected server..') ConfigManager.setSelectedServer(data.getMainServer().rawServer.id) ConfigManager.save() } } win.webContents.send('distributionIndexDone', data != null) } // Ensure Distribution is downloaded and cached. DistroAPI.getDistribution() .then(heliosDistro => { logger.info('Loaded distribution index.') onDistroLoad(heliosDistro) }) .catch(err => { logger.info('Failed to load an older version of the distribution index.') logger.info('Application cannot run.') logger.error(err) onDistroLoad(null) }) // Clean up temp dir incase previous launches ended unexpectedly. fs.remove(path.join(tmpdir(), ConfigManager.getTempNativeFolder()), (err) => { if(err){ logger.warn('Error while cleaning natives directory', err) } else { logger.info('Cleaned natives directory.') } }) const autoUpdateChannel = new MessageChannel() // ORIGINAL BELOW // Setup auto updater. function initAutoUpdater(data) { if(data){ autoUpdater.allowPrerelease = true } else { // Defaults to true if application version contains prerelease components (e.g. 0.12.1-alpha.1) // autoUpdater.allowPrerelease = true } if(!app.isPackaged){ autoUpdater.autoInstallOnAppQuit = false autoUpdater.updateConfigPath = path.join(__dirname, 'dev-app-update.yml') } if(process.platform === 'darwin'){ autoUpdater.autoDownload = false } autoUpdater.on('update-available', (info) => { autoUpdateChannel.port1.postMessage(['autoUpdateNotification', 'update-available', info]) }) autoUpdater.on('update-downloaded', (info) => { autoUpdateChannel.port1.postMessage(['autoUpdateNotification', 'update-downloaded', info]) }) autoUpdater.on('update-not-available', (info) => { autoUpdateChannel.port1.postMessage(['autoUpdateNotification', 'update-not-available', info]) }) autoUpdater.on('checking-for-update', () => { autoUpdateChannel.port1.postMessage(['autoUpdateNotification', 'checking-for-update']) }) autoUpdater.on('error', (err) => { autoUpdateChannel.port1.postMessage(['autoUpdateNotification', 'realerror', err]) }) } /* // all on autoupdatechannel [ command, arg1, arg2, ... ] */ autoUpdateChannel.port1.on('message', (event) => { const command = event[0] switch(command) { case 'initAutoUpdater': console.log('Initializing auto updater.') initAutoUpdater(event[1]) autoUpdateChannel.port1.postMessage([ 'autoUpdateNotification', 'ready' ]) break case 'checkForUpdate': // TODO Test that error is passed properly autoUpdater.checkForUpdates() .catch(err => { autoUpdateChannel.port1.postMessage([ 'autoUpdateNotification', 'realerror', err ]) }) break case 'allowPrereleaseChange': if(!event[1]){ const preRelComp = semver.prerelease(app.getVersion()) if(preRelComp != null && preRelComp.length > 0){ autoUpdater.allowPrerelease = true } else { autoUpdater.allowPrerelease = event[1] } } else { autoUpdater.allowPrerelease = event[1] } break case 'installUpdateNow': autoUpdater.quitAndInstall() break default: console.log('Unknown command', command) break } }) // Open channel to listen for update actions. ipcMain.on('autoUpdateAction', (event, arg, data) => { switch(arg){ case 'initAutoUpdater': console.log('Initializing auto updater.') initAutoUpdater(event, data) event.sender.send('autoUpdateNotification', 'ready') break case 'checkForUpdate': autoUpdater.checkForUpdates() .catch(err => { event.sender.send('autoUpdateNotification', 'realerror', err) }) break case 'allowPrereleaseChange': if(!data){ const preRelComp = semver.prerelease(app.getVersion()) if(preRelComp != null && preRelComp.length > 0){ autoUpdater.allowPrerelease = true } else { autoUpdater.allowPrerelease = data } } else { autoUpdater.allowPrerelease = data } break case 'installUpdateNow': autoUpdater.quitAndInstall() break default: console.log('Unknown argument', arg) break } }) // Handle trash item. ipcMain.handle(SHELL_OPCODE.TRASH_ITEM, async (event, ...args) => { try { await shell.trashItem(args[0]) return { result: true } } catch(error) { return { result: false, error: error } } }) // Disable hardware acceleration. // https://electronjs.org/docs/tutorial/offscreen-rendering app.disableHardwareAcceleration() const REDIRECT_URI_PREFIX = 'https://login.microsoftonline.com/common/oauth2/nativeclient?' // Microsoft Auth Login let msftAuthWindow let msftAuthSuccess let msftAuthViewSuccess let msftAuthViewOnClose ipcMain.on(MSFT_OPCODE.OPEN_LOGIN, (ipcEvent, ...arguments_) => { if (msftAuthWindow) { ipcEvent.reply(MSFT_OPCODE.REPLY_LOGIN, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.ALREADY_OPEN, msftAuthViewOnClose) return } msftAuthSuccess = false msftAuthViewSuccess = arguments_[0] msftAuthViewOnClose = arguments_[1] msftAuthWindow = new BrowserWindow({ title: LangLoader.queryJS('index.microsoftLoginTitle'), backgroundColor: '#222222', width: 520, height: 600, frame: true, icon: getPlatformIcon('SealCircle') }) msftAuthWindow.on('closed', () => { msftAuthWindow = undefined }) msftAuthWindow.on('close', () => { if(!msftAuthSuccess) { ipcEvent.reply(MSFT_OPCODE.REPLY_LOGIN, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.NOT_FINISHED, msftAuthViewOnClose) } }) msftAuthWindow.webContents.on('did-navigate', (_, uri) => { if (uri.startsWith(REDIRECT_URI_PREFIX)) { let queries = uri.substring(REDIRECT_URI_PREFIX.length).split('#', 1).toString().split('&') let queryMap = {} queries.forEach(query => { const [name, value] = query.split('=') queryMap[name] = decodeURI(value) }) ipcEvent.reply(MSFT_OPCODE.REPLY_LOGIN, MSFT_REPLY_TYPE.SUCCESS, queryMap, msftAuthViewSuccess) msftAuthSuccess = true msftAuthWindow.close() msftAuthWindow = null } }) msftAuthWindow.removeMenu() msftAuthWindow.loadURL(`https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?prompt=select_account&client_id=${AZURE_CLIENT_ID}&response_type=code&scope=XboxLive.signin%20offline_access&redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient`) }) // Microsoft Auth Logout let msftLogoutWindow let msftLogoutSuccess let msftLogoutSuccessSent ipcMain.on(MSFT_OPCODE.OPEN_LOGOUT, (ipcEvent, uuid, isLastAccount) => { if (msftLogoutWindow) { ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.ALREADY_OPEN) return } msftLogoutSuccess = false msftLogoutSuccessSent = false msftLogoutWindow = new BrowserWindow({ title: LangLoader.queryJS('index.microsoftLogoutTitle'), backgroundColor: '#222222', width: 520, height: 600, frame: true, icon: getPlatformIcon('SealCircle') }) msftLogoutWindow.on('closed', () => { msftLogoutWindow = undefined }) msftLogoutWindow.on('close', () => { if(!msftLogoutSuccess) { ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.NOT_FINISHED) } else if(!msftLogoutSuccessSent) { msftLogoutSuccessSent = true ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.SUCCESS, uuid, isLastAccount) } }) msftLogoutWindow.webContents.on('did-navigate', (_, uri) => { if(uri.startsWith('https://login.microsoftonline.com/common/oauth2/v2.0/logoutsession')) { msftLogoutSuccess = true setTimeout(() => { if(!msftLogoutSuccessSent) { msftLogoutSuccessSent = true ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.SUCCESS, uuid, isLastAccount) } if(msftLogoutWindow) { msftLogoutWindow.close() msftLogoutWindow = null } }, 5000) } }) msftLogoutWindow.removeMenu() msftLogoutWindow.loadURL('https://login.microsoftonline.com/common/oauth2/v2.0/logout') }) // 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 win function createWindow() { win = new BrowserWindow({ width: 980, height: 552, icon: getPlatformIcon('SealCircle'), frame: false, webPreferences: { preload: path.join(__dirname, 'app', 'assets', 'js', 'preloader.js'), nodeIntegration: true, contextIsolation: true }, backgroundColor: '#171614' }) // Disable zoom, needed for darwin. win.webContents.setZoomLevel(0) win.webContents.setVisualZoomLevelLimits(1, 1) const data = { bkid: Math.floor((Math.random() * fs.readdirSync(path.join(__dirname, 'app', 'assets', 'images', 'backgrounds')).length)), lang: (str, placeHolders) => queryEJS(str, placeHolders) } Object.entries(data).forEach(([key, val]) => ejse.data(key, val)) win.loadURL(pathToFileURL(path.join(__dirname, 'app', 'app.ejs')).toString()) /*win.once('ready-to-show', () => { win.show() })*/ win.removeMenu() win.resizable = true win.on('closed', () => { win = null }) } function createMenu() { if(process.platform === 'darwin') { // Extend default included application menu to continue support for quit keyboard shortcut let applicationSubMenu = { label: 'Application', submenu: [{ label: 'About Application', selector: 'orderFrontStandardAboutPanel:' }, { type: 'separator' }, { label: 'Quit', accelerator: 'Command+Q', click: () => { app.quit() } }] } // New edit menu adds support for text-editing keyboard shortcuts let editSubMenu = { label: 'Edit', submenu: [{ label: 'Undo', accelerator: 'CmdOrCtrl+Z', selector: 'undo:' }, { label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', selector: 'redo:' }, { type: 'separator' }, { label: 'Cut', accelerator: 'CmdOrCtrl+X', selector: 'cut:' }, { label: 'Copy', accelerator: 'CmdOrCtrl+C', selector: 'copy:' }, { label: 'Paste', accelerator: 'CmdOrCtrl+V', selector: 'paste:' }, { label: 'Select All', accelerator: 'CmdOrCtrl+A', selector: 'selectAll:' }] } // Bundle submenus into a single template and build a menu object with it let menuTemplate = [applicationSubMenu, editSubMenu] let menuObject = Menu.buildFromTemplate(menuTemplate) // Assign it to the application Menu.setApplicationMenu(menuObject) } } function getPlatformIcon(filename){ let ext switch(process.platform) { case 'win32': ext = 'ico' break case 'darwin': case 'linux': default: ext = 'png' break } return path.join(__dirname, 'app', 'assets', 'images', `${filename}.${ext}`) } app.whenReady().then(() => { ipcMain.handle('os.totalmem', () => totalmem()) ipcMain.handle('os.freemem', () => freemem()) ipcMain.handle('semver.prerelease', (version) => prerelease(version)) ipcMain.handle('path.join', (...args) => path.join(args)) ipcMain.handle('app.isDev', () => !app.isPackaged) ipcMain.handle('app.getVersion', () => app.getVersion()) ipcMain.handle('shell.openExternal', (url) => shell.openExternal(url)) ipcMain.handle('shell.openPath', (path) => shell.openPath(path)) ipcMain.handle('xwindow.close', () => win.close()) ipcMain.handle('xwindow.setProgressBar', (progress) => win.setProgressBar(progress)) ipcMain.handle('xwindow.toggleDevTools', () => win.webContents.toggleDevTools()) ipcMain.handle('xwindow.minimize', () => win.minimize()) ipcMain.handle('xwindow.maximize', () => win.maximize()) ipcMain.handle('xwindow.unmaximize', () => win.unmaximize()) ipcMain.handle('xwindow.isMaximized', () => win.isMaximized()) ipcMain.handle('process.platform', () => process.platform) ipcMain.handle('process.arch', () => process.arch) ipcMain.handle('hc.type', () => Type) ipcMain.handle('AuthManager.addMojangAccount', async (username, password) => await addMojangAccount(username, password)) ipcMain.handle('AuthManager.addMicrosoftAccount', async (authCode) => await addMicrosoftAccount(authCode)) ipcMain.handle('AuthManager.removeMojangAccount', async (uuid) => await removeMojangAccount(uuid)) ipcMain.handle('AuthManager.removeMicrosoftAccount', async (uuid) => await removeMicrosoftAccount(uuid)) ipcMain.handle('AuthManager.validateSelected', async () => await validateSelected()) ipcMain.handle('Lang.getLang', () => getLang()) ipcMain.handle('AutoUpdater.port2', () => autoUpdateChannel.port2) createWindow() createMenu() }) app.on('window-all-closed', () => { // On macOS it is common for applications and their menu bar // to stay active until the user quits explicitly with Cmd + Q if (process.platform !== 'darwin') { app.quit() } }) 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 (win === null) { createWindow() } })