HeliosLauncher/index.js

535 lines
17 KiB
JavaScript

// 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, queryJS } = 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: 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: 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()
}
})