// Requirements const os = require('os') const semver = require('semver') const DropinModUtil = require('./assets/js/dropinmodutil') 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: 'Executables', extensions: ['exe'] }, { name: 'All Files', 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*1000 + '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( 'Something Went Wrong', 'Microsoft authentication failed. Please try again.', 'OK' ) 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, 'OK' ) 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 = { title: 'Unknown Error During Login', desc: 'An unknown error has occurred. Please see the console for details.' } } 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( 'Warning
This is Your Last Account', 'In order to use the launcher you must be logged into at least one account. You will need to login again after.

Are you sure you want to log out?', 'I\'m Sure', 'Cancel' ) 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( 'Something Went Wrong', 'Microsoft logout failed. Please try again.', 'OK' ) 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 = 'Selected Account ✔' } else { if(selBtn.hasAttribute('selected')){ selBtn.removeAttribute('selected') } selBtn.innerHTML = 'Select Account' } }) } 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}
Username
${acc.displayName}
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') /** * 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){ 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( `Failed to Delete
Drop-in Mod ${fullName}`, 'Make sure the file is not in use and try again.', 'Okay' ) 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( 'Failed to Toggle
One or More Drop-in Mods', err.message, 'Okay' ) 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 ? `
Main Server
` : ''}
` } } // 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() 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 = `Selected: Java ${details.semverStr} (${details.vendor})` } else { settingsJavaExecDetails.innerHTML = 'Invalid Selection' } } function populateJavaReqDesc(server) { settingsJavaReqDesc.innerHTML = `Requires Java ${server.effectiveJavaOptions.suggestedMajor} x64.` } // TODO Update to use semver range function populateJvmOptsLink(server) { const major = server.effectiveJavaOptions.suggestedMajor settingsJvmOptsLink.innerHTML = `Available Options for Java ${major} (HotSpot VM)` 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 = 'Pre-release' titleElement.style.color = '#ff886d' checkElement.style.background = '#ff886d' } else { titleElement.innerHTML = 'Stable Release' 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 = 'Failed to load release notes.' }) } /** * 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 = `New ${isPrerelease(data.version) ? 'Pre-release' : 'Release'} Available` settingsUpdateChangelogCont.style.display = null settingsUpdateChangelogTitle.innerHTML = data.releaseName settingsUpdateChangelogText.innerHTML = data.releaseNotes populateVersionInformation(data.version, settingsUpdateVersionValue, settingsUpdateVersionTitle, settingsUpdateVersionCheck) if(process.platform === 'darwin'){ settingsUpdateButtonStatus('Download from GitHubClose the launcher and run the dmg to update.', false, () => { shell.openExternal(data.darwindownload) }) } else { settingsUpdateButtonStatus('Downloading..', true) } } else { settingsUpdateTitle.innerHTML = 'You Are Running the Latest Version' settingsUpdateChangelogCont.style.display = 'none' populateVersionInformation(remote.app.getVersion(), settingsUpdateVersionValue, settingsUpdateVersionTitle, settingsUpdateVersionCheck) settingsUpdateButtonStatus('Check for Updates', false, () => { if(!isDev){ ipcRenderer.send('autoUpdateAction', 'checkForUpdate') settingsUpdateButtonStatus('Checking for Updates..', 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)