// Requirements const os = require('os') const semver = require('semver') const { AssetGuard } = require('./assets/js/assetguard') const settingsState = { invalid: new Set() } /** * General Settings Functions */ /** * Bind value validators to the settings UI elements. These will * validate against the criteria defined in the ConfigManager (if * and). 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. */ function initSettingsValues(){ const sEls = document.getElementById('settingsContainer').querySelectorAll('[cValue]') Array.from(sEls).map((v, index, arr) => { const cVal = v.getAttribute('cValue') const gFn = ConfigManager['get' + cVal] if(typeof gFn === 'function'){ if(v.tagName === 'INPUT'){ if(v.type === 'number' || v.type === 'text'){ // Special Conditions if(cVal === 'JavaExecutable'){ populateJavaExecDetails(v.value) v.value = gFn() } else if(cVal === 'JVMOptions'){ v.value = gFn().join(' ') } else { v.value = gFn() } } else if(v.type === 'checkbox'){ v.checked = gFn() } } else if(v.tagName === 'DIV'){ if(v.classList.contains('rangeSlider')){ // Special Conditions if(cVal === 'MinRAM' || cVal === 'MaxRAM'){ let val = gFn() if(val.endsWith('M')){ val = Number(val.substring(0, val.length-1))/1000 } else { val = Number.parseFloat(val) } v.setAttribute('value', val) } else { v.setAttribute('value', Number.parseFloat(gFn())) } } } } }) } /** * 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 sFn = ConfigManager['set' + cVal] if(typeof sFn === 'function'){ if(v.tagName === 'INPUT'){ if(v.type === 'number' || v.type === 'text'){ // Special Conditions if(cVal === 'JVMOptions'){ sFn(v.value.split(' ')) } else { sFn(v.value) } } else if(v.type === 'checkbox'){ sFn(v.checked) // 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' } sFn(val) } else { sFn(v.getAttribute('value')) } } } } }) } 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 } /* Closes the settings view and saves all data. */ settingsNavDone.onclick = () => { saveSettingsValues() saveModConfiguration() ConfigManager.save() switchView(getCurrentView(), VIEWS.landing) } /** * Account Management Tab */ // Bind the add account button. document.getElementById('settingsAddAccount').onclick = (e) => { switchView(getCurrentView(), VIEWS.login, 500, 500, () => { loginViewOnCancel = VIEWS.settings loginViewOnSuccess = VIEWS.settings loginCancelEnabled(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) switchView(getCurrentView(), VIEWS.login) toggleOverlay(false) }) setDismissHandler(() => { toggleOverlay(false) }) toggleOverlay(true, true) } else { processLogOut(val, isLastAccount) } } }) } /** * 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() AuthManager.removeAccount(uuid).then(() => { if(!isLastAccount && uuid === prevSelAcc.uuid){ const selAcc = ConfigManager.getSelectedAccount() refreshAuthAccountSelected(selAcc.uuid) updateSelectedAccount(selAcc) validateSelectedAccount() } }) $(parent).fadeOut(250, () => { parent.remove() }) } /** * 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 settingsCurrentAccounts = document.getElementById('settingsCurrentAccounts') /** * Add auth account elements for each one stored in the authentication database. */ function populateAuthAccounts(){ const authAccounts = ConfigManager.getAuthAccounts() const authKeys = Object.keys(authAccounts) const selectedUUID = ConfigManager.getSelectedAccount().uuid let authAccountStr = '' authKeys.map((val) => { const acc = authAccounts[val] authAccountStr += `
${acc.displayName}
Username
${acc.displayName}
UUID
${acc.uuid}
` }) settingsCurrentAccounts.innerHTML = authAccountStr } /** * 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') function resolveModsForUI(){ const serv = ConfigManager.getSelectedServer() const distro = DistroManager.getDistribution() const servConf = ConfigManager.getModConfiguration(serv) const modStr = parseModulesForUI(distro.getServer(serv).getModules(), false, servConf.mods) document.getElementById('settingsReqModsContent').innerHTML = modStr.reqMods document.getElementById('settingsOptModsContent').innerHTML = modStr.optMods } function parseModulesForUI(mdls, submodules = false, servConf){ let reqMods = '' let optMods = '' for(const mdl of mdls){ if(mdl.getType() === DistroManager.Types.ForgeMod || mdl.getType() === DistroManager.Types.LiteMod || mdl.getType() === DistroManager.Types.LiteLoader){ if(mdl.getRequired().isRequired()){ reqMods += `
${mdl.getName()} v${mdl.getVersion()}
${mdl.hasSubModules() ? `
${Object.values(parseModulesForUI(mdl.getSubModules(), true)).join('')}
` : ''}
` } else { const conf = servConf[mdl.getVersionlessID()] const val = typeof conf === 'object' ? conf.value : conf optMods += `
${mdl.getName()} v${mdl.getVersion()}
${mdl.hasSubModules() ? `
${Object.values(parseModulesForUI(mdl.getSubModules(), 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(m of Object.entries(modConf)){ const val = settingsModsContainer.querySelectorAll(`[formod='${m[0]}']`)[0].checked if(typeof m[1] === 'boolean'){ modConf[m[0]] = val } else { if(m[1] != null){ modConf[m[0]].value = val modConf[m[0]].mods = _saveModConfiguration(modConf[m[0]].mods) } } } return modConf } /** * Prepare the Java tab for display. */ function prepareModsTab(){ resolveModsForUI() bindModsToggleSwitch() } /** * 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 settingsJavaExecVal = document.getElementById('settingsJavaExecVal') const settingsJavaExecSel = document.getElementById('settingsJavaExecSel') // Store maximum memory values. const SETTINGS_MAX_MEMORY = ConfigManager.getAbsoluteMaxRAM() const SETTINGS_MIN_MEMORY = ConfigManager.getAbsoluteMinRAM() // 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 ) // 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()-1000000000)/1000000000 // 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()-1000000000)/1000000000 // 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()-1000000000)/1000000000).toFixed(1) + 'G' settingsMemoryAvail.innerHTML = Number(os.freemem()/1000000000).toFixed(1) + 'G' } // Bind the executable file input to the display text input. settingsJavaExecSel.onchange = (e) => { settingsJavaExecVal.value = settingsJavaExecSel.files[0].path populateJavaExecDetails(settingsJavaExecVal.value) } /** * Validate the provided executable path and display the data on * the UI. * * @param {string} execPath The executable path to populate against. */ function populateJavaExecDetails(execPath){ AssetGuard._validateJavaBinary(execPath).then(v => { if(v.valid){ settingsJavaExecDetails.innerHTML = `Selected: Java ${v.version.major} Update ${v.version.update} (x${v.arch})` } else { settingsJavaExecDetails.innerHTML = 'Invalid Selection' } }) } /** * Prepare the Java tab for display. */ function prepareJavaTab(){ bindRangeSlider() populateMemoryStatus() } /** * About Tab */ const settingsAboutCurrentVersionCheck = document.getElementById('settingsAboutCurrentVersionCheck') const settingsAboutCurrentVersionTitle = document.getElementById('settingsAboutCurrentVersionTitle') const settingsAboutCurrentVersionValue = document.getElementById('settingsAboutCurrentVersionValue') const settingsChangelogTitle = document.getElementById('settingsChangelogTitle') const settingsChangelogText = document.getElementById('settingsChangelogText') const settingsChangelogButton = document.getElementById('settingsChangelogButton') // Bind the devtools toggle button. document.getElementById('settingsAboutDevToolsButton').onclick = (e) => { let window = remote.getCurrentWindow() window.toggleDevTools() } /** * Retrieve the version information and display it on the UI. */ function populateVersionInformation(){ const version = remote.app.getVersion() settingsAboutCurrentVersionValue.innerHTML = version const preRelComp = semver.prerelease(version) if(preRelComp != null && preRelComp.length > 0){ settingsAboutCurrentVersionTitle.innerHTML = 'Pre-release' settingsAboutCurrentVersionTitle.style.color = '#ff886d' settingsAboutCurrentVersionCheck.style.background = '#ff886d' } else { settingsAboutCurrentVersionTitle.innerHTML = 'Stable Release' settingsAboutCurrentVersionTitle.style.color = null settingsAboutCurrentVersionCheck.style.background = null } } /** * 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/WesterosCraftCode/ElectronLauncher/releases.atom', success: (data) => { const version = 'v' + remote.app.getVersion() const entries = $(data).find('entry') for(let i=0; i { settingsChangelogText.innerHTML = 'Failed to load release notes.' }) } /** * Prepare account tab for display. */ function prepareAboutTab(){ populateVersionInformation() populateReleaseNotes() } /** * Settings preparation functions. */ /** * Prepare the entire settings UI. * * @param {boolean} first Whether or not it is the first load. */ function prepareSettings(first = false) { if(first){ setupSettingsTabs() initSettingsValidators() } else { prepareModsTab() } initSettingsValues() prepareAccountsTab() prepareJavaTab() prepareAboutTab() } // Prepare the settings UI on startup. prepareSettings(true)