// Requirements const os = require('os') const semver = require('semver') const DropinModUtil = require('./assets/js/dropinmodutil') const athShield = require('./assets/athshield/parserAthShield') const { MSFT_OPCODE, MSFT_REPLY_TYPE, MSFT_ERROR } = require('./assets/js/ipcconstants') const settingsState = { invalid: new Set() } function bindSettingsSelect(){ for(let ele of document.getElementsByClassName('settingsSelectContainer')) { const selectedDiv = ele.getElementsByClassName('settingsSelectSelected')[0] selectedDiv.onclick = (e) => { e.stopPropagation() closeSettingsSelect(e.target) e.target.nextElementSibling.toggleAttribute('hidden') e.target.classList.toggle('select-arrow-active') } } } function closeSettingsSelect(el){ for(let ele of document.getElementsByClassName('settingsSelectContainer')) { const selectedDiv = ele.getElementsByClassName('settingsSelectSelected')[0] const optionsDiv = ele.getElementsByClassName('settingsSelectOptions')[0] if(!(selectedDiv === el)) { selectedDiv.classList.remove('select-arrow-active') optionsDiv.setAttribute('hidden', '') } } } /* If the user clicks anywhere outside the select box, then close all select boxes: */ document.addEventListener('click', closeSettingsSelect) bindSettingsSelect() function bindFileSelectors(){ for(let ele of document.getElementsByClassName('settingsFileSelButton')){ ele.onclick = async e => { const isJavaExecSel = ele.id === 'settingsJavaExecSel' const directoryDialog = ele.hasAttribute('dialogDirectory') && ele.getAttribute('dialogDirectory') == 'true' const properties = directoryDialog ? ['openDirectory', 'createDirectory'] : ['openFile'] const options = { properties } if(ele.hasAttribute('dialogTitle')) { options.title = ele.getAttribute('dialogTitle') } if(isJavaExecSel && process.platform === 'win32') { options.filters = [ { name: Lang.queryJS('settings.fileSelectors.executables'), extensions: ['exe'] }, { name: Lang.queryJS('settings.fileSelectors.allFiles'), extensions: ['*'] } ] } const res = await remote.dialog.showOpenDialog(remote.getCurrentWindow(), options) if(!res.canceled) { ele.previousElementSibling.value = res.filePaths[0] if(isJavaExecSel) { await populateJavaExecDetails(ele.previousElementSibling.value) } } } } } bindFileSelectors() /** * General Settings Functions */ /** * Bind value validators to the settings UI elements. These will * validate against the criteria defined in the ConfigManager (if * any). If the value is invalid, the UI will reflect this and saving * will be disabled until the value is corrected. This is an automated * process. More complex UI may need to be bound separately. */ function initSettingsValidators(){ const sEls = document.getElementById('settingsContainer').querySelectorAll('[cValue]') Array.from(sEls).map((v, index, arr) => { const vFn = ConfigManager['validate' + v.getAttribute('cValue')] if(typeof vFn === 'function'){ if(v.tagName === 'INPUT'){ if(v.type === 'number' || v.type === 'text'){ v.addEventListener('keyup', (e) => { const v = e.target if(!vFn(v.value)){ settingsState.invalid.add(v.id) v.setAttribute('error', '') settingsSaveDisabled(true) } else { if(v.hasAttribute('error')){ v.removeAttribute('error') settingsState.invalid.delete(v.id) if(settingsState.invalid.size === 0){ settingsSaveDisabled(false) } } } }) } } } }) } /** * Load configuration values onto the UI. This is an automated process. */ async function initSettingsValues(){ const sEls = document.getElementById('settingsContainer').querySelectorAll('[cValue]') for(const v of sEls) { const cVal = v.getAttribute('cValue') const serverDependent = v.hasAttribute('serverDependent') // Means the first argument is the server id. const gFn = ConfigManager['get' + cVal] const gFnOpts = [] if(serverDependent) { gFnOpts.push(ConfigManager.getSelectedServer()) } if(typeof gFn === 'function'){ if(v.tagName === 'INPUT'){ if(v.type === 'number' || v.type === 'text'){ // Special Conditions if(cVal === 'JavaExecutable'){ v.value = gFn.apply(null, gFnOpts) await populateJavaExecDetails(v.value) } else if (cVal === 'DataDirectory'){ v.value = gFn.apply(null, gFnOpts) } else if(cVal === 'JVMOptions'){ v.value = gFn.apply(null, gFnOpts).join(' ') } else { v.value = gFn.apply(null, gFnOpts) } } else if(v.type === 'checkbox'){ v.checked = gFn.apply(null, gFnOpts) } } else if(v.tagName === 'DIV'){ if(v.classList.contains('rangeSlider')){ // Special Conditions if(cVal === 'MinRAM' || cVal === 'MaxRAM'){ let val = gFn.apply(null, gFnOpts) if(val.endsWith('M')){ val = Number(val.substring(0, val.length-1))/1024 } else { val = Number.parseFloat(val) } v.setAttribute('value', val) } else { v.setAttribute('value', Number.parseFloat(gFn.apply(null, gFnOpts))) } } } } } } /** * Save the settings values. */ function saveSettingsValues(){ const sEls = document.getElementById('settingsContainer').querySelectorAll('[cValue]') Array.from(sEls).map((v, index, arr) => { const cVal = v.getAttribute('cValue') const serverDependent = v.hasAttribute('serverDependent') // Means the first argument is the server id. const sFn = ConfigManager['set' + cVal] const sFnOpts = [] if(serverDependent) { sFnOpts.push(ConfigManager.getSelectedServer()) } if(typeof sFn === 'function'){ if(v.tagName === 'INPUT'){ if(v.type === 'number' || v.type === 'text'){ // Special Conditions if(cVal === 'JVMOptions'){ if(!v.value.trim()) { sFnOpts.push([]) sFn.apply(null, sFnOpts) } else { sFnOpts.push(v.value.trim().split(/\s+/)) sFn.apply(null, sFnOpts) } } else { sFnOpts.push(v.value) sFn.apply(null, sFnOpts) } } else if(v.type === 'checkbox'){ sFnOpts.push(v.checked) sFn.apply(null, sFnOpts) // Special Conditions if(cVal === 'AllowPrerelease'){ changeAllowPrerelease(v.checked) } } } else if(v.tagName === 'DIV'){ if(v.classList.contains('rangeSlider')){ // Special Conditions if(cVal === 'MinRAM' || cVal === 'MaxRAM'){ let val = Number(v.getAttribute('value')) if(val%1 > 0){ val = val*1024 + 'M' } else { val = val + 'G' } sFnOpts.push(val) sFn.apply(null, sFnOpts) } else { sFnOpts.push(v.getAttribute('value')) sFn.apply(null, sFnOpts) } } } } }) } let selectedSettingsTab = 'settingsTabAccount' /** * Modify the settings container UI when the scroll threshold reaches * a certain poin. * * @param {UIEvent} e The scroll event. */ function settingsTabScrollListener(e){ if(e.target.scrollTop > Number.parseFloat(getComputedStyle(e.target.firstElementChild).marginTop)){ document.getElementById('settingsContainer').setAttribute('scrolled', '') } else { document.getElementById('settingsContainer').removeAttribute('scrolled') } } /** * Bind functionality for the settings navigation items. */ function setupSettingsTabs(){ Array.from(document.getElementsByClassName('settingsNavItem')).map((val) => { if(val.hasAttribute('rSc')){ val.onclick = () => { settingsNavItemListener(val) } } }) } /** * Settings nav item onclick lisener. Function is exposed so that * other UI elements can quickly toggle to a certain tab from other views. * * @param {Element} ele The nav item which has been clicked. * @param {boolean} fade Optional. True to fade transition. */ function settingsNavItemListener(ele, fade = true){ if(ele.hasAttribute('selected')){ return } const navItems = document.getElementsByClassName('settingsNavItem') for(let i=0; i { $(`#${selectedSettingsTab}`).fadeIn({ duration: 250, start: () => { settingsTabScrollListener({ target: document.getElementById(selectedSettingsTab) }) } }) }) } else { $(`#${prevTab}`).hide(0, () => { $(`#${selectedSettingsTab}`).show({ duration: 0, start: () => { settingsTabScrollListener({ target: document.getElementById(selectedSettingsTab) }) } }) }) } } const settingsNavDone = document.getElementById('settingsNavDone') /** * Set if the settings save (done) button is disabled. * * @param {boolean} v True to disable, false to enable. */ function settingsSaveDisabled(v){ settingsNavDone.disabled = v } function fullSettingsSave() { saveSettingsValues() saveModConfiguration() ConfigManager.save() saveDropinModConfiguration() saveShaderpackSettings() } /* Closes the settings view and saves all data. */ settingsNavDone.onclick = () => { fullSettingsSave() switchView(getCurrentView(), VIEWS.landing) } /** * Account Management Tab */ const msftLoginLogger = LoggerUtil.getLogger('Microsoft Login') const msftLogoutLogger = LoggerUtil.getLogger('Microsoft Logout') // Bind the add mojang account button. document.getElementById('settingsAddMojangAccount').onclick = (e) => { switchView(getCurrentView(), VIEWS.login, 500, 500, () => { loginViewOnCancel = VIEWS.settings loginViewOnSuccess = VIEWS.settings loginCancelEnabled(true) }) } // Bind the add microsoft account button. document.getElementById('settingsAddMicrosoftAccount').onclick = (e) => { switchView(getCurrentView(), VIEWS.waiting, 500, 500, () => { ipcRenderer.send(MSFT_OPCODE.OPEN_LOGIN, VIEWS.settings, VIEWS.settings) }) } // Bind reply for Microsoft Login. ipcRenderer.on(MSFT_OPCODE.REPLY_LOGIN, (_, ...arguments_) => { if (arguments_[0] === MSFT_REPLY_TYPE.ERROR) { const viewOnClose = arguments_[2] console.log(arguments_) switchView(getCurrentView(), viewOnClose, 500, 500, () => { if(arguments_[1] === MSFT_ERROR.NOT_FINISHED) { // User cancelled. msftLoginLogger.info('Login cancelled by user.') return } // Unexpected error. setOverlayContent( Lang.queryJS('settings.msftLogin.errorTitle'), Lang.queryJS('settings.msftLogin.errorMessage'), Lang.queryJS('settings.msftLogin.okButton') ) setOverlayHandler(() => { toggleOverlay(false) }) toggleOverlay(true) }) } else if(arguments_[0] === MSFT_REPLY_TYPE.SUCCESS) { const queryMap = arguments_[1] const viewOnClose = arguments_[2] // Error from request to Microsoft. if (Object.prototype.hasOwnProperty.call(queryMap, 'error')) { switchView(getCurrentView(), viewOnClose, 500, 500, () => { // TODO Dont know what these errors are. Just show them I guess. // This is probably if you messed up the app registration with Azure. let error = queryMap.error // Error might be 'access_denied' ? let errorDesc = queryMap.error_description console.log('Error getting authCode, is Azure application registered correctly?') console.log(error) console.log(errorDesc) console.log('Full query map: ', queryMap) setOverlayContent( error, errorDesc, Lang.queryJS('settings.msftLogin.okButton') ) setOverlayHandler(() => { toggleOverlay(false) }) toggleOverlay(true) }) } else { msftLoginLogger.info('Acquired authCode, proceeding with authentication.') const authCode = queryMap.code AuthManager.addMicrosoftAccount(authCode).then(value => { updateSelectedAccount(value) switchView(getCurrentView(), viewOnClose, 500, 500, async () => { await prepareSettings() }) }) .catch((displayableError) => { let actualDisplayableError if(isDisplayableError(displayableError)) { msftLoginLogger.error('Error while logging in.', displayableError) actualDisplayableError = displayableError } else { // Uh oh. msftLoginLogger.error('Unhandled error during login.', displayableError) actualDisplayableError = Lang.queryJS('login.error.unknown') } switchView(getCurrentView(), viewOnClose, 500, 500, () => { setOverlayContent(actualDisplayableError.title, actualDisplayableError.desc, Lang.queryJS('login.tryAgain')) setOverlayHandler(() => { toggleOverlay(false) }) toggleOverlay(true) }) }) } } }) /** * Bind functionality for the account selection buttons. If another account * is selected, the UI of the previously selected account will be updated. */ function bindAuthAccountSelect(){ Array.from(document.getElementsByClassName('settingsAuthAccountSelect')).map((val) => { val.onclick = (e) => { if(val.hasAttribute('selected')){ return } const selectBtns = document.getElementsByClassName('settingsAuthAccountSelect') for(let i=0; i { val.onclick = (e) => { let isLastAccount = false if(Object.keys(ConfigManager.getAuthAccounts()).length === 1){ isLastAccount = true setOverlayContent( Lang.queryJS('settings.authAccountLogout.lastAccountWarningTitle'), Lang.queryJS('settings.authAccountLogout.lastAccountWarningMessage'), Lang.queryJS('settings.authAccountLogout.confirmButton'), Lang.queryJS('settings.authAccountLogout.cancelButton') ) setOverlayHandler(() => { processLogOut(val, isLastAccount) toggleOverlay(false) }) setDismissHandler(() => { toggleOverlay(false) }) toggleOverlay(true, true) } else { processLogOut(val, isLastAccount) } } }) } let msAccDomElementCache /** * Process a log out. * * @param {Element} val The log out button element. * @param {boolean} isLastAccount If this logout is on the last added account. */ function processLogOut(val, isLastAccount){ const parent = val.closest('.settingsAuthAccount') const uuid = parent.getAttribute('uuid') const prevSelAcc = ConfigManager.getSelectedAccount() const targetAcc = ConfigManager.getAuthAccount(uuid) if(targetAcc.type === 'microsoft') { msAccDomElementCache = parent switchView(getCurrentView(), VIEWS.waiting, 500, 500, () => { ipcRenderer.send(MSFT_OPCODE.OPEN_LOGOUT, uuid, isLastAccount) }) } else { AuthManager.removeMojangAccount(uuid).then(() => { if(!isLastAccount && uuid === prevSelAcc.uuid){ const selAcc = ConfigManager.getSelectedAccount() refreshAuthAccountSelected(selAcc.uuid) updateSelectedAccount(selAcc) validateSelectedAccount() } if(isLastAccount) { loginOptionsCancelEnabled(false) loginOptionsViewOnLoginSuccess = VIEWS.settings loginOptionsViewOnLoginCancel = VIEWS.loginOptions switchView(getCurrentView(), VIEWS.loginOptions) } }) $(parent).fadeOut(250, () => { parent.remove() }) } } // Bind reply for Microsoft Logout. ipcRenderer.on(MSFT_OPCODE.REPLY_LOGOUT, (_, ...arguments_) => { if (arguments_[0] === MSFT_REPLY_TYPE.ERROR) { switchView(getCurrentView(), VIEWS.settings, 500, 500, () => { if(arguments_.length > 1 && arguments_[1] === MSFT_ERROR.NOT_FINISHED) { // User cancelled. msftLogoutLogger.info('Logout cancelled by user.') return } // Unexpected error. setOverlayContent( Lang.queryJS('settings.msftLogout.errorTitle'), Lang.queryJS('settings.msftLogout.errorMessage'), Lang.queryJS('settings.msftLogout.okButton') ) setOverlayHandler(() => { toggleOverlay(false) }) toggleOverlay(true) }) } else if(arguments_[0] === MSFT_REPLY_TYPE.SUCCESS) { const uuid = arguments_[1] const isLastAccount = arguments_[2] const prevSelAcc = ConfigManager.getSelectedAccount() msftLogoutLogger.info('Logout Successful. uuid:', uuid) AuthManager.removeMicrosoftAccount(uuid) .then(() => { if(!isLastAccount && uuid === prevSelAcc.uuid){ const selAcc = ConfigManager.getSelectedAccount() refreshAuthAccountSelected(selAcc.uuid) updateSelectedAccount(selAcc) validateSelectedAccount() } if(isLastAccount) { loginOptionsCancelEnabled(false) loginOptionsViewOnLoginSuccess = VIEWS.settings loginOptionsViewOnLoginCancel = VIEWS.loginOptions switchView(getCurrentView(), VIEWS.loginOptions) } if(msAccDomElementCache) { msAccDomElementCache.remove() msAccDomElementCache = null } }) .finally(() => { if(!isLastAccount) { switchView(getCurrentView(), VIEWS.settings, 500, 500) } }) } }) /** * Refreshes the status of the selected account on the auth account * elements. * * @param {string} uuid The UUID of the new selected account. */ function refreshAuthAccountSelected(uuid){ Array.from(document.getElementsByClassName('settingsAuthAccount')).map((val) => { const selBtn = val.getElementsByClassName('settingsAuthAccountSelect')[0] if(uuid === val.getAttribute('uuid')){ selBtn.setAttribute('selected', '') selBtn.innerHTML = Lang.queryJS('settings.authAccountSelect.selectedButton') } else { if(selBtn.hasAttribute('selected')){ selBtn.removeAttribute('selected') } selBtn.innerHTML = Lang.queryJS('settings.authAccountSelect.selectButton') } }) } const settingsCurrentMicrosoftAccounts = document.getElementById('settingsCurrentMicrosoftAccounts') const settingsCurrentMojangAccounts = document.getElementById('settingsCurrentMojangAccounts') /** * Add auth account elements for each one stored in the authentication database. */ function populateAuthAccounts(){ const authAccounts = ConfigManager.getAuthAccounts() const authKeys = Object.keys(authAccounts) if(authKeys.length === 0){ return } const selectedUUID = ConfigManager.getSelectedAccount().uuid let microsoftAuthAccountStr = '' let mojangAuthAccountStr = '' authKeys.forEach((val) => { const acc = authAccounts[val] const accHtml = `
${acc.displayName}
${Lang.queryJS('settings.authAccountPopulate.username')}
${acc.displayName}
${Lang.queryJS('settings.authAccountPopulate.uuid')}
${acc.uuid}
` if(acc.type === 'microsoft') { microsoftAuthAccountStr += accHtml } else { mojangAuthAccountStr += accHtml } }) settingsCurrentMicrosoftAccounts.innerHTML = microsoftAuthAccountStr settingsCurrentMojangAccounts.innerHTML = mojangAuthAccountStr } /** * Prepare the accounts tab for display. */ function prepareAccountsTab() { populateAuthAccounts() bindAuthAccountSelect() bindAuthAccountLogOut() } /** * Minecraft Tab */ /** * Disable decimals, negative signs, and scientific notation. */ document.getElementById('settingsGameWidth').addEventListener('keydown', (e) => { if(/^[-.eE]$/.test(e.key)){ e.preventDefault() } }) document.getElementById('settingsGameHeight').addEventListener('keydown', (e) => { if(/^[-.eE]$/.test(e.key)){ e.preventDefault() } }) /** * Mods Tab */ const settingsModsContainer = document.getElementById('settingsModsContainer') /** * Manages the display and interaction state of the Mods tab and its buttons based on the type of `athShield`. * * The function performs the following: * - If `athShield.type` is 'hidden': hides the Mods button entirely. * - If `athShield.type` is 'blocked': shows the Mods button, displays the Mods tab, and disables all buttons within the Mods tab. * - Otherwise: shows and enables all components (Mods button and buttons within the Mods tab) normally. * * @return {void} */ function manageModCategory() { const modsButton = document.querySelector('button[rSc="settingsTabMods"]') const dropInMods = document.getElementById('settingsDropinModsContainer') if (athShield.type === 'hidden') { // Hide the Mods navigation button modsButton.style.display = 'none' } else if (athShield.type === 'blocked') { // Hide the drop-in mods elements dropInMods.style.display = 'none' } } /** * Resolve and update the mods on the UI. */ async function resolveModsForUI(){ const serv = ConfigManager.getSelectedServer() const distro = await DistroAPI.getDistribution() const servConf = ConfigManager.getModConfiguration(serv) const modStr = parseModulesForUI(distro.getServerById(serv).modules, false, servConf.mods) document.getElementById('settingsReqModsContent').innerHTML = modStr.reqMods document.getElementById('settingsOptModsContent').innerHTML = modStr.optMods } /** * Recursively build the mod UI elements. * * @param {Object[]} mdls An array of modules to parse. * @param {boolean} submodules Whether or not we are parsing submodules. * @param {Object} servConf The server configuration object for this module level. */ function parseModulesForUI(mdls, submodules, servConf){ let reqMods = '' let optMods = '' for(const mdl of mdls){ if(mdl.rawModule.type === Type.ForgeMod || mdl.rawModule.type === Type.LiteMod || mdl.rawModule.type === Type.LiteLoader || mdl.rawModule.type === Type.FabricMod){ if(mdl.getRequired().value){ reqMods += `
${mdl.rawModule.name} v${mdl.mavenComponents.version}
${mdl.subModules.length > 0 ? `
${Object.values(parseModulesForUI(mdl.subModules, true, servConf[mdl.getVersionlessMavenIdentifier()])).join('')}
` : ''}
` } else { const conf = servConf[mdl.getVersionlessMavenIdentifier()] const val = typeof conf === 'object' ? conf.value : conf optMods += `
${mdl.rawModule.name} v${mdl.mavenComponents.version}
${mdl.subModules.length > 0 ? `
${Object.values(parseModulesForUI(mdl.subModules, true, conf.mods)).join('')}
` : ''}
` } } } return { reqMods, optMods } } /** * Bind functionality to mod config toggle switches. Switching the value * will also switch the status color on the left of the mod UI. */ function bindModsToggleSwitch(){ const sEls = settingsModsContainer.querySelectorAll('[formod]') Array.from(sEls).map((v, index, arr) => { v.onchange = () => { if(v.checked) { document.getElementById(v.getAttribute('formod')).setAttribute('enabled', '') } else { document.getElementById(v.getAttribute('formod')).removeAttribute('enabled') } } }) } /** * Save the mod configuration based on the UI values. */ function saveModConfiguration(){ const serv = ConfigManager.getSelectedServer() const modConf = ConfigManager.getModConfiguration(serv) modConf.mods = _saveModConfiguration(modConf.mods) ConfigManager.setModConfiguration(serv, modConf) } /** * Recursively save mod config with submods. * * @param {Object} modConf Mod config object to save. */ function _saveModConfiguration(modConf){ for(let m of Object.entries(modConf)){ const tSwitch = settingsModsContainer.querySelectorAll(`[formod='${m[0]}']`) if(!tSwitch[0].hasAttribute('dropin')){ if(typeof m[1] === 'boolean'){ modConf[m[0]] = tSwitch[0].checked } else { if(m[1] != null){ if(tSwitch.length > 0){ modConf[m[0]].value = tSwitch[0].checked } modConf[m[0]].mods = _saveModConfiguration(modConf[m[0]].mods) } } } } return modConf } // Drop-in mod elements. let CACHE_SETTINGS_MODS_DIR let CACHE_DROPIN_MODS /** * Resolve any located drop-in mods for this server and * populate the results onto the UI. */ async function resolveDropinModsForUI(){ const serv = (await DistroAPI.getDistribution()).getServerById(ConfigManager.getSelectedServer()) CACHE_SETTINGS_MODS_DIR = path.join(ConfigManager.getInstanceDirectory(), serv.rawServer.id, 'mods') CACHE_DROPIN_MODS = DropinModUtil.scanForDropinMods(CACHE_SETTINGS_MODS_DIR, serv.rawServer.minecraftVersion) let dropinMods = '' for(dropin of CACHE_DROPIN_MODS){ dropinMods += `
${dropin.name}
` } document.getElementById('settingsDropinModsContent').innerHTML = dropinMods } /** * Bind the remove button for each loaded drop-in mod. */ function bindDropinModsRemoveButton(){ const sEls = settingsModsContainer.querySelectorAll('[remmod]') Array.from(sEls).map((v, index, arr) => { v.onclick = async () => { const fullName = v.getAttribute('remmod') const res = await DropinModUtil.deleteDropinMod(CACHE_SETTINGS_MODS_DIR, fullName) if(res){ document.getElementById(fullName).remove() } else { setOverlayContent( Lang.queryJS('settings.dropinMods.deleteFailedTitle', { fullName }), Lang.queryJS('settings.dropinMods.deleteFailedMessage'), Lang.queryJS('settings.dropinMods.okButton') ) setOverlayHandler(null) toggleOverlay(true) } } }) } /** * Bind functionality to the file system button for the selected * server configuration. */ function bindDropinModFileSystemButton(){ const fsBtn = document.getElementById('settingsDropinFileSystemButton') fsBtn.onclick = () => { DropinModUtil.validateDir(CACHE_SETTINGS_MODS_DIR) shell.openPath(CACHE_SETTINGS_MODS_DIR) } fsBtn.ondragenter = e => { e.dataTransfer.dropEffect = 'move' fsBtn.setAttribute('drag', '') e.preventDefault() } fsBtn.ondragover = e => { e.preventDefault() } fsBtn.ondragleave = e => { fsBtn.removeAttribute('drag') } fsBtn.ondrop = async e => { fsBtn.removeAttribute('drag') e.preventDefault() DropinModUtil.addDropinMods(e.dataTransfer.files, CACHE_SETTINGS_MODS_DIR) await reloadDropinMods() } } /** * Save drop-in mod states. Enabling and disabling is just a matter * of adding/removing the .disabled extension. */ function saveDropinModConfiguration(){ for(dropin of CACHE_DROPIN_MODS){ const dropinUI = document.getElementById(dropin.fullName) if(dropinUI != null){ const dropinUIEnabled = dropinUI.hasAttribute('enabled') if(DropinModUtil.isDropinModEnabled(dropin.fullName) != dropinUIEnabled){ DropinModUtil.toggleDropinMod(CACHE_SETTINGS_MODS_DIR, dropin.fullName, dropinUIEnabled).catch(err => { if(!isOverlayVisible()){ setOverlayContent( Lang.queryJS('settings.dropinMods.failedToggleTitle'), err.message, Lang.queryJS('settings.dropinMods.okButton') ) setOverlayHandler(null) toggleOverlay(true) } }) } } } } // Refresh the drop-in mods when F5 is pressed. // Only active on the mods tab. document.addEventListener('keydown', async (e) => { if(getCurrentView() === VIEWS.settings && selectedSettingsTab === 'settingsTabMods'){ if(e.key === 'F5'){ await reloadDropinMods() saveShaderpackSettings() await resolveShaderpacksForUI() } } }) async function reloadDropinMods(){ await resolveDropinModsForUI() bindDropinModsRemoveButton() bindDropinModFileSystemButton() bindModsToggleSwitch() } // Shaderpack let CACHE_SETTINGS_INSTANCE_DIR let CACHE_SHADERPACKS let CACHE_SELECTED_SHADERPACK /** * Load shaderpack information. */ async function resolveShaderpacksForUI(){ const serv = (await DistroAPI.getDistribution()).getServerById(ConfigManager.getSelectedServer()) CACHE_SETTINGS_INSTANCE_DIR = path.join(ConfigManager.getInstanceDirectory(), serv.rawServer.id) CACHE_SHADERPACKS = DropinModUtil.scanForShaderpacks(CACHE_SETTINGS_INSTANCE_DIR) CACHE_SELECTED_SHADERPACK = DropinModUtil.getEnabledShaderpack(CACHE_SETTINGS_INSTANCE_DIR) setShadersOptions(CACHE_SHADERPACKS, CACHE_SELECTED_SHADERPACK) } function setShadersOptions(arr, selected){ const cont = document.getElementById('settingsShadersOptions') cont.innerHTML = '' for(let opt of arr) { const d = document.createElement('DIV') d.innerHTML = opt.name d.setAttribute('value', opt.fullName) if(opt.fullName === selected) { d.setAttribute('selected', '') document.getElementById('settingsShadersSelected').innerHTML = opt.name } d.addEventListener('click', function(e) { this.parentNode.previousElementSibling.innerHTML = this.innerHTML for(let sib of this.parentNode.children){ sib.removeAttribute('selected') } this.setAttribute('selected', '') closeSettingsSelect() }) cont.appendChild(d) } } function saveShaderpackSettings(){ let sel = 'OFF' for(let opt of document.getElementById('settingsShadersOptions').childNodes){ if(opt.hasAttribute('selected')){ sel = opt.getAttribute('value') } } DropinModUtil.setEnabledShaderpack(CACHE_SETTINGS_INSTANCE_DIR, sel) } function bindShaderpackButton() { const spBtn = document.getElementById('settingsShaderpackButton') spBtn.onclick = () => { const p = path.join(CACHE_SETTINGS_INSTANCE_DIR, 'shaderpacks') DropinModUtil.validateDir(p) shell.openPath(p) } spBtn.ondragenter = e => { e.dataTransfer.dropEffect = 'move' spBtn.setAttribute('drag', '') e.preventDefault() } spBtn.ondragover = e => { e.preventDefault() } spBtn.ondragleave = e => { spBtn.removeAttribute('drag') } spBtn.ondrop = async e => { spBtn.removeAttribute('drag') e.preventDefault() DropinModUtil.addShaderpacks(e.dataTransfer.files, CACHE_SETTINGS_INSTANCE_DIR) saveShaderpackSettings() await resolveShaderpacksForUI() } } // Server status bar functions. /** * Load the currently selected server information onto the mods tab. */ async function loadSelectedServerOnModsTab(){ const serv = (await DistroAPI.getDistribution()).getServerById(ConfigManager.getSelectedServer()) for(const el of document.getElementsByClassName('settingsSelServContent')) { el.innerHTML = `
${serv.rawServer.name} ${serv.rawServer.description}
${serv.rawServer.minecraftVersion}
${serv.rawServer.version}
${serv.rawServer.mainServer ? `
${Lang.queryJS('settings.serverListing.mainServer')}
` : ''}
` } } // Bind functionality to the server switch button. Array.from(document.getElementsByClassName('settingsSwitchServerButton')).forEach(el => { el.addEventListener('click', async e => { e.target.blur() await toggleServerSelection(true) }) }) /** * Save mod configuration for the current selected server. */ function saveAllModConfigurations(){ saveModConfiguration() ConfigManager.save() saveDropinModConfiguration() } /** * Function to refresh the current tab whenever the selected * server is changed. */ function animateSettingsTabRefresh(){ $(`#${selectedSettingsTab}`).fadeOut(500, async () => { await prepareSettings() $(`#${selectedSettingsTab}`).fadeIn(500) }) } /** * Prepare the Mods tab for display. */ async function prepareModsTab(first){ await resolveModsForUI() await resolveDropinModsForUI() await resolveShaderpacksForUI() manageModCategory() bindDropinModsRemoveButton() bindDropinModFileSystemButton() bindShaderpackButton() bindModsToggleSwitch() await loadSelectedServerOnModsTab() } /** * Java Tab */ // DOM Cache const settingsMaxRAMRange = document.getElementById('settingsMaxRAMRange') const settingsMinRAMRange = document.getElementById('settingsMinRAMRange') const settingsMaxRAMLabel = document.getElementById('settingsMaxRAMLabel') const settingsMinRAMLabel = document.getElementById('settingsMinRAMLabel') const settingsMemoryTotal = document.getElementById('settingsMemoryTotal') const settingsMemoryAvail = document.getElementById('settingsMemoryAvail') const settingsJavaExecDetails = document.getElementById('settingsJavaExecDetails') const settingsJavaReqDesc = document.getElementById('settingsJavaReqDesc') const settingsJvmOptsLink = document.getElementById('settingsJvmOptsLink') // Bind on change event for min memory container. settingsMinRAMRange.onchange = (e) => { // Current range values const sMaxV = Number(settingsMaxRAMRange.getAttribute('value')) const sMinV = Number(settingsMinRAMRange.getAttribute('value')) // Get reference to range bar. const bar = e.target.getElementsByClassName('rangeSliderBar')[0] // Calculate effective total memory. const max = os.totalmem()/1073741824 // Change range bar color based on the selected value. if(sMinV >= max/2){ bar.style.background = '#e86060' } else if(sMinV >= max/4) { bar.style.background = '#e8e18b' } else { bar.style.background = null } // Increase maximum memory if the minimum exceeds its value. if(sMaxV < sMinV){ const sliderMeta = calculateRangeSliderMeta(settingsMaxRAMRange) updateRangedSlider(settingsMaxRAMRange, sMinV, ((sMinV-sliderMeta.min)/sliderMeta.step)*sliderMeta.inc) settingsMaxRAMLabel.innerHTML = sMinV.toFixed(1) + 'G' } // Update label settingsMinRAMLabel.innerHTML = sMinV.toFixed(1) + 'G' } // Bind on change event for max memory container. settingsMaxRAMRange.onchange = (e) => { // Current range values const sMaxV = Number(settingsMaxRAMRange.getAttribute('value')) const sMinV = Number(settingsMinRAMRange.getAttribute('value')) // Get reference to range bar. const bar = e.target.getElementsByClassName('rangeSliderBar')[0] // Calculate effective total memory. const max = os.totalmem()/1073741824 // Change range bar color based on the selected value. if(sMaxV >= max/2){ bar.style.background = '#e86060' } else if(sMaxV >= max/4) { bar.style.background = '#e8e18b' } else { bar.style.background = null } // Decrease the minimum memory if the maximum value is less. if(sMaxV < sMinV){ const sliderMeta = calculateRangeSliderMeta(settingsMaxRAMRange) updateRangedSlider(settingsMinRAMRange, sMaxV, ((sMaxV-sliderMeta.min)/sliderMeta.step)*sliderMeta.inc) settingsMinRAMLabel.innerHTML = sMaxV.toFixed(1) + 'G' } settingsMaxRAMLabel.innerHTML = sMaxV.toFixed(1) + 'G' } /** * Calculate common values for a ranged slider. * * @param {Element} v The range slider to calculate against. * @returns {Object} An object with meta values for the provided ranged slider. */ function calculateRangeSliderMeta(v){ const val = { max: Number(v.getAttribute('max')), min: Number(v.getAttribute('min')), step: Number(v.getAttribute('step')), } val.ticks = (val.max-val.min)/val.step val.inc = 100/val.ticks return val } /** * Binds functionality to the ranged sliders. They're more than * just divs now :'). */ function bindRangeSlider(){ Array.from(document.getElementsByClassName('rangeSlider')).map((v) => { // Reference the track (thumb). const track = v.getElementsByClassName('rangeSliderTrack')[0] // Set the initial slider value. const value = v.getAttribute('value') const sliderMeta = calculateRangeSliderMeta(v) updateRangedSlider(v, value, ((value-sliderMeta.min)/sliderMeta.step)*sliderMeta.inc) // The magic happens when we click on the track. track.onmousedown = (e) => { // Stop moving the track on mouse up. document.onmouseup = (e) => { document.onmousemove = null document.onmouseup = null } // Move slider according to the mouse position. document.onmousemove = (e) => { // Distance from the beginning of the bar in pixels. const diff = e.pageX - v.offsetLeft - track.offsetWidth/2 // Don't move the track off the bar. if(diff >= 0 && diff <= v.offsetWidth-track.offsetWidth/2){ // Convert the difference to a percentage. const perc = (diff/v.offsetWidth)*100 // Calculate the percentage of the closest notch. const notch = Number(perc/sliderMeta.inc).toFixed(0)*sliderMeta.inc // If we're close to that notch, stick to it. if(Math.abs(perc-notch) < sliderMeta.inc/2){ updateRangedSlider(v, sliderMeta.min+(sliderMeta.step*(notch/sliderMeta.inc)), notch) } } } } }) } /** * Update a ranged slider's value and position. * * @param {Element} element The ranged slider to update. * @param {string | number} value The new value for the ranged slider. * @param {number} notch The notch that the slider should now be at. */ function updateRangedSlider(element, value, notch){ const oldVal = element.getAttribute('value') const bar = element.getElementsByClassName('rangeSliderBar')[0] const track = element.getElementsByClassName('rangeSliderTrack')[0] element.setAttribute('value', value) if(notch < 0){ notch = 0 } else if(notch > 100) { notch = 100 } const event = new MouseEvent('change', { target: element, type: 'change', bubbles: false, cancelable: true }) let cancelled = !element.dispatchEvent(event) if(!cancelled){ track.style.left = notch + '%' bar.style.width = notch + '%' } else { element.setAttribute('value', oldVal) } } /** * Display the total and available RAM. */ function populateMemoryStatus(){ settingsMemoryTotal.innerHTML = Number((os.totalmem()-1073741824)/1073741824).toFixed(1) + 'G' settingsMemoryAvail.innerHTML = Number(os.freemem()/1073741824).toFixed(1) + 'G' } /** * Validate the provided executable path and display the data on * the UI. * * @param {string} execPath The executable path to populate against. */ async function populateJavaExecDetails(execPath){ const server = (await DistroAPI.getDistribution()).getServerById(ConfigManager.getSelectedServer()) const details = await validateSelectedJvm(ensureJavaDirIsRoot(execPath), server.effectiveJavaOptions.supported) if(details != null) { settingsJavaExecDetails.innerHTML = Lang.queryJS('settings.java.selectedJava', { version: details.semverStr, vendor: details.vendor }) } else { settingsJavaExecDetails.innerHTML = Lang.queryJS('settings.java.invalidSelection') } } function populateJavaReqDesc(server) { settingsJavaReqDesc.innerHTML = Lang.queryJS('settings.java.requiresJava', { major: server.effectiveJavaOptions.suggestedMajor }) } function populateJvmOptsLink(server) { const major = server.effectiveJavaOptions.suggestedMajor settingsJvmOptsLink.innerHTML = Lang.queryJS('settings.java.availableOptions', { major: major }) if(major >= 12) { settingsJvmOptsLink.href = `https://docs.oracle.com/en/java/javase/${major}/docs/specs/man/java.html#extra-options-for-java` } else if(major >= 11) { settingsJvmOptsLink.href = 'https://docs.oracle.com/en/java/javase/11/tools/java.html#GUID-3B1CE181-CD30-4178-9602-230B800D4FAE' } else if(major >= 9) { settingsJvmOptsLink.href = `https://docs.oracle.com/javase/${major}/tools/java.htm` } else { settingsJvmOptsLink.href = `https://docs.oracle.com/javase/${major}/docs/technotes/tools/${process.platform === 'win32' ? 'windows' : 'unix'}/java.html` } } function bindMinMaxRam(server) { // Store maximum memory values. const SETTINGS_MAX_MEMORY = ConfigManager.getAbsoluteMaxRAM(server.rawServer.javaOptions?.ram) const SETTINGS_MIN_MEMORY = ConfigManager.getAbsoluteMinRAM(server.rawServer.javaOptions?.ram) // Set the max and min values for the ranged sliders. settingsMaxRAMRange.setAttribute('max', SETTINGS_MAX_MEMORY) settingsMaxRAMRange.setAttribute('min', SETTINGS_MIN_MEMORY) settingsMinRAMRange.setAttribute('max', SETTINGS_MAX_MEMORY) settingsMinRAMRange.setAttribute('min', SETTINGS_MIN_MEMORY) } /** * Prepare the Java tab for display. */ async function prepareJavaTab(){ const server = (await DistroAPI.getDistribution()).getServerById(ConfigManager.getSelectedServer()) bindMinMaxRam(server) bindRangeSlider(server) populateMemoryStatus() populateJavaReqDesc(server) populateJvmOptsLink(server) } /** * About Tab */ const settingsTabAbout = document.getElementById('settingsTabAbout') const settingsAboutChangelogTitle = settingsTabAbout.getElementsByClassName('settingsChangelogTitle')[0] const settingsAboutChangelogText = settingsTabAbout.getElementsByClassName('settingsChangelogText')[0] const settingsAboutChangelogButton = settingsTabAbout.getElementsByClassName('settingsChangelogButton')[0] // Bind the devtools toggle button. document.getElementById('settingsAboutDevToolsButton').onclick = (e) => { let window = remote.getCurrentWindow() window.toggleDevTools() } /** * Return whether or not the provided version is a prerelease. * * @param {string} version The semver version to test. * @returns {boolean} True if the version is a prerelease, otherwise false. */ function isPrerelease(version){ const preRelComp = semver.prerelease(version) return preRelComp != null && preRelComp.length > 0 } /** * Utility method to display version information on the * About and Update settings tabs. * * @param {string} version The semver version to display. * @param {Element} valueElement The value element. * @param {Element} titleElement The title element. * @param {Element} checkElement The check mark element. */ function populateVersionInformation(version, valueElement, titleElement, checkElement){ valueElement.innerHTML = version if(isPrerelease(version)){ titleElement.innerHTML = Lang.queryJS('settings.about.preReleaseTitle') titleElement.style.color = '#ff886d' checkElement.style.background = '#ff886d' } else { titleElement.innerHTML = Lang.queryJS('settings.about.stableReleaseTitle') titleElement.style.color = null checkElement.style.background = null } } /** * Retrieve the version information and display it on the UI. */ function populateAboutVersionInformation(){ populateVersionInformation(remote.app.getVersion(), document.getElementById('settingsAboutCurrentVersionValue'), document.getElementById('settingsAboutCurrentVersionTitle'), document.getElementById('settingsAboutCurrentVersionCheck')) } /** * Fetches the GitHub atom release feed and parses it for the release notes * of the current version. This value is displayed on the UI. */ function populateReleaseNotes(){ $.ajax({ url: 'https://github.com/dscalzi/HeliosLauncher/releases.atom', success: (data) => { const version = 'v' + remote.app.getVersion() const entries = $(data).find('entry') for(let i=0; i { settingsAboutChangelogText.innerHTML = Lang.queryJS('settings.about.releaseNotesFailed') }) } /** * Prepare account tab for display. */ function prepareAboutTab(){ populateAboutVersionInformation() populateReleaseNotes() } /** * Update Tab */ const settingsTabUpdate = document.getElementById('settingsTabUpdate') const settingsUpdateTitle = document.getElementById('settingsUpdateTitle') const settingsUpdateVersionCheck = document.getElementById('settingsUpdateVersionCheck') const settingsUpdateVersionTitle = document.getElementById('settingsUpdateVersionTitle') const settingsUpdateVersionValue = document.getElementById('settingsUpdateVersionValue') const settingsUpdateChangelogTitle = settingsTabUpdate.getElementsByClassName('settingsChangelogTitle')[0] const settingsUpdateChangelogText = settingsTabUpdate.getElementsByClassName('settingsChangelogText')[0] const settingsUpdateChangelogCont = settingsTabUpdate.getElementsByClassName('settingsChangelogContainer')[0] const settingsUpdateActionButton = document.getElementById('settingsUpdateActionButton') /** * Update the properties of the update action button. * * @param {string} text The new button text. * @param {boolean} disabled Optional. Disable or enable the button * @param {function} handler Optional. New button event handler. */ function settingsUpdateButtonStatus(text, disabled = false, handler = null){ settingsUpdateActionButton.innerHTML = text settingsUpdateActionButton.disabled = disabled if(handler != null){ settingsUpdateActionButton.onclick = handler } } /** * Populate the update tab with relevant information. * * @param {Object} data The update data. */ function populateSettingsUpdateInformation(data){ if(data != null){ settingsUpdateTitle.innerHTML = isPrerelease(data.version) ? Lang.queryJS('settings.updates.newPreReleaseTitle') : Lang.queryJS('settings.updates.newReleaseTitle') settingsUpdateChangelogCont.style.display = null settingsUpdateChangelogTitle.innerHTML = data.releaseName settingsUpdateChangelogText.innerHTML = data.releaseNotes populateVersionInformation(data.version, settingsUpdateVersionValue, settingsUpdateVersionTitle, settingsUpdateVersionCheck) if(process.platform === 'darwin'){ settingsUpdateButtonStatus(Lang.queryJS('settings.updates.downloadButton'), false, () => { shell.openExternal(data.darwindownload) }) } else { settingsUpdateButtonStatus(Lang.queryJS('settings.updates.downloadingButton'), true) } } else { settingsUpdateTitle.innerHTML = Lang.queryJS('settings.updates.latestVersionTitle') settingsUpdateChangelogCont.style.display = 'none' populateVersionInformation(remote.app.getVersion(), settingsUpdateVersionValue, settingsUpdateVersionTitle, settingsUpdateVersionCheck) settingsUpdateButtonStatus(Lang.queryJS('settings.updates.checkForUpdatesButton'), false, () => { if(!isDev){ ipcRenderer.send('autoUpdateAction', 'checkForUpdate') settingsUpdateButtonStatus(Lang.queryJS('settings.updates.checkingForUpdatesButton'), true) } }) } } /** * Prepare update tab for display. * * @param {Object} data The update data. */ function prepareUpdateTab(data = null){ populateSettingsUpdateInformation(data) } /** * Settings preparation functions. */ /** * Prepare the entire settings UI. * * @param {boolean} first Whether or not it is the first load. */ async function prepareSettings(first = false) { if(first){ setupSettingsTabs() initSettingsValidators() prepareUpdateTab() } else { await prepareModsTab() } await initSettingsValues() prepareAccountsTab() await prepareJavaTab() prepareAboutTab() } // Prepare the settings UI on startup. //prepareSettings(true)