Launcher/app/assets/js/scripts/settings.js
2021-07-17 16:18:22 +01:00

1498 lines
52 KiB
JavaScript

// Requirements
const os = require('os')
const semver = require('semver')
const { JavaGuard } = require('./assets/js/assetguard')
const DropinModUtil = require('./assets/js/dropinmodutil')
const loggerSettings = LoggerUtil('%c[Settings]', 'color: #353232; font-weight: bold')
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 bindFolderOpeners(){
for(let ele of document.getElementsByClassName('settingsFolderOpenButton')){
ele.onclick = async e => {
const pathId = ele.getAttribute('pathId')
if(pathId){
if(pathId === 'DataDirectory'){
shell.openPath(ConfigManager.getDataDirectory())
}
}
}
}
}
bindFolderOpeners()
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) {
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
* 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 === 'DataDirectory') {
v.value = gFn()
} else if (cVal === 'ServerCode'){
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 < navItems.length; i++) {
if (navItems[i].hasAttribute('selected')) {
navItems[i].removeAttribute('selected')
}
}
ele.setAttribute('selected', '')
let prevTab = selectedSettingsTab
selectedSettingsTab = ele.getAttribute('rSc')
document.getElementById(prevTab).onscroll = null
document.getElementById(selectedSettingsTab).onscroll = settingsTabScrollListener
if(fade){
$(`#${prevTab}`).fadeOut(150, () => {
$(`#${selectedSettingsTab}`).fadeIn({
duration: 150,
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()
saveDropinModConfiguration()
saveShaderpackSettings()
switchView(getCurrentView(), VIEWS.landing)
if(hasRPC){
if(ConfigManager.getSelectedServer()){
const serv = DistroManager.getDistribution().getServer(ConfigManager.getSelectedServer())
DiscordWrapper.updateDetails('Ready to Play!')
DiscordWrapper.updateState('Server: ' + serv.getName())
} else {
DiscordWrapper.updateDetails('Landing Screen...')
}
}
}
/**
* Account Management Tab
*/
// Bind the add account button.
document.getElementById('settingsAddAccount').onclick = (e) => {
switchView(getCurrentView(), VIEWS.login, 250, 250, () => {
loginViewOnCancel = VIEWS.settings
loginViewOnSuccess = VIEWS.settings
loginCancelEnabled(true)
if(hasRPC){
DiscordWrapper.updateDetails('Adding an Account...')
DiscordWrapper.clearState()
}
})
}
/**
* Binds the functionality within the server codes section of the launcher settings
*/
function bindServerCodeButtons(){
// Sets up the onclick listeners for the button to add codes
document.getElementById('settingsAddServerCode').onclick = () => {
for(let ele of document.getElementsByClassName('settingsInputServerCodeVal')){
const code = ele.value
ele.value = ''
if(!ConfigManager.getServerCodes().includes(code) && code){
ConfigManager.getServerCodes().push(code)
ConfigManager.save()
loggerSettings.log('Added modpack code to configuration and saved it')
prepareLauncherTab()
} else {
loggerSettings.log('Modpack code already exists or is empty, not adding.')
}
}
}
// Sets up the onclick listeners for each remove code buttons
const sEls = document.querySelectorAll('[remcode]')
Array.from(sEls).map((v, index, arr) => {
v.onclick = () => {
if(v.hasAttribute('remcode')){
const code = v.getAttribute('remcode')
if(ConfigManager.getServerCodes().includes(code)){
ConfigManager.getServerCodes().splice(ConfigManager.getServerCodes().indexOf(code), 1)
ConfigManager.save()
loggerSettings.log('Added removed code from configuration and saved it')
prepareLauncherTab()
}
}
loggerSettings.log('Modpack code doesnt exist!, not removing.')
}
})
}
/**
* 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 < selectBtns.length; i++) {
if (selectBtns[i].hasAttribute('selected')) {
selectBtns[i].removeAttribute('selected')
selectBtns[i].innerHTML = 'Select Account'
}
}
val.setAttribute('selected', '')
val.innerHTML = 'Selected Account &#10004;'
setSelectedAccount(val.closest('.settingsAuthAccount').getAttribute('uuid'))
}
})
}
/**
* Bind functionality for the log out button. If the logged out account was
* the selected account, another account will be selected and the UI will
* be updated accordingly.
*/
function bindAuthAccountLogOut() {
Array.from(document.getElementsByClassName('settingsAuthAccountLogOut')).map((val) => {
val.onclick = (e) => {
let isLastAccount = false
if (Object.keys(ConfigManager.getAuthAccounts()).length === 1) {
isLastAccount = true
setOverlayContent(
'Warning<br>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.<br><br>Are you sure you want to log out?',
'I\'m Sure',
'Cancel'
)
setOverlayHandler(() => {
processLogOut(val, isLastAccount)
toggleOverlay(false)
switchView(getCurrentView(), VIEWS.login)
if(hasRPC){
DiscordWrapper.updateDetails('Adding an Account...')
DiscordWrapper.clearState()
}
})
setDismissHandler(() => {
toggleOverlay(false)
})
toggleOverlay(true, true)
} else {
processLogOut(val, isLastAccount)
}
}
})
}
let data = null
/**
* 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, skip = false) {
data = {
val,
isLastAccount
}
const parent = val.closest('.settingsAuthAccount')
const uuid = parent.getAttribute('uuid')
if (!skip) {
const account = ConfigManager.getAuthAccount(uuid)
if (account.type === 'microsoft') {
toggleOverlay(true, false, 'msOverlay')
ipcRenderer.send('openMSALogoutWindow', 'open')
}
}
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(150, () => {
parent.remove()
})
}
ipcRenderer.on('MSALogoutWindowReply', (event, ...args) => {
toggleOverlay(false, false, 'msOverlay')
processLogOut(data.val, data.isLastAccount, true)
})
/**
* 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 &#10004;'
} 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)
if (authKeys.length === 0) {
return
}
const selectedUUID = ConfigManager.getSelectedAccount().uuid
let authAccountStr = ''
authKeys.map((val) => {
const acc = authAccounts[val]
authAccountStr += `<div class="settingsAuthAccount" uuid="${acc.uuid}">
<div class="settingsAuthAccountLeft">
<img class="settingsAuthAccountImage" alt="${acc.displayName}" src="https://crafatar.com/renders/body/${acc.uuid}?scale=3&default=MHF_Steve&overlay">
</div>
<div class="settingsAuthAccountRight">
<div class="settingsAuthAccountDetails">
<div class="settingsAuthAccountDetailPane">
<div class="settingsAuthAccountDetailTitle">Username</div>
<div class="settingsAuthAccountDetailValue">${acc.displayName}</div>
</div>
<div class="settingsAuthAccountDetailPane">
<div class="settingsAuthAccountDetailTitle">UUID</div>
<div class="settingsAuthAccountDetailValue">${acc.uuid}</div>
</div>
</div>
<div class="settingsAuthAccountActions">
<button class="settingsAuthAccountSelect" ${selectedUUID === acc.uuid ? 'selected>Selected Account &#10004;' : '>Select Account'}</button>
<div class="settingsAuthAccountWrapper">
<button class="settingsAuthAccountLogOut">Log Out</button>
</div>
</div>
</div>
</div>`
})
settingsCurrentAccounts.innerHTML = authAccountStr
}
/**
* Prepare the accounts tab for display.
*/
function prepareAccountsTab() {
populateAuthAccounts()
bindAuthAccountSelect()
bindAuthAccountLogOut()
}
/**
* Prepare the accounts tab for display.
*/
function prepareLauncherTab() {
resolveServerCodesForUI()
bindServerCodeButtons()
}
/**
* 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.
*/
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
}
/**
* 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.getType() === DistroManager.Types.ForgeMod || mdl.getType() === DistroManager.Types.LiteMod || mdl.getType() === DistroManager.Types.LiteLoader) {
if (mdl.getRequired().isRequired()) {
reqMods += `<div id="${mdl.getVersionlessID()}" class="settingsBaseMod settings${submodules ? 'Sub' : ''}Mod" enabled>
<div class="settingsModContent">
<div class="settingsModMainWrapper">
<div class="settingsModStatus"></div>
<div class="settingsModDetails">
<span class="settingsModName">${mdl.getName()}</span>
<span class="settingsModVersion">v${mdl.getVersion()}</span>
</div>
</div>
<label class="toggleSwitch" reqmod>
<input type="checkbox" checked>
<span class="toggleSwitchSlider"></span>
</label>
</div>
${mdl.hasSubModules() ? `<div class="settingsSubModContainer">
${Object.values(parseModulesForUI(mdl.getSubModules(), true, servConf[mdl.getVersionlessID()])).join('')}
</div>` : ''}
</div>`
} else {
const conf = servConf[mdl.getVersionlessID()]
const val = typeof conf === 'object' ? conf.value : conf
optMods += `<div id="${mdl.getVersionlessID()}" class="settingsBaseMod settings${submodules ? 'Sub' : ''}Mod" ${val ? 'enabled' : ''}>
<div class="settingsModContent">
<div class="settingsModMainWrapper">
<div class="settingsModStatus"></div>
<div class="settingsModDetails">
<span class="settingsModName">${mdl.getName()}</span>
<span class="settingsModVersion">v${mdl.getVersion()}</span>
</div>
</div>
<label class="toggleSwitch">
<input type="checkbox" formod="${mdl.getVersionlessID()}" ${val ? 'checked' : ''}>
<span class="toggleSwitchSlider"></span>
</label>
</div>
${mdl.hasSubModules() ? `<div class="settingsSubModContainer">
${Object.values(parseModulesForUI(mdl.getSubModules(), true, conf.mods)).join('')}
</div>` : ''}
</div>`
}
}
}
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.
*/
function resolveDropinModsForUI() {
const serv = DistroManager.getDistribution().getServer(ConfigManager.getSelectedServer())
CACHE_SETTINGS_MODS_DIR = path.join(ConfigManager.getInstanceDirectory(), serv.getID(), 'mods')
CACHE_DROPIN_MODS = DropinModUtil.scanForDropinMods(CACHE_SETTINGS_MODS_DIR, serv.getMinecraftVersion())
let dropinMods = ''
for(dropin of CACHE_DROPIN_MODS){
dropinMods += `<div id="${dropin.fullName}" class="settingsBaseMod settingsDropinMod" ${!dropin.disabled ? 'enabled' : ''}>
<div class="settingsModContent">
<div class="settingsModMainWrapper">
<div class="settingsModStatus"></div>
<div class="settingsModDetails">
<span class="settingsModName">${dropin.name}</span>
<div class="settingsDropinRemoveWrapper">
<button class="settingsDropinRemoveButton" remmod="${dropin.fullName}">Remove</button>
</div>
</div>
</div>
<label class="toggleSwitch">
<input type="checkbox" formod="${dropin.fullName}" dropin ${!dropin.disabled ? 'checked' : ''}>
<span class="toggleSwitchSlider"></span>
</label>
</div>
</div>`
}
document.getElementById('settingsDropinModsContent').innerHTML = dropinMods
}
function resolveServerCodesForUI(){
/* Server Codes */
let servCodes = ''
for(let servCode of ConfigManager.getServerCodes()){
const servs = DistroManager.getDistribution().getServersFromCode(servCode)
const valid = servs && servs.length
servCodes +=
`
<div id="${servCode}" class="settingsServerCode" ${valid ? 'valid' : ''}>
<div class="settingsServerCodeContent">
<div class="settingsServerCodeMainWrapper">
<div class="settingsServerCodeStatus"></div>
<div class="settingsServerCodeDetails">
<span class="settingsServerCodeName">${servCode}</span>
<div class="settingsServerCodeServerNamesContent" code="${servCode}">
</div>
</div>
</div>
<div class="settingsServerCodeRemoveWrapper">
<button class="settingsServerCodeRemoveButton" id="settingsRemoveServerCode" remcode="${servCode}">Remove</button>
</div>
</div>
</div>
`
}
document.getElementById('settingsServerCodesListContent').innerHTML = servCodes
/* Server Names List */
for(let ele of document.getElementsByClassName('settingsServerCodeServerNamesContent')){
servNames = ''
const code = ele.getAttribute('code')
const servs = DistroManager.getDistribution().getServersFromCode(code)
const valid = servs && servs.length
if(valid){
for(let serv of servs){
loggerSettings.log('Modpack: ' + serv.getName())
servNames +=
`
<span class="settingsServerCodeServerName">${serv.getName()}</span>
`
}
} else {
servNames =
`
<span class="settingsServerCodeServerName">Invalid Code</span>
`
}
ele.innerHTML = servNames
}
}
/**
* 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 = () => {
const fullName = v.getAttribute('remmod')
const res = DropinModUtil.deleteDropinMod(CACHE_SETTINGS_MODS_DIR, fullName)
if (res) {
document.getElementById(fullName).remove()
} else {
setOverlayContent(
`Failed to Delete<br>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 = e => {
fsBtn.removeAttribute('drag')
e.preventDefault()
DropinModUtil.addDropinMods(e.dataTransfer.files, CACHE_SETTINGS_MODS_DIR)
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<br>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', (e) => {
if (getCurrentView() === VIEWS.settings && selectedSettingsTab === 'settingsTabMods') {
if (e.key === 'F5') {
reloadDropinMods()
resolveShaderpacksForUI()
}
}
})
function reloadDropinMods() {
resolveDropinModsForUI()
bindDropinModsRemoveButton()
bindDropinModFileSystemButton()
bindModsToggleSwitch()
}
// Shaderpack
let CACHE_SETTINGS_INSTANCE_DIR
let CACHE_SHADERPACKS
let CACHE_SELECTED_SHADERPACK
/**
* Load shaderpack information.
*/
function resolveShaderpacksForUI() {
const serv = DistroManager.getDistribution().getServer(ConfigManager.getSelectedServer())
CACHE_SETTINGS_INSTANCE_DIR = path.join(ConfigManager.getInstanceDirectory(), serv.getID())
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 = e => {
spBtn.removeAttribute('drag')
e.preventDefault()
DropinModUtil.addShaderpacks(e.dataTransfer.files, CACHE_SETTINGS_INSTANCE_DIR)
saveShaderpackSettings()
resolveShaderpacksForUI()
}
}
// Server status bar functions.
/**
* Load the currently selected server information onto the mods tab.
*/
function loadSelectedServerOnModsTab() {
const serv = DistroManager.getDistribution().getServer(ConfigManager.getSelectedServer())
document.getElementById('settingsSelServContent').innerHTML = `
<div class="serverListingDetails">
<img class="serverListingImg" src="${serv.getIcon()}"/>
<span class="serverListingName">${serv.getName()}</span>
<span class="serverListingDescription">${serv.getDescription()}</span>
<div class="serverListingInfo">
<div class="serverListingRevision">${serv.getVersion()}</div>
</div>
</div>
`
}
// Bind functionality to the server switch button.
document.getElementById('settingsSwitchServerButton').addEventListener('click', (e) => {
e.target.blur()
toggleServerSelection(true)
})
/**
* Save mod configuration for the current selected server.
*/
function saveAllModConfigurations() {
saveModConfiguration()
ConfigManager.save()
saveDropinModConfiguration()
}
/**
* Function to refresh the mods tab whenever the selected
* server is changed.
*/
function animateModsTabRefresh(){
$('#settingsTabMods').fadeOut(150, () => {
prepareModsTab()
$('#settingsTabMods').fadeIn(150)
})
}
/**
* Prepare the Mods tab for display.
*/
function prepareModsTab(first) {
resolveModsForUI()
resolveDropinModsForUI()
resolveShaderpacksForUI()
bindDropinModsRemoveButton()
bindDropinModFileSystemButton()
bindShaderpackButton()
bindModsToggleSwitch()
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')
// 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/1.25){
bar.style.background = '#e86060'
} else if(sMinV >= max/2) {
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/1.25){
bar.style.background = '#e86060'
} else if(sMaxV >= max/2) {
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'
}
/**
* Validate the provided executable path and display the data on
* the UI.
*
* @param {string} execPath The executable path to populate against.
*/
function populateJavaExecDetails(execPath) {
const jg = new JavaGuard(DistroManager.getDistribution().getServer(ConfigManager.getSelectedServer()).getMinecraftVersion())
jg._validateJavaBinary(execPath).then(v => {
if (v.valid) {
const vendor = v.vendor != null ? ` (${v.vendor})` : ''
if (v.version.major < 9) {
settingsJavaExecDetails.innerHTML = `Selected: Java ${v.version.major} Update ${v.version.update} (x${v.arch})${vendor}`
} else {
settingsJavaExecDetails.innerHTML = `Selected: Java ${v.version.major}.${v.version.minor}.${v.version.revision} (x${v.arch})${vendor}`
}
} else {
settingsJavaExecDetails.innerHTML = 'Invalid Selection'
}
})
}
/**
* Prepare the Java tab for display.
*/
function prepareJavaTab() {
bindRangeSlider()
populateMemoryStatus()
}
/**
* 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)
if(preRelComp != null && preRelComp.includes('release')) {
return false
}
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/ModRealms-Network/HeliosLauncher/releases.atom',
success: (data) => {
const version = 'v' + remote.app.getVersion()
const entries = $(data).find('entry')
for (let i = 0; i < entries.length; i++) {
const entry = $(entries[i])
let id = entry.find('id').text()
id = id.substring(id.lastIndexOf('/') + 1)
if (id === version) {
settingsAboutChangelogTitle.innerHTML = entry.find('title').text()
settingsAboutChangelogText.innerHTML = entry.find('content').text()
settingsAboutChangelogButton.href = entry.find('link').attr('href')
}
}
},
timeout: 10000
}).catch(err => {
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 GitHub<span style="font-size: 10px;color: gray;text-shadow: none !important;">Close the launcher and run the dmg to update.</span>', 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.
*/
function prepareSettings(first = false) {
if (first) {
setupSettingsTabs()
initSettingsValidators()
prepareUpdateTab()
} else {
prepareModsTab()
}
initSettingsValues()
prepareAccountsTab()
prepareJavaTab()
prepareLauncherTab()
prepareAboutTab()
}
// Prepare the settings UI on startup.
//prepareSettings(true)