diff --git a/app/assets/js/scripts/landing.js b/app/assets/js/scripts/landing.js index c19f0b67..e7ffab57 100644 --- a/app/assets/js/scripts/landing.js +++ b/app/assets/js/scripts/landing.js @@ -1,1143 +1,1143 @@ -/** - * Script for landing.ejs - */ -// Requirements -const cp = require('child_process') -const crypto = require('crypto') -const {URL} = require('url') - -// Internal Requirements -const DiscordWrapper = require('./assets/js/discordwrapper') -const Mojang = require('./assets/js/mojang') -const ProcessBuilder = require('./assets/js/processbuilder') -const ServerStatus = require('./assets/js/serverstatus') - -// Launch Elements -const launch_content = document.getElementById('launch_content') -const launch_details = document.getElementById('launch_details') -const launch_progress = document.getElementById('launch_progress') -const launch_progress_label = document.getElementById('launch_progress_label') -const launch_details_text = document.getElementById('launch_details_text') -const server_selection_button = document.getElementById('server_selection_button') -const user_text = document.getElementById('user_text') - -const loggerLanding = LoggerUtil('%c[Landing]', 'color: #000668; font-weight: bold') - -/* Launch Progress Wrapper Functions */ - -/** - * Show/hide the loading area. - * - * @param {boolean} loading True if the loading area should be shown, otherwise false. - */ -function toggleLaunchArea(loading){ - if(loading){ - launch_details.style.display = 'flex' - launch_content.style.display = 'none' - } else { - launch_details.style.display = 'none' - launch_content.style.display = 'inline-flex' - } -} - -/** - * Set the details text of the loading area. - * - * @param {string} details The new text for the loading details. - */ -function setLaunchDetails(details){ - launch_details_text.innerHTML = details -} - -/** - * Set the value of the loading progress bar and display that value. - * - * @param {number} value The progress value. - * @param {number} max The total size. - * @param {number|string} percent Optional. The percentage to display on the progress label. - */ -function setLaunchPercentage(value, max, percent = ((value/max)*100)){ - launch_progress.setAttribute('max', max) - launch_progress.setAttribute('value', value) - launch_progress_label.innerHTML = percent + '%' -} - -/** - * Set the value of the OS progress bar and display that on the UI. - * - * @param {number} value The progress value. - * @param {number} max The total download size. - * @param {number|string} percent Optional. The percentage to display on the progress label. - */ -function setDownloadPercentage(value, max, percent = ((value/max)*100)){ - remote.getCurrentWindow().setProgressBar(value/max) - setLaunchPercentage(value, max, percent) -} - -/** - * Enable or disable the launch button. - * - * @param {boolean} val True to enable, false to disable. - */ -function setLaunchEnabled(val){ - document.getElementById('launch_button').disabled = !val -} - -// Bind launch button -document.getElementById('launch_button').addEventListener('click', function(e){ - loggerLanding.log('Launching game..') - const mcVersion = DistroManager.getDistribution().getServer(ConfigManager.getSelectedServer()).getMinecraftVersion() - const jExe = ConfigManager.getJavaExecutable() - if(jExe == null){ - asyncSystemScan(mcVersion) - } else { - - setLaunchDetails(Lang.queryJS('landing.launch.pleaseWait')) - toggleLaunchArea(true) - setLaunchPercentage(0, 100) - - const jg = new JavaGuard(mcVersion) - jg._validateJavaBinary(jExe).then((v) => { - loggerLanding.log('Java version meta', v) - if(v.valid){ - dlAsync() - } else { - asyncSystemScan(mcVersion) - } - }) - } -}) - -// Bind settings button -document.getElementById('settingsMediaButton').onclick = (e) => { - prepareSettings() - switchView(getCurrentView(), VIEWS.settings) -} - -// Bind avatar overlay button. -document.getElementById('avatarOverlay').onclick = (e) => { - prepareSettings() - switchView(getCurrentView(), VIEWS.settings, 500, 500, () => { - settingsNavItemListener(document.getElementById('settingsNavAccount'), false) - }) -} - -// Bind selected account -function updateSelectedAccount(authUser){ - let username = 'No Account Selected' - if(authUser != null){ - if(authUser.displayName != null){ - username = authUser.displayName - } - if(authUser.uuid != null){ - document.getElementById('avatarContainer').style.backgroundImage = `url('https://crafatar.com/renders/body/${authUser.uuid}')` - } - } - user_text.innerHTML = username -} -updateSelectedAccount(ConfigManager.getSelectedAccount()) - -// Bind selected server -function updateSelectedServer(serv){ - if(getCurrentView() === VIEWS.settings){ - saveAllModConfigurations() - } - ConfigManager.setSelectedServer(serv != null ? serv.getID() : null) - ConfigManager.save() - server_selection_button.innerHTML = '\u2022 ' + (serv != null ? serv.getName() : 'No Server Selected') - if(getCurrentView() === VIEWS.settings){ - animateModsTabRefresh() - } - setLaunchEnabled(serv != null) -} -// Real text is set in uibinder.js on distributionIndexDone. -server_selection_button.innerHTML = '\u2022 Loading..' -server_selection_button.onclick = (e) => { - e.target.blur() - toggleServerSelection(true) -} - -// Update Mojang Status Color -const refreshMojangStatuses = async function(){ - loggerLanding.log('Refreshing Mojang Statuses..') - - let status = 'grey' - let tooltipEssentialHTML = '' - let tooltipNonEssentialHTML = '' - - try { - const statuses = await Mojang.status() - greenCount = 0 - greyCount = 0 - - for(let i=0; i - - ${service.name} - ` - } else { - tooltipNonEssentialHTML += `
- - ${service.name} -
` - } - - if(service.status === 'yellow' && status !== 'red'){ - status = 'yellow' - } else if(service.status === 'red'){ - status = 'red' - } else { - if(service.status === 'grey'){ - ++greyCount - } - ++greenCount - } - - } - - if(greenCount === statuses.length){ - if(greyCount === statuses.length){ - status = 'grey' - } else { - status = 'green' - } - } - - } catch (err) { - loggerLanding.warn('Unable to refresh Mojang service status.') - loggerLanding.debug(err) - } - - document.getElementById('mojangStatusEssentialContainer').innerHTML = tooltipEssentialHTML - document.getElementById('mojangStatusNonEssentialContainer').innerHTML = tooltipNonEssentialHTML - document.getElementById('mojang_status_icon').style.color = Mojang.statusToHex(status) -} - -const refreshServerStatus = async function(fade = false){ - loggerLanding.log('Refreshing Server Status') - const serv = DistroManager.getDistribution().getServer(ConfigManager.getSelectedServer()) - - let pLabel = 'SERVER' - let pVal = 'OFFLINE' - - try { - const serverURL = new URL('my://' + serv.getAddress()) - const servStat = await ServerStatus.getStatus(serverURL.hostname, serverURL.port) - if(servStat.online){ - pLabel = 'PLAYERS' - pVal = servStat.onlinePlayers + '/' + servStat.maxPlayers - } - - } catch (err) { - loggerLanding.warn('Unable to refresh server status, assuming offline.') - loggerLanding.debug(err) - } - if(fade){ - $('#server_status_wrapper').fadeOut(250, () => { - document.getElementById('landingPlayerLabel').innerHTML = pLabel - document.getElementById('player_count').innerHTML = pVal - $('#server_status_wrapper').fadeIn(500) - }) - } else { - document.getElementById('landingPlayerLabel').innerHTML = pLabel - document.getElementById('player_count').innerHTML = pVal - } - -} - -refreshMojangStatuses() -// Server Status is refreshed in uibinder.js on distributionIndexDone. - -// Set refresh rate to once every 5 minutes. -let mojangStatusListener = setInterval(() => refreshMojangStatuses(true), 300000) -let serverStatusListener = setInterval(() => refreshServerStatus(true), 300000) - -/** - * Shows an error overlay, toggles off the launch area. - * - * @param {string} title The overlay title. - * @param {string} desc The overlay description. - */ -function showLaunchFailure(title, desc){ - setOverlayContent( - title, - desc, - 'Okay' - ) - setOverlayHandler(null) - toggleOverlay(true) - toggleLaunchArea(false) -} - -/* System (Java) Scan */ - -let sysAEx -let scanAt - -let extractListener - -/** - * Asynchronously scan the system for valid Java installations. - * - * @param {string} mcVersion The Minecraft version we are scanning for. - * @param {boolean} launchAfter Whether we should begin to launch after scanning. - */ -function asyncSystemScan(mcVersion, launchAfter = true){ - - setLaunchDetails('Please wait..') - toggleLaunchArea(true) - setLaunchPercentage(0, 100) - - const loggerSysAEx = LoggerUtil('%c[SysAEx]', 'color: #353232; font-weight: bold') - - const forkEnv = JSON.parse(JSON.stringify(process.env)) - forkEnv.CONFIG_DIRECT_PATH = ConfigManager.getLauncherDirectory() - - // Fork a process to run validations. - sysAEx = cp.fork(path.join(__dirname, 'assets', 'js', 'assetexec.js'), [ - 'JavaGuard', - mcVersion - ], { - env: forkEnv, - stdio: 'pipe' - }) - // Stdout - sysAEx.stdio[1].setEncoding('utf8') - sysAEx.stdio[1].on('data', (data) => { - loggerSysAEx.log(data) - }) - // Stderr - sysAEx.stdio[2].setEncoding('utf8') - sysAEx.stdio[2].on('data', (data) => { - loggerSysAEx.log(data) - }) - - sysAEx.on('message', (m) => { - - if(m.context === 'validateJava'){ - if(m.result == null){ - // If the result is null, no valid Java installation was found. - // Show this information to the user. - setOverlayContent( - 'No Compatible
Java Installation Found', - 'In order to join WesterosCraft, you need a 64-bit installation of Java 8. Would you like us to install a copy? By installing, you accept Oracle\'s license agreement.', - 'Install Java', - 'Install Manually' - ) - setOverlayHandler(() => { - setLaunchDetails('Preparing Java Download..') - sysAEx.send({task: 'changeContext', class: 'AssetGuard', args: [ConfigManager.getCommonDirectory(),ConfigManager.getJavaExecutable()]}) - sysAEx.send({task: 'execute', function: '_enqueueOpenJDK', argsArr: [ConfigManager.getDataDirectory()]}) - toggleOverlay(false) - }) - setDismissHandler(() => { - $('#overlayContent').fadeOut(250, () => { - //$('#overlayDismiss').toggle(false) - setOverlayContent( - 'Java is Required
to Launch', - 'A valid x64 installation of Java 8 is required to launch.

Please refer to our Java Management Guide for instructions on how to manually install Java.', - 'I Understand', - 'Go Back' - ) - setOverlayHandler(() => { - toggleLaunchArea(false) - toggleOverlay(false) - }) - setDismissHandler(() => { - toggleOverlay(false, true) - asyncSystemScan() - }) - $('#overlayContent').fadeIn(250) - }) - }) - toggleOverlay(true, true) - - } else { - // Java installation found, use this to launch the game. - ConfigManager.setJavaExecutable(m.result) - ConfigManager.save() - - // We need to make sure that the updated value is on the settings UI. - // Just incase the settings UI is already open. - settingsJavaExecVal.value = m.result - populateJavaExecDetails(settingsJavaExecVal.value) - - if(launchAfter){ - dlAsync() - } - sysAEx.disconnect() - } - } else if(m.context === '_enqueueOpenJDK'){ - - if(m.result === true){ - - // Oracle JRE enqueued successfully, begin download. - setLaunchDetails('Downloading Java..') - sysAEx.send({task: 'execute', function: 'processDlQueues', argsArr: [[{id:'java', limit:1}]]}) - - } else { - - // Oracle JRE enqueue failed. Probably due to a change in their website format. - // User will have to follow the guide to install Java. - setOverlayContent( - 'Unexpected Issue:
Java Download Failed', - 'Unfortunately we\'ve encountered an issue while attempting to install Java. You will need to manually install a copy. Please check out our Troubleshooting Guide for more details and instructions.', - 'I Understand' - ) - setOverlayHandler(() => { - toggleOverlay(false) - toggleLaunchArea(false) - }) - toggleOverlay(true) - sysAEx.disconnect() - - } - - } else if(m.context === 'progress'){ - - switch(m.data){ - case 'download': - // Downloading.. - setDownloadPercentage(m.value, m.total, m.percent) - break - } - - } else if(m.context === 'complete'){ - - switch(m.data){ - case 'download': { - // Show installing progress bar. - remote.getCurrentWindow().setProgressBar(2) - - // Wait for extration to complete. - const eLStr = 'Extracting' - let dotStr = '' - setLaunchDetails(eLStr) - extractListener = setInterval(() => { - if(dotStr.length >= 3){ - dotStr = '' - } else { - dotStr += '.' - } - setLaunchDetails(eLStr + dotStr) - }, 750) - break - } - case 'java': - // Download & extraction complete, remove the loading from the OS progress bar. - remote.getCurrentWindow().setProgressBar(-1) - - // Extraction completed successfully. - ConfigManager.setJavaExecutable(m.args[0]) - ConfigManager.save() - - if(extractListener != null){ - clearInterval(extractListener) - extractListener = null - } - - setLaunchDetails('Java Installed!') - - if(launchAfter){ - dlAsync() - } - - sysAEx.disconnect() - break - } - - } else if(m.context === 'error'){ - console.log(m.error) - } - }) - - // Begin system Java scan. - setLaunchDetails('Checking system info..') - sysAEx.send({task: 'execute', function: 'validateJava', argsArr: [ConfigManager.getDataDirectory()]}) - -} - -// Keep reference to Minecraft Process -let proc -// Is DiscordRPC enabled -let hasRPC = false -// Joined server regex -const SERVER_JOINED_REGEX = /\[.+\]: \[CHAT\] [a-zA-Z0-9_]{1,16} joined the game/ -const GAME_JOINED_REGEX = /\[.+\]: Skipping bad option: lastServer:/ -const GAME_LAUNCH_REGEX = /^\[.+\]: (?:MinecraftForge .+ Initialized|ModLauncher .+ starting: .+)$/ -const MIN_LINGER = 5000 - -let aEx -let serv -let versionData -let forgeData - -let progressListener - -function dlAsync(login = true){ - - // Login parameter is temporary for debug purposes. Allows testing the validation/downloads without - // launching the game. - - if(login) { - if(ConfigManager.getSelectedAccount() == null){ - loggerLanding.error('You must be logged into an account.') - return - } - } - - setLaunchDetails('Please wait..') - toggleLaunchArea(true) - setLaunchPercentage(0, 100) - - const loggerAEx = LoggerUtil('%c[AEx]', 'color: #353232; font-weight: bold') - const loggerLaunchSuite = LoggerUtil('%c[LaunchSuite]', 'color: #000668; font-weight: bold') - - const forkEnv = JSON.parse(JSON.stringify(process.env)) - forkEnv.CONFIG_DIRECT_PATH = ConfigManager.getLauncherDirectory() - - // Start AssetExec to run validations and downloads in a forked process. - aEx = cp.fork(path.join(__dirname, 'assets', 'js', 'assetexec.js'), [ - 'AssetGuard', - ConfigManager.getCommonDirectory(), - ConfigManager.getJavaExecutable() - ], { - env: forkEnv, - stdio: 'pipe' - }) - // Stdout - aEx.stdio[1].setEncoding('utf8') - aEx.stdio[1].on('data', (data) => { - loggerAEx.log(data) - }) - // Stderr - aEx.stdio[2].setEncoding('utf8') - aEx.stdio[2].on('data', (data) => { - loggerAEx.log(data) - }) - aEx.on('error', (err) => { - loggerLaunchSuite.error('Error during launch', err) - showLaunchFailure('Error During Launch', err.message || 'See console (CTRL + Shift + i) for more details.') - }) - aEx.on('close', (code, signal) => { - if(code !== 0){ - loggerLaunchSuite.error(`AssetExec exited with code ${code}, assuming error.`) - showLaunchFailure('Error During Launch', 'See console (CTRL + Shift + i) for more details.') - } - }) - - // Establish communications between the AssetExec and current process. - aEx.on('message', (m) => { - - if(m.context === 'validate'){ - switch(m.data){ - case 'distribution': - setLaunchPercentage(20, 100) - loggerLaunchSuite.log('Validated distibution index.') - setLaunchDetails('Loading version information..') - break - case 'version': - setLaunchPercentage(40, 100) - loggerLaunchSuite.log('Version data loaded.') - setLaunchDetails('Validating asset integrity..') - break - case 'assets': - setLaunchPercentage(60, 100) - loggerLaunchSuite.log('Asset Validation Complete') - setLaunchDetails('Validating library integrity..') - break - case 'libraries': - setLaunchPercentage(80, 100) - loggerLaunchSuite.log('Library validation complete.') - setLaunchDetails('Validating miscellaneous file integrity..') - break - case 'files': - setLaunchPercentage(100, 100) - loggerLaunchSuite.log('File validation complete.') - setLaunchDetails('Downloading files..') - break - } - } else if(m.context === 'progress'){ - switch(m.data){ - case 'assets': { - const perc = (m.value/m.total)*20 - setLaunchPercentage(40+perc, 100, parseInt(40+perc)) - break - } - case 'download': - setDownloadPercentage(m.value, m.total, m.percent) - break - case 'extract': { - // Show installing progress bar. - remote.getCurrentWindow().setProgressBar(2) - - // Download done, extracting. - const eLStr = 'Extracting libraries' - let dotStr = '' - setLaunchDetails(eLStr) - progressListener = setInterval(() => { - if(dotStr.length >= 3){ - dotStr = '' - } else { - dotStr += '.' - } - setLaunchDetails(eLStr + dotStr) - }, 750) - break - } - } - } else if(m.context === 'complete'){ - switch(m.data){ - case 'download': - // Download and extraction complete, remove the loading from the OS progress bar. - remote.getCurrentWindow().setProgressBar(-1) - if(progressListener != null){ - clearInterval(progressListener) - progressListener = null - } - - setLaunchDetails('Preparing to launch..') - break - } - } else if(m.context === 'error'){ - switch(m.data){ - case 'download': - loggerLaunchSuite.error('Error while downloading:', m.error) - - if(m.error.code === 'ENOENT'){ - showLaunchFailure( - 'Download Error', - 'Could not connect to the file server. Ensure that you are connected to the internet and try again.' - ) - } else { - showLaunchFailure( - 'Download Error', - 'Check the console (CTRL + Shift + i) for more details. Please try again.' - ) - } - - remote.getCurrentWindow().setProgressBar(-1) - - // Disconnect from AssetExec - aEx.disconnect() - break - } - } else if(m.context === 'validateEverything'){ - - let allGood = true - - // If these properties are not defined it's likely an error. - if(m.result.forgeData == null || m.result.versionData == null){ - loggerLaunchSuite.error('Error during validation:', m.result) - - loggerLaunchSuite.error('Error during launch', m.result.error) - showLaunchFailure('Error During Launch', 'Please check the console (CTRL + Shift + i) for more details.') - - allGood = false - } - - forgeData = m.result.forgeData - versionData = m.result.versionData - - if(login && allGood) { - const authUser = ConfigManager.getSelectedAccount() - loggerLaunchSuite.log(`Sending selected account (${authUser.displayName}) to ProcessBuilder.`) - let pb = new ProcessBuilder(serv, versionData, forgeData, authUser, remote.app.getVersion()) - setLaunchDetails('Launching game..') - - const onLoadComplete = () => { - toggleLaunchArea(false) - if(hasRPC){ - DiscordWrapper.updateDetails('Loading game..') - } - proc.stdout.on('data', gameStateChange) - proc.stdout.removeListener('data', tempListener) - proc.stderr.removeListener('data', gameErrorListener) - } - const start = Date.now() - - // Attach a temporary listener to the client output. - // Will wait for a certain bit of text meaning that - // the client application has started, and we can hide - // the progress bar stuff. - const tempListener = function(data){ - if(GAME_LAUNCH_REGEX.test(data.trim())){ - const diff = Date.now()-start - if(diff < MIN_LINGER) { - setTimeout(onLoadComplete, MIN_LINGER-diff) - } else { - onLoadComplete() - } - } - } - - // Listener for Discord RPC. - const gameStateChange = function(data){ - data = data.trim() - if(SERVER_JOINED_REGEX.test(data)){ - DiscordWrapper.updateDetails('Exploring the Realm!') - } else if(GAME_JOINED_REGEX.test(data)){ - DiscordWrapper.updateDetails('Sailing to Westeros!') - } - } - - const gameErrorListener = function(data){ - data = data.trim() - if(data.indexOf('Could not find or load main class net.minecraft.launchwrapper.Launch') > -1){ - loggerLaunchSuite.error('Game launch failed, LaunchWrapper was not downloaded properly.') - showLaunchFailure('Error During Launch', 'The main file, LaunchWrapper, failed to download properly. As a result, the game cannot launch.

To fix this issue, temporarily turn off your antivirus software and launch the game again.

If you have time, please submit an issue and let us know what antivirus software you use. We\'ll contact them and try to straighten things out.') - } - } - - try { - // Build Minecraft process. - proc = pb.build() - - // Bind listeners to stdout. - proc.stdout.on('data', tempListener) - proc.stderr.on('data', gameErrorListener) - - setLaunchDetails('Done. Enjoy the server!') - - // Init Discord Hook - const distro = DistroManager.getDistribution() - if(distro.discord != null && serv.discord != null){ - DiscordWrapper.initRPC(distro.discord, serv.discord) - hasRPC = true - proc.on('close', (code, signal) => { - loggerLaunchSuite.log('Shutting down Discord Rich Presence..') - DiscordWrapper.shutdownRPC() - hasRPC = false - proc = null - }) - } - - } catch(err) { - - loggerLaunchSuite.error('Error during launch', err) - showLaunchFailure('Error During Launch', 'Please check the console (CTRL + Shift + i) for more details.') - - } - } - - // Disconnect from AssetExec - aEx.disconnect() - - } - }) - - // Begin Validations - - // Validate Forge files. - setLaunchDetails('Loading server information..') - - refreshDistributionIndex(true, (data) => { - onDistroRefresh(data) - serv = data.getServer(ConfigManager.getSelectedServer()) - aEx.send({task: 'execute', function: 'validateEverything', argsArr: [ConfigManager.getSelectedServer(), DistroManager.isDevMode()]}) - }, (err) => { - loggerLaunchSuite.log('Error while fetching a fresh copy of the distribution index.', err) - refreshDistributionIndex(false, (data) => { - onDistroRefresh(data) - serv = data.getServer(ConfigManager.getSelectedServer()) - aEx.send({task: 'execute', function: 'validateEverything', argsArr: [ConfigManager.getSelectedServer(), DistroManager.isDevMode()]}) - }, (err) => { - loggerLaunchSuite.error('Unable to refresh distribution index.', err) - if(DistroManager.getDistribution() == null){ - showLaunchFailure('Fatal Error', 'Could not load a copy of the distribution index. See the console (CTRL + Shift + i) for more details.') - - // Disconnect from AssetExec - aEx.disconnect() - } else { - serv = data.getServer(ConfigManager.getSelectedServer()) - aEx.send({task: 'execute', function: 'validateEverything', argsArr: [ConfigManager.getSelectedServer(), DistroManager.isDevMode()]}) - } - }) - }) -} - -/** - * News Loading Functions - */ - -// DOM Cache -const newsContent = document.getElementById('newsContent') -const newsArticleTitle = document.getElementById('newsArticleTitle') -const newsArticleDate = document.getElementById('newsArticleDate') -const newsArticleAuthor = document.getElementById('newsArticleAuthor') -const newsArticleComments = document.getElementById('newsArticleComments') -const newsNavigationStatus = document.getElementById('newsNavigationStatus') -const newsArticleContentScrollable = document.getElementById('newsArticleContentScrollable') -const nELoadSpan = document.getElementById('nELoadSpan') - -// News slide caches. -let newsActive = false -let newsGlideCount = 0 - -/** - * Show the news UI via a slide animation. - * - * @param {boolean} up True to slide up, otherwise false. - */ -function slide_(up){ - const lCUpper = document.querySelector('#landingContainer > #upper') - const lCLLeft = document.querySelector('#landingContainer > #lower > #left') - const lCLCenter = document.querySelector('#landingContainer > #lower > #center') - const lCLRight = document.querySelector('#landingContainer > #lower > #right') - const newsBtn = document.querySelector('#landingContainer > #lower > #center #content') - const landingContainer = document.getElementById('landingContainer') - const newsContainer = document.querySelector('#landingContainer > #newsContainer') - - newsGlideCount++ - - if(up){ - lCUpper.style.top = '-200vh' - lCLLeft.style.top = '-200vh' - lCLCenter.style.top = '-200vh' - lCLRight.style.top = '-200vh' - newsBtn.style.top = '130vh' - newsContainer.style.top = '0px' - //date.toLocaleDateString('en-US', {month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: 'numeric'}) - //landingContainer.style.background = 'rgba(29, 29, 29, 0.55)' - landingContainer.style.background = 'rgba(0, 0, 0, 0.50)' - setTimeout(() => { - if(newsGlideCount === 1){ - lCLCenter.style.transition = 'none' - newsBtn.style.transition = 'none' - } - newsGlideCount-- - }, 2000) - } else { - setTimeout(() => { - newsGlideCount-- - }, 2000) - landingContainer.style.background = null - lCLCenter.style.transition = null - newsBtn.style.transition = null - newsContainer.style.top = '100%' - lCUpper.style.top = '0px' - lCLLeft.style.top = '0px' - lCLCenter.style.top = '0px' - lCLRight.style.top = '0px' - newsBtn.style.top = '10px' - } -} - -// Bind news button. -document.getElementById('newsButton').onclick = () => { - // Toggle tabbing. - if(newsActive){ - $('#landingContainer *').removeAttr('tabindex') - $('#newsContainer *').attr('tabindex', '-1') - } else { - $('#landingContainer *').attr('tabindex', '-1') - $('#newsContainer, #newsContainer *, #lower, #lower #center *').removeAttr('tabindex') - if(newsAlertShown){ - $('#newsButtonAlert').fadeOut(2000) - newsAlertShown = false - ConfigManager.setNewsCacheDismissed(true) - ConfigManager.save() - } - } - slide_(!newsActive) - newsActive = !newsActive -} - -// Array to store article meta. -let newsArr = null - -// News load animation listener. -let newsLoadingListener = null - -/** - * Set the news loading animation. - * - * @param {boolean} val True to set loading animation, otherwise false. - */ -function setNewsLoading(val){ - if(val){ - const nLStr = 'Checking for News' - let dotStr = '..' - nELoadSpan.innerHTML = nLStr + dotStr - newsLoadingListener = setInterval(() => { - if(dotStr.length >= 3){ - dotStr = '' - } else { - dotStr += '.' - } - nELoadSpan.innerHTML = nLStr + dotStr - }, 750) - } else { - if(newsLoadingListener != null){ - clearInterval(newsLoadingListener) - newsLoadingListener = null - } - } -} - -// Bind retry button. -newsErrorRetry.onclick = () => { - $('#newsErrorFailed').fadeOut(250, () => { - initNews() - $('#newsErrorLoading').fadeIn(250) - }) -} - -newsArticleContentScrollable.onscroll = (e) => { - if(e.target.scrollTop > Number.parseFloat($('.newsArticleSpacerTop').css('height'))){ - newsContent.setAttribute('scrolled', '') - } else { - newsContent.removeAttribute('scrolled') - } -} - -/** - * Reload the news without restarting. - * - * @returns {Promise.} A promise which resolves when the news - * content has finished loading and transitioning. - */ -function reloadNews(){ - return new Promise((resolve, reject) => { - $('#newsContent').fadeOut(250, () => { - $('#newsErrorLoading').fadeIn(250) - initNews().then(() => { - resolve() - }) - }) - }) -} - -let newsAlertShown = false - -/** - * Show the news alert indicating there is new news. - */ -function showNewsAlert(){ - newsAlertShown = true - $(newsButtonAlert).fadeIn(250) -} - -/** - * Initialize News UI. This will load the news and prepare - * the UI accordingly. - * - * @returns {Promise.} A promise which resolves when the news - * content has finished loading and transitioning. - */ -function initNews(){ - - return new Promise((resolve, reject) => { - setNewsLoading(true) - - let news = {} - loadNews().then(news => { - - newsArr = news.articles || null - - if(newsArr == null){ - // News Loading Failed - setNewsLoading(false) - - $('#newsErrorLoading').fadeOut(250, () => { - $('#newsErrorFailed').fadeIn(250, () => { - resolve() - }) - }) - } else if(newsArr.length === 0) { - // No News Articles - setNewsLoading(false) - - ConfigManager.setNewsCache({ - date: null, - content: null, - dismissed: false - }) - ConfigManager.save() - - $('#newsErrorLoading').fadeOut(250, () => { - $('#newsErrorNone').fadeIn(250, () => { - resolve() - }) - }) - } else { - // Success - setNewsLoading(false) - - const lN = newsArr[0] - const cached = ConfigManager.getNewsCache() - let newHash = crypto.createHash('sha1').update(lN.content).digest('hex') - let newDate = new Date(lN.date) - let isNew = false - - if(cached.date != null && cached.content != null){ - - if(new Date(cached.date) >= newDate){ - - // Compare Content - if(cached.content !== newHash){ - isNew = true - showNewsAlert() - } else { - if(!cached.dismissed){ - isNew = true - showNewsAlert() - } - } - - } else { - isNew = true - showNewsAlert() - } - - } else { - isNew = true - showNewsAlert() - } - - if(isNew){ - ConfigManager.setNewsCache({ - date: newDate.getTime(), - content: newHash, - dismissed: false - }) - ConfigManager.save() - } - - const switchHandler = (forward) => { - let cArt = parseInt(newsContent.getAttribute('article')) - let nxtArt = forward ? (cArt >= newsArr.length-1 ? 0 : cArt + 1) : (cArt <= 0 ? newsArr.length-1 : cArt - 1) - - displayArticle(newsArr[nxtArt], nxtArt+1) - } - - document.getElementById('newsNavigateRight').onclick = () => { switchHandler(true) } - document.getElementById('newsNavigateLeft').onclick = () => { switchHandler(false) } - - $('#newsErrorContainer').fadeOut(250, () => { - displayArticle(newsArr[0], 1) - $('#newsContent').fadeIn(250, () => { - resolve() - }) - }) - } - - }) - - }) -} - -/** - * Add keyboard controls to the news UI. Left and right arrows toggle - * between articles. If you are on the landing page, the up arrow will - * open the news UI. - */ -document.addEventListener('keydown', (e) => { - if(newsActive){ - if(e.key === 'ArrowRight' || e.key === 'ArrowLeft'){ - document.getElementById(e.key === 'ArrowRight' ? 'newsNavigateRight' : 'newsNavigateLeft').click() - } - // Interferes with scrolling an article using the down arrow. - // Not sure of a straight forward solution at this point. - // if(e.key === 'ArrowDown'){ - // document.getElementById('newsButton').click() - // } - } else { - if(getCurrentView() === VIEWS.landing){ - if(e.key === 'ArrowUp'){ - document.getElementById('newsButton').click() - } - } - } -}) - -/** - * Display a news article on the UI. - * - * @param {Object} articleObject The article meta object. - * @param {number} index The article index. - */ -function displayArticle(articleObject, index){ - newsArticleTitle.innerHTML = articleObject.title - newsArticleTitle.href = articleObject.link - newsArticleAuthor.innerHTML = 'by ' + articleObject.author - newsArticleDate.innerHTML = articleObject.date - newsArticleComments.innerHTML = articleObject.comments - newsArticleComments.href = articleObject.commentsLink - newsArticleContentScrollable.innerHTML = '
' + articleObject.content + '
' - Array.from(newsArticleContentScrollable.getElementsByClassName('bbCodeSpoilerButton')).forEach(v => { - v.onclick = () => { - const text = v.parentElement.getElementsByClassName('bbCodeSpoilerText')[0] - text.style.display = text.style.display === 'block' ? 'none' : 'block' - } - }) - newsNavigationStatus.innerHTML = index + ' of ' + newsArr.length - newsContent.setAttribute('article', index-1) -} - -/** - * Load news information from the RSS feed specified in the - * distribution index. - */ -function loadNews(){ - return new Promise((resolve, reject) => { - const distroData = DistroManager.getDistribution() - const newsFeed = distroData.getRSS() - const newsHost = new URL(newsFeed).origin + '/' - $.ajax({ - url: newsFeed, - success: (data) => { - const items = $(data).find('item') - const articles = [] - - for(let i=0; i { - resolve({ - articles: null - }) - }) - }) +/** + * Script for landing.ejs + */ +// Requirements +const cp = require('child_process') +const crypto = require('crypto') +const {URL} = require('url') + +// Internal Requirements +const DiscordWrapper = require('./assets/js/discordwrapper') +const Mojang = require('./assets/js/mojang') +const ProcessBuilder = require('./assets/js/processbuilder') +const ServerStatus = require('./assets/js/serverstatus') + +// Launch Elements +const launch_content = document.getElementById('launch_content') +const launch_details = document.getElementById('launch_details') +const launch_progress = document.getElementById('launch_progress') +const launch_progress_label = document.getElementById('launch_progress_label') +const launch_details_text = document.getElementById('launch_details_text') +const server_selection_button = document.getElementById('server_selection_button') +const user_text = document.getElementById('user_text') + +const loggerLanding = LoggerUtil('%c[Landing]', 'color: #000668; font-weight: bold') + +/* Launch Progress Wrapper Functions */ + +/** + * Show/hide the loading area. + * + * @param {boolean} loading True if the loading area should be shown, otherwise false. + */ +function toggleLaunchArea(loading){ + if(loading){ + launch_details.style.display = 'flex' + launch_content.style.display = 'none' + } else { + launch_details.style.display = 'none' + launch_content.style.display = 'inline-flex' + } +} + +/** + * Set the details text of the loading area. + * + * @param {string} details The new text for the loading details. + */ +function setLaunchDetails(details){ + launch_details_text.innerHTML = details +} + +/** + * Set the value of the loading progress bar and display that value. + * + * @param {number} value The progress value. + * @param {number} max The total size. + * @param {number|string} percent Optional. The percentage to display on the progress label. + */ +function setLaunchPercentage(value, max, percent = ((value/max)*100)){ + launch_progress.setAttribute('max', max) + launch_progress.setAttribute('value', value) + launch_progress_label.innerHTML = percent + '%' +} + +/** + * Set the value of the OS progress bar and display that on the UI. + * + * @param {number} value The progress value. + * @param {number} max The total download size. + * @param {number|string} percent Optional. The percentage to display on the progress label. + */ +function setDownloadPercentage(value, max, percent = ((value/max)*100)){ + remote.getCurrentWindow().setProgressBar(value/max) + setLaunchPercentage(value, max, percent) +} + +/** + * Enable or disable the launch button. + * + * @param {boolean} val True to enable, false to disable. + */ +function setLaunchEnabled(val){ + document.getElementById('launch_button').disabled = !val +} + +// Bind launch button +document.getElementById('launch_button').addEventListener('click', function(e){ + loggerLanding.log('Launching game..') + const mcVersion = DistroManager.getDistribution().getServer(ConfigManager.getSelectedServer()).getMinecraftVersion() + const jExe = ConfigManager.getJavaExecutable() + if(jExe == null){ + asyncSystemScan(mcVersion) + } else { + + setLaunchDetails(Lang.queryJS('landing.launch.pleaseWait')) + toggleLaunchArea(true) + setLaunchPercentage(0, 100) + + const jg = new JavaGuard(mcVersion) + jg._validateJavaBinary(jExe).then((v) => { + loggerLanding.log('Java version meta', v) + if(v.valid){ + dlAsync() + } else { + asyncSystemScan(mcVersion) + } + }) + } +}) + +// Bind settings button +document.getElementById('settingsMediaButton').onclick = (e) => { + prepareSettings() + switchView(getCurrentView(), VIEWS.settings) +} + +// Bind avatar overlay button. +document.getElementById('avatarOverlay').onclick = (e) => { + prepareSettings() + switchView(getCurrentView(), VIEWS.settings, 500, 500, () => { + settingsNavItemListener(document.getElementById('settingsNavAccount'), false) + }) +} + +// Bind selected account +function updateSelectedAccount(authUser){ + let username = 'No Account Selected' + if(authUser != null){ + if(authUser.displayName != null){ + username = authUser.displayName + } + if(authUser.uuid != null){ + document.getElementById('avatarContainer').style.backgroundImage = `url('https://crafatar.com/renders/body/${authUser.uuid}')` + } + } + user_text.innerHTML = username +} +updateSelectedAccount(ConfigManager.getSelectedAccount()) + +// Bind selected server +function updateSelectedServer(serv){ + if(getCurrentView() === VIEWS.settings){ + saveAllModConfigurations() + } + ConfigManager.setSelectedServer(serv != null ? serv.getID() : null) + ConfigManager.save() + server_selection_button.innerHTML = '\u2022 ' + (serv != null ? serv.getName() : 'No Server Selected') + if(getCurrentView() === VIEWS.settings){ + animateModsTabRefresh() + } + setLaunchEnabled(serv != null) +} +// Real text is set in uibinder.js on distributionIndexDone. +server_selection_button.innerHTML = '\u2022 Loading..' +server_selection_button.onclick = (e) => { + e.target.blur() + toggleServerSelection(true) +} + +// Update Mojang Status Color +const refreshMojangStatuses = async function(){ + loggerLanding.log('Refreshing Mojang Statuses..') + + let status = 'grey' + let tooltipEssentialHTML = '' + let tooltipNonEssentialHTML = '' + + try { + const statuses = await Mojang.status() + greenCount = 0 + greyCount = 0 + + for(let i=0; i + + ${service.name} + ` + } else { + tooltipNonEssentialHTML += `
+ + ${service.name} +
` + } + + if(service.status === 'yellow' && status !== 'red'){ + status = 'yellow' + } else if(service.status === 'red'){ + status = 'red' + } else { + if(service.status === 'grey'){ + ++greyCount + } + ++greenCount + } + + } + + if(greenCount === statuses.length){ + if(greyCount === statuses.length){ + status = 'grey' + } else { + status = 'green' + } + } + + } catch (err) { + loggerLanding.warn('Unable to refresh Mojang service status.') + loggerLanding.debug(err) + } + + document.getElementById('mojangStatusEssentialContainer').innerHTML = tooltipEssentialHTML + document.getElementById('mojangStatusNonEssentialContainer').innerHTML = tooltipNonEssentialHTML + document.getElementById('mojang_status_icon').style.color = Mojang.statusToHex(status) +} + +const refreshServerStatus = async function(fade = false){ + loggerLanding.log('Refreshing Server Status') + const serv = DistroManager.getDistribution().getServer(ConfigManager.getSelectedServer()) + + let pLabel = 'SERVER' + let pVal = 'OFFLINE' + + try { + const serverURL = new URL('my://' + serv.getAddress()) + const servStat = await ServerStatus.getStatus(serverURL.hostname, serverURL.port) + if(servStat.online){ + pLabel = 'PLAYERS' + pVal = servStat.onlinePlayers + '/' + servStat.maxPlayers + } + + } catch (err) { + loggerLanding.warn('Unable to refresh server status, assuming offline.') + loggerLanding.debug(err) + } + if(fade){ + $('#server_status_wrapper').fadeOut(250, () => { + document.getElementById('landingPlayerLabel').innerHTML = pLabel + document.getElementById('player_count').innerHTML = pVal + $('#server_status_wrapper').fadeIn(500) + }) + } else { + document.getElementById('landingPlayerLabel').innerHTML = pLabel + document.getElementById('player_count').innerHTML = pVal + } + +} + +refreshMojangStatuses() +// Server Status is refreshed in uibinder.js on distributionIndexDone. + +// Set refresh rate to once every 5 minutes. +let mojangStatusListener = setInterval(() => refreshMojangStatuses(true), 300000) +let serverStatusListener = setInterval(() => refreshServerStatus(true), 300000) + +/** + * Shows an error overlay, toggles off the launch area. + * + * @param {string} title The overlay title. + * @param {string} desc The overlay description. + */ +function showLaunchFailure(title, desc){ + setOverlayContent( + title, + desc, + 'Okay' + ) + setOverlayHandler(null) + toggleOverlay(true) + toggleLaunchArea(false) +} + +/* System (Java) Scan */ + +let sysAEx +let scanAt + +let extractListener + +/** + * Asynchronously scan the system for valid Java installations. + * + * @param {string} mcVersion The Minecraft version we are scanning for. + * @param {boolean} launchAfter Whether we should begin to launch after scanning. + */ +function asyncSystemScan(mcVersion, launchAfter = true){ + + setLaunchDetails('Please wait..') + toggleLaunchArea(true) + setLaunchPercentage(0, 100) + + const loggerSysAEx = LoggerUtil('%c[SysAEx]', 'color: #353232; font-weight: bold') + + const forkEnv = JSON.parse(JSON.stringify(process.env)) + forkEnv.CONFIG_DIRECT_PATH = ConfigManager.getLauncherDirectory() + + // Fork a process to run validations. + sysAEx = cp.fork(path.join(__dirname, 'assets', 'js', 'assetexec.js'), [ + 'JavaGuard', + mcVersion + ], { + env: forkEnv, + stdio: 'pipe' + }) + // Stdout + sysAEx.stdio[1].setEncoding('utf8') + sysAEx.stdio[1].on('data', (data) => { + loggerSysAEx.log(data) + }) + // Stderr + sysAEx.stdio[2].setEncoding('utf8') + sysAEx.stdio[2].on('data', (data) => { + loggerSysAEx.log(data) + }) + + sysAEx.on('message', (m) => { + + if(m.context === 'validateJava'){ + if(m.result == null){ + // If the result is null, no valid Java installation was found. + // Show this information to the user. + setOverlayContent( + 'No Compatible
Java Installation Found', + 'In order to join NemesisMC, you need a 64-bit installation of Java 8. Would you like us to install a copy? By installing, you accept Oracle\'s license agreement.', + 'Install Java', + 'Install Manually' + ) + setOverlayHandler(() => { + setLaunchDetails('Preparing Java Download..') + sysAEx.send({task: 'changeContext', class: 'AssetGuard', args: [ConfigManager.getCommonDirectory(),ConfigManager.getJavaExecutable()]}) + sysAEx.send({task: 'execute', function: '_enqueueOpenJDK', argsArr: [ConfigManager.getDataDirectory()]}) + toggleOverlay(false) + }) + setDismissHandler(() => { + $('#overlayContent').fadeOut(250, () => { + //$('#overlayDismiss').toggle(false) + setOverlayContent( + 'Java is Required
to Launch', + 'A valid x64 installation of Java 8 is required to launch.

Please refer to our Java Management Guide for instructions on how to manually install Java.', + 'I Understand', + 'Go Back' + ) + setOverlayHandler(() => { + toggleLaunchArea(false) + toggleOverlay(false) + }) + setDismissHandler(() => { + toggleOverlay(false, true) + asyncSystemScan() + }) + $('#overlayContent').fadeIn(250) + }) + }) + toggleOverlay(true, true) + + } else { + // Java installation found, use this to launch the game. + ConfigManager.setJavaExecutable(m.result) + ConfigManager.save() + + // We need to make sure that the updated value is on the settings UI. + // Just incase the settings UI is already open. + settingsJavaExecVal.value = m.result + populateJavaExecDetails(settingsJavaExecVal.value) + + if(launchAfter){ + dlAsync() + } + sysAEx.disconnect() + } + } else if(m.context === '_enqueueOpenJDK'){ + + if(m.result === true){ + + // Oracle JRE enqueued successfully, begin download. + setLaunchDetails('Downloading Java..') + sysAEx.send({task: 'execute', function: 'processDlQueues', argsArr: [[{id:'java', limit:1}]]}) + + } else { + + // Oracle JRE enqueue failed. Probably due to a change in their website format. + // User will have to follow the guide to install Java. + setOverlayContent( + 'Unexpected Issue:
Java Download Failed', + 'Unfortunately we\'ve encountered an issue while attempting to install Java. You will need to manually install a copy. Please check out our Troubleshooting Guide for more details and instructions.', + 'I Understand' + ) + setOverlayHandler(() => { + toggleOverlay(false) + toggleLaunchArea(false) + }) + toggleOverlay(true) + sysAEx.disconnect() + + } + + } else if(m.context === 'progress'){ + + switch(m.data){ + case 'download': + // Downloading.. + setDownloadPercentage(m.value, m.total, m.percent) + break + } + + } else if(m.context === 'complete'){ + + switch(m.data){ + case 'download': { + // Show installing progress bar. + remote.getCurrentWindow().setProgressBar(2) + + // Wait for extration to complete. + const eLStr = 'Extracting' + let dotStr = '' + setLaunchDetails(eLStr) + extractListener = setInterval(() => { + if(dotStr.length >= 3){ + dotStr = '' + } else { + dotStr += '.' + } + setLaunchDetails(eLStr + dotStr) + }, 750) + break + } + case 'java': + // Download & extraction complete, remove the loading from the OS progress bar. + remote.getCurrentWindow().setProgressBar(-1) + + // Extraction completed successfully. + ConfigManager.setJavaExecutable(m.args[0]) + ConfigManager.save() + + if(extractListener != null){ + clearInterval(extractListener) + extractListener = null + } + + setLaunchDetails('Java Installed!') + + if(launchAfter){ + dlAsync() + } + + sysAEx.disconnect() + break + } + + } else if(m.context === 'error'){ + console.log(m.error) + } + }) + + // Begin system Java scan. + setLaunchDetails('Checking system info..') + sysAEx.send({task: 'execute', function: 'validateJava', argsArr: [ConfigManager.getDataDirectory()]}) + +} + +// Keep reference to Minecraft Process +let proc +// Is DiscordRPC enabled +let hasRPC = false +// Joined server regex +const SERVER_JOINED_REGEX = /\[.+\]: \[CHAT\] [a-zA-Z0-9_]{1,16} joined the game/ +const GAME_JOINED_REGEX = /\[.+\]: Skipping bad option: lastServer:/ +const GAME_LAUNCH_REGEX = /^\[.+\]: (?:MinecraftForge .+ Initialized|ModLauncher .+ starting: .+)$/ +const MIN_LINGER = 5000 + +let aEx +let serv +let versionData +let forgeData + +let progressListener + +function dlAsync(login = true){ + + // Login parameter is temporary for debug purposes. Allows testing the validation/downloads without + // launching the game. + + if(login) { + if(ConfigManager.getSelectedAccount() == null){ + loggerLanding.error('You must be logged into an account.') + return + } + } + + setLaunchDetails('Please wait..') + toggleLaunchArea(true) + setLaunchPercentage(0, 100) + + const loggerAEx = LoggerUtil('%c[AEx]', 'color: #353232; font-weight: bold') + const loggerLaunchSuite = LoggerUtil('%c[LaunchSuite]', 'color: #000668; font-weight: bold') + + const forkEnv = JSON.parse(JSON.stringify(process.env)) + forkEnv.CONFIG_DIRECT_PATH = ConfigManager.getLauncherDirectory() + + // Start AssetExec to run validations and downloads in a forked process. + aEx = cp.fork(path.join(__dirname, 'assets', 'js', 'assetexec.js'), [ + 'AssetGuard', + ConfigManager.getCommonDirectory(), + ConfigManager.getJavaExecutable() + ], { + env: forkEnv, + stdio: 'pipe' + }) + // Stdout + aEx.stdio[1].setEncoding('utf8') + aEx.stdio[1].on('data', (data) => { + loggerAEx.log(data) + }) + // Stderr + aEx.stdio[2].setEncoding('utf8') + aEx.stdio[2].on('data', (data) => { + loggerAEx.log(data) + }) + aEx.on('error', (err) => { + loggerLaunchSuite.error('Error during launch', err) + showLaunchFailure('Error During Launch', err.message || 'See console (CTRL + Shift + i) for more details.') + }) + aEx.on('close', (code, signal) => { + if(code !== 0){ + loggerLaunchSuite.error(`AssetExec exited with code ${code}, assuming error.`) + showLaunchFailure('Error During Launch', 'See console (CTRL + Shift + i) for more details.') + } + }) + + // Establish communications between the AssetExec and current process. + aEx.on('message', (m) => { + + if(m.context === 'validate'){ + switch(m.data){ + case 'distribution': + setLaunchPercentage(20, 100) + loggerLaunchSuite.log('Validated distibution index.') + setLaunchDetails('Loading version information..') + break + case 'version': + setLaunchPercentage(40, 100) + loggerLaunchSuite.log('Version data loaded.') + setLaunchDetails('Validating asset integrity..') + break + case 'assets': + setLaunchPercentage(60, 100) + loggerLaunchSuite.log('Asset Validation Complete') + setLaunchDetails('Validating library integrity..') + break + case 'libraries': + setLaunchPercentage(80, 100) + loggerLaunchSuite.log('Library validation complete.') + setLaunchDetails('Validating miscellaneous file integrity..') + break + case 'files': + setLaunchPercentage(100, 100) + loggerLaunchSuite.log('File validation complete.') + setLaunchDetails('Downloading files..') + break + } + } else if(m.context === 'progress'){ + switch(m.data){ + case 'assets': { + const perc = (m.value/m.total)*20 + setLaunchPercentage(40+perc, 100, parseInt(40+perc)) + break + } + case 'download': + setDownloadPercentage(m.value, m.total, m.percent) + break + case 'extract': { + // Show installing progress bar. + remote.getCurrentWindow().setProgressBar(2) + + // Download done, extracting. + const eLStr = 'Extracting libraries' + let dotStr = '' + setLaunchDetails(eLStr) + progressListener = setInterval(() => { + if(dotStr.length >= 3){ + dotStr = '' + } else { + dotStr += '.' + } + setLaunchDetails(eLStr + dotStr) + }, 750) + break + } + } + } else if(m.context === 'complete'){ + switch(m.data){ + case 'download': + // Download and extraction complete, remove the loading from the OS progress bar. + remote.getCurrentWindow().setProgressBar(-1) + if(progressListener != null){ + clearInterval(progressListener) + progressListener = null + } + + setLaunchDetails('Preparing to launch..') + break + } + } else if(m.context === 'error'){ + switch(m.data){ + case 'download': + loggerLaunchSuite.error('Error while downloading:', m.error) + + if(m.error.code === 'ENOENT'){ + showLaunchFailure( + 'Download Error', + 'Could not connect to the file server. Ensure that you are connected to the internet and try again.' + ) + } else { + showLaunchFailure( + 'Download Error', + 'Check the console (CTRL + Shift + i) for more details. Please try again.' + ) + } + + remote.getCurrentWindow().setProgressBar(-1) + + // Disconnect from AssetExec + aEx.disconnect() + break + } + } else if(m.context === 'validateEverything'){ + + let allGood = true + + // If these properties are not defined it's likely an error. + if(m.result.forgeData == null || m.result.versionData == null){ + loggerLaunchSuite.error('Error during validation:', m.result) + + loggerLaunchSuite.error('Error during launch', m.result.error) + showLaunchFailure('Error During Launch', 'Please check the console (CTRL + Shift + i) for more details.') + + allGood = false + } + + forgeData = m.result.forgeData + versionData = m.result.versionData + + if(login && allGood) { + const authUser = ConfigManager.getSelectedAccount() + loggerLaunchSuite.log(`Sending selected account (${authUser.displayName}) to ProcessBuilder.`) + let pb = new ProcessBuilder(serv, versionData, forgeData, authUser, remote.app.getVersion()) + setLaunchDetails('Launching game..') + + const onLoadComplete = () => { + toggleLaunchArea(false) + if(hasRPC){ + DiscordWrapper.updateDetails('Loading game..') + } + proc.stdout.on('data', gameStateChange) + proc.stdout.removeListener('data', tempListener) + proc.stderr.removeListener('data', gameErrorListener) + } + const start = Date.now() + + // Attach a temporary listener to the client output. + // Will wait for a certain bit of text meaning that + // the client application has started, and we can hide + // the progress bar stuff. + const tempListener = function(data){ + if(GAME_LAUNCH_REGEX.test(data.trim())){ + const diff = Date.now()-start + if(diff < MIN_LINGER) { + setTimeout(onLoadComplete, MIN_LINGER-diff) + } else { + onLoadComplete() + } + } + } + + // Listener for Discord RPC. + const gameStateChange = function(data){ + data = data.trim() + if(SERVER_JOINED_REGEX.test(data)){ + DiscordWrapper.updateDetails('Exploring the Network!') + } else if(GAME_JOINED_REGEX.test(data)){ + DiscordWrapper.updateDetails('Exploring the Network!') + } + } + + const gameErrorListener = function(data){ + data = data.trim() + if(data.indexOf('Could not find or load main class net.minecraft.launchwrapper.Launch') > -1){ + loggerLaunchSuite.error('Game launch failed, LaunchWrapper was not downloaded properly.') + showLaunchFailure('Error During Launch', 'The main file, LaunchWrapper, failed to download properly. As a result, the game cannot launch.

To fix this issue, temporarily turn off your antivirus software and launch the game again.

If you have time, please submit an issue and let us know what antivirus software you use. We\'ll contact them and try to straighten things out.') + } + } + + try { + // Build Minecraft process. + proc = pb.build() + + // Bind listeners to stdout. + proc.stdout.on('data', tempListener) + proc.stderr.on('data', gameErrorListener) + + setLaunchDetails('Done. Enjoy the server!') + + // Init Discord Hook + const distro = DistroManager.getDistribution() + if(distro.discord != null && serv.discord != null){ + DiscordWrapper.initRPC(distro.discord, serv.discord) + hasRPC = true + proc.on('close', (code, signal) => { + loggerLaunchSuite.log('Shutting down Discord Rich Presence..') + DiscordWrapper.shutdownRPC() + hasRPC = false + proc = null + }) + } + + } catch(err) { + + loggerLaunchSuite.error('Error during launch', err) + showLaunchFailure('Error During Launch', 'Please check the console (CTRL + Shift + i) for more details.') + + } + } + + // Disconnect from AssetExec + aEx.disconnect() + + } + }) + + // Begin Validations + + // Validate Forge files. + setLaunchDetails('Loading server information..') + + refreshDistributionIndex(true, (data) => { + onDistroRefresh(data) + serv = data.getServer(ConfigManager.getSelectedServer()) + aEx.send({task: 'execute', function: 'validateEverything', argsArr: [ConfigManager.getSelectedServer(), DistroManager.isDevMode()]}) + }, (err) => { + loggerLaunchSuite.log('Error while fetching a fresh copy of the distribution index.', err) + refreshDistributionIndex(false, (data) => { + onDistroRefresh(data) + serv = data.getServer(ConfigManager.getSelectedServer()) + aEx.send({task: 'execute', function: 'validateEverything', argsArr: [ConfigManager.getSelectedServer(), DistroManager.isDevMode()]}) + }, (err) => { + loggerLaunchSuite.error('Unable to refresh distribution index.', err) + if(DistroManager.getDistribution() == null){ + showLaunchFailure('Fatal Error', 'Could not load a copy of the distribution index. See the console (CTRL + Shift + i) for more details.') + + // Disconnect from AssetExec + aEx.disconnect() + } else { + serv = data.getServer(ConfigManager.getSelectedServer()) + aEx.send({task: 'execute', function: 'validateEverything', argsArr: [ConfigManager.getSelectedServer(), DistroManager.isDevMode()]}) + } + }) + }) +} + +/** + * News Loading Functions + */ + +// DOM Cache +const newsContent = document.getElementById('newsContent') +const newsArticleTitle = document.getElementById('newsArticleTitle') +const newsArticleDate = document.getElementById('newsArticleDate') +const newsArticleAuthor = document.getElementById('newsArticleAuthor') +const newsArticleComments = document.getElementById('newsArticleComments') +const newsNavigationStatus = document.getElementById('newsNavigationStatus') +const newsArticleContentScrollable = document.getElementById('newsArticleContentScrollable') +const nELoadSpan = document.getElementById('nELoadSpan') + +// News slide caches. +let newsActive = false +let newsGlideCount = 0 + +/** + * Show the news UI via a slide animation. + * + * @param {boolean} up True to slide up, otherwise false. + */ +function slide_(up){ + const lCUpper = document.querySelector('#landingContainer > #upper') + const lCLLeft = document.querySelector('#landingContainer > #lower > #left') + const lCLCenter = document.querySelector('#landingContainer > #lower > #center') + const lCLRight = document.querySelector('#landingContainer > #lower > #right') + const newsBtn = document.querySelector('#landingContainer > #lower > #center #content') + const landingContainer = document.getElementById('landingContainer') + const newsContainer = document.querySelector('#landingContainer > #newsContainer') + + newsGlideCount++ + + if(up){ + lCUpper.style.top = '-200vh' + lCLLeft.style.top = '-200vh' + lCLCenter.style.top = '-200vh' + lCLRight.style.top = '-200vh' + newsBtn.style.top = '130vh' + newsContainer.style.top = '0px' + //date.toLocaleDateString('en-US', {month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: 'numeric'}) + //landingContainer.style.background = 'rgba(29, 29, 29, 0.55)' + landingContainer.style.background = 'rgba(0, 0, 0, 0.50)' + setTimeout(() => { + if(newsGlideCount === 1){ + lCLCenter.style.transition = 'none' + newsBtn.style.transition = 'none' + } + newsGlideCount-- + }, 2000) + } else { + setTimeout(() => { + newsGlideCount-- + }, 2000) + landingContainer.style.background = null + lCLCenter.style.transition = null + newsBtn.style.transition = null + newsContainer.style.top = '100%' + lCUpper.style.top = '0px' + lCLLeft.style.top = '0px' + lCLCenter.style.top = '0px' + lCLRight.style.top = '0px' + newsBtn.style.top = '10px' + } +} + +// Bind news button. +document.getElementById('newsButton').onclick = () => { + // Toggle tabbing. + if(newsActive){ + $('#landingContainer *').removeAttr('tabindex') + $('#newsContainer *').attr('tabindex', '-1') + } else { + $('#landingContainer *').attr('tabindex', '-1') + $('#newsContainer, #newsContainer *, #lower, #lower #center *').removeAttr('tabindex') + if(newsAlertShown){ + $('#newsButtonAlert').fadeOut(2000) + newsAlertShown = false + ConfigManager.setNewsCacheDismissed(true) + ConfigManager.save() + } + } + slide_(!newsActive) + newsActive = !newsActive +} + +// Array to store article meta. +let newsArr = null + +// News load animation listener. +let newsLoadingListener = null + +/** + * Set the news loading animation. + * + * @param {boolean} val True to set loading animation, otherwise false. + */ +function setNewsLoading(val){ + if(val){ + const nLStr = 'Checking for News' + let dotStr = '..' + nELoadSpan.innerHTML = nLStr + dotStr + newsLoadingListener = setInterval(() => { + if(dotStr.length >= 3){ + dotStr = '' + } else { + dotStr += '.' + } + nELoadSpan.innerHTML = nLStr + dotStr + }, 750) + } else { + if(newsLoadingListener != null){ + clearInterval(newsLoadingListener) + newsLoadingListener = null + } + } +} + +// Bind retry button. +newsErrorRetry.onclick = () => { + $('#newsErrorFailed').fadeOut(250, () => { + initNews() + $('#newsErrorLoading').fadeIn(250) + }) +} + +newsArticleContentScrollable.onscroll = (e) => { + if(e.target.scrollTop > Number.parseFloat($('.newsArticleSpacerTop').css('height'))){ + newsContent.setAttribute('scrolled', '') + } else { + newsContent.removeAttribute('scrolled') + } +} + +/** + * Reload the news without restarting. + * + * @returns {Promise.} A promise which resolves when the news + * content has finished loading and transitioning. + */ +function reloadNews(){ + return new Promise((resolve, reject) => { + $('#newsContent').fadeOut(250, () => { + $('#newsErrorLoading').fadeIn(250) + initNews().then(() => { + resolve() + }) + }) + }) +} + +let newsAlertShown = false + +/** + * Show the news alert indicating there is new news. + */ +function showNewsAlert(){ + newsAlertShown = true + $(newsButtonAlert).fadeIn(250) +} + +/** + * Initialize News UI. This will load the news and prepare + * the UI accordingly. + * + * @returns {Promise.} A promise which resolves when the news + * content has finished loading and transitioning. + */ +function initNews(){ + + return new Promise((resolve, reject) => { + setNewsLoading(true) + + let news = {} + loadNews().then(news => { + + newsArr = news.articles || null + + if(newsArr == null){ + // News Loading Failed + setNewsLoading(false) + + $('#newsErrorLoading').fadeOut(250, () => { + $('#newsErrorFailed').fadeIn(250, () => { + resolve() + }) + }) + } else if(newsArr.length === 0) { + // No News Articles + setNewsLoading(false) + + ConfigManager.setNewsCache({ + date: null, + content: null, + dismissed: false + }) + ConfigManager.save() + + $('#newsErrorLoading').fadeOut(250, () => { + $('#newsErrorNone').fadeIn(250, () => { + resolve() + }) + }) + } else { + // Success + setNewsLoading(false) + + const lN = newsArr[0] + const cached = ConfigManager.getNewsCache() + let newHash = crypto.createHash('sha1').update(lN.content).digest('hex') + let newDate = new Date(lN.date) + let isNew = false + + if(cached.date != null && cached.content != null){ + + if(new Date(cached.date) >= newDate){ + + // Compare Content + if(cached.content !== newHash){ + isNew = true + showNewsAlert() + } else { + if(!cached.dismissed){ + isNew = true + showNewsAlert() + } + } + + } else { + isNew = true + showNewsAlert() + } + + } else { + isNew = true + showNewsAlert() + } + + if(isNew){ + ConfigManager.setNewsCache({ + date: newDate.getTime(), + content: newHash, + dismissed: false + }) + ConfigManager.save() + } + + const switchHandler = (forward) => { + let cArt = parseInt(newsContent.getAttribute('article')) + let nxtArt = forward ? (cArt >= newsArr.length-1 ? 0 : cArt + 1) : (cArt <= 0 ? newsArr.length-1 : cArt - 1) + + displayArticle(newsArr[nxtArt], nxtArt+1) + } + + document.getElementById('newsNavigateRight').onclick = () => { switchHandler(true) } + document.getElementById('newsNavigateLeft').onclick = () => { switchHandler(false) } + + $('#newsErrorContainer').fadeOut(250, () => { + displayArticle(newsArr[0], 1) + $('#newsContent').fadeIn(250, () => { + resolve() + }) + }) + } + + }) + + }) +} + +/** + * Add keyboard controls to the news UI. Left and right arrows toggle + * between articles. If you are on the landing page, the up arrow will + * open the news UI. + */ +document.addEventListener('keydown', (e) => { + if(newsActive){ + if(e.key === 'ArrowRight' || e.key === 'ArrowLeft'){ + document.getElementById(e.key === 'ArrowRight' ? 'newsNavigateRight' : 'newsNavigateLeft').click() + } + // Interferes with scrolling an article using the down arrow. + // Not sure of a straight forward solution at this point. + // if(e.key === 'ArrowDown'){ + // document.getElementById('newsButton').click() + // } + } else { + if(getCurrentView() === VIEWS.landing){ + if(e.key === 'ArrowUp'){ + document.getElementById('newsButton').click() + } + } + } +}) + +/** + * Display a news article on the UI. + * + * @param {Object} articleObject The article meta object. + * @param {number} index The article index. + */ +function displayArticle(articleObject, index){ + newsArticleTitle.innerHTML = articleObject.title + newsArticleTitle.href = articleObject.link + newsArticleAuthor.innerHTML = 'by ' + articleObject.author + newsArticleDate.innerHTML = articleObject.date + newsArticleComments.innerHTML = articleObject.comments + newsArticleComments.href = articleObject.commentsLink + newsArticleContentScrollable.innerHTML = '
' + articleObject.content + '
' + Array.from(newsArticleContentScrollable.getElementsByClassName('bbCodeSpoilerButton')).forEach(v => { + v.onclick = () => { + const text = v.parentElement.getElementsByClassName('bbCodeSpoilerText')[0] + text.style.display = text.style.display === 'block' ? 'none' : 'block' + } + }) + newsNavigationStatus.innerHTML = index + ' of ' + newsArr.length + newsContent.setAttribute('article', index-1) +} + +/** + * Load news information from the RSS feed specified in the + * distribution index. + */ +function loadNews(){ + return new Promise((resolve, reject) => { + const distroData = DistroManager.getDistribution() + const newsFeed = distroData.getRSS() + const newsHost = new URL(newsFeed).origin + '/' + $.ajax({ + url: newsFeed, + success: (data) => { + const items = $(data).find('item') + const articles = [] + + for(let i=0; i { + resolve({ + articles: null + }) + }) + }) } \ No newline at end of file diff --git a/app/assets/js/scripts/login.js b/app/assets/js/scripts/login.js index 34078bd1..06f7425b 100644 --- a/app/assets/js/scripts/login.js +++ b/app/assets/js/scripts/login.js @@ -1,300 +1,300 @@ -/** - * Script for login.ejs - */ -// Validation Regexes. -const validUsername = /^[a-zA-Z0-9_]{1,16}$/ -const basicEmail = /^\S+@\S+\.\S+$/ -//const validEmail = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i - -// Login Elements -const loginCancelContainer = document.getElementById('loginCancelContainer') -const loginCancelButton = document.getElementById('loginCancelButton') -const loginEmailError = document.getElementById('loginEmailError') -const loginUsername = document.getElementById('loginUsername') -const loginPasswordError = document.getElementById('loginPasswordError') -const loginPassword = document.getElementById('loginPassword') -const checkmarkContainer = document.getElementById('checkmarkContainer') -const loginRememberOption = document.getElementById('loginRememberOption') -const loginButton = document.getElementById('loginButton') -const loginForm = document.getElementById('loginForm') - -// Control variables. -let lu = false, lp = false - -const loggerLogin = LoggerUtil('%c[Login]', 'color: #000668; font-weight: bold') - - -/** - * Show a login error. - * - * @param {HTMLElement} element The element on which to display the error. - * @param {string} value The error text. - */ -function showError(element, value){ - element.innerHTML = value - element.style.opacity = 1 -} - -/** - * Shake a login error to add emphasis. - * - * @param {HTMLElement} element The element to shake. - */ -function shakeError(element){ - if(element.style.opacity == 1){ - element.classList.remove('shake') - void element.offsetWidth - element.classList.add('shake') - } -} - -/** - * Validate that an email field is neither empty nor invalid. - * - * @param {string} value The email value. - */ -function validateEmail(value){ - if(value){ - if(!basicEmail.test(value) && !validUsername.test(value)){ - showError(loginEmailError, Lang.queryJS('login.error.invalidValue')) - loginDisabled(true) - lu = false - } else { - loginEmailError.style.opacity = 0 - lu = true - if(lp){ - loginDisabled(false) - } - } - } else { - lu = false - showError(loginEmailError, Lang.queryJS('login.error.requiredValue')) - loginDisabled(true) - } -} - -/** - * Validate that the password field is not empty. - * - * @param {string} value The password value. - */ -function validatePassword(value){ - if(value){ - loginPasswordError.style.opacity = 0 - lp = true - if(lu){ - loginDisabled(false) - } - } else { - lp = false - showError(loginPasswordError, Lang.queryJS('login.error.invalidValue')) - loginDisabled(true) - } -} - -// Emphasize errors with shake when focus is lost. -loginUsername.addEventListener('focusout', (e) => { - validateEmail(e.target.value) - shakeError(loginEmailError) -}) -loginPassword.addEventListener('focusout', (e) => { - validatePassword(e.target.value) - shakeError(loginPasswordError) -}) - -// Validate input for each field. -loginUsername.addEventListener('input', (e) => { - validateEmail(e.target.value) -}) -loginPassword.addEventListener('input', (e) => { - validatePassword(e.target.value) -}) - -/** - * Enable or disable the login button. - * - * @param {boolean} v True to enable, false to disable. - */ -function loginDisabled(v){ - if(loginButton.disabled !== v){ - loginButton.disabled = v - } -} - -/** - * Enable or disable loading elements. - * - * @param {boolean} v True to enable, false to disable. - */ -function loginLoading(v){ - if(v){ - loginButton.setAttribute('loading', v) - loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.login'), Lang.queryJS('login.loggingIn')) - } else { - loginButton.removeAttribute('loading') - loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.loggingIn'), Lang.queryJS('login.login')) - } -} - -/** - * Enable or disable login form. - * - * @param {boolean} v True to enable, false to disable. - */ -function formDisabled(v){ - loginDisabled(v) - loginCancelButton.disabled = v - loginUsername.disabled = v - loginPassword.disabled = v - if(v){ - checkmarkContainer.setAttribute('disabled', v) - } else { - checkmarkContainer.removeAttribute('disabled') - } - loginRememberOption.disabled = v -} - -/** - * Parses an error and returns a user-friendly title and description - * for our error overlay. - * - * @param {Error | {cause: string, error: string, errorMessage: string}} err A Node.js - * error or Mojang error response. - */ -function resolveError(err){ - // Mojang Response => err.cause | err.error | err.errorMessage - // Node error => err.code | err.message - if(err.cause != null && err.cause === 'UserMigratedException') { - return { - title: Lang.queryJS('login.error.userMigrated.title'), - desc: Lang.queryJS('login.error.userMigrated.desc') - } - } else { - if(err.error != null){ - if(err.error === 'ForbiddenOperationException'){ - if(err.errorMessage != null){ - if(err.errorMessage === 'Invalid credentials. Invalid username or password.'){ - return { - title: Lang.queryJS('login.error.invalidCredentials.title'), - desc: Lang.queryJS('login.error.invalidCredentials.desc') - } - } else if(err.errorMessage === 'Invalid credentials.'){ - return { - title: Lang.queryJS('login.error.rateLimit.title'), - desc: Lang.queryJS('login.error.rateLimit.desc') - } - } - } - } - } else { - // Request errors (from Node). - if(err.code != null){ - if(err.code === 'ENOENT'){ - // No Internet. - return { - title: Lang.queryJS('login.error.noInternet.title'), - desc: Lang.queryJS('login.error.noInternet.desc') - } - } else if(err.code === 'ENOTFOUND'){ - // Could not reach server. - return { - title: Lang.queryJS('login.error.authDown.title'), - desc: Lang.queryJS('login.error.authDown.desc') - } - } - } - } - } - if(err.message != null){ - if(err.message === 'NotPaidAccount'){ - return { - title: Lang.queryJS('login.error.notPaid.title'), - desc: Lang.queryJS('login.error.notPaid.desc') - } - } else { - // Unknown error with request. - return { - title: Lang.queryJS('login.error.unknown.title'), - desc: err.message - } - } - } else { - // Unknown Mojang error. - return { - title: err.error, - desc: err.errorMessage - } - } -} - -let loginViewOnSuccess = VIEWS.landing -let loginViewOnCancel = VIEWS.settings -let loginViewCancelHandler - -function loginCancelEnabled(val){ - if(val){ - $(loginCancelContainer).show() - } else { - $(loginCancelContainer).hide() - } -} - -loginCancelButton.onclick = (e) => { - switchView(getCurrentView(), loginViewOnCancel, 500, 500, () => { - loginUsername.value = '' - loginPassword.value = '' - loginCancelEnabled(false) - if(loginViewCancelHandler != null){ - loginViewCancelHandler() - loginViewCancelHandler = null - } - }) -} - -// Disable default form behavior. -loginForm.onsubmit = () => { return false } - -// Bind login button behavior. -loginButton.addEventListener('click', () => { - // Disable form. - formDisabled(true) - - // Show loading stuff. - loginLoading(true) - - AuthManager.addAccount(loginUsername.value, loginPassword.value).then((value) => { - updateSelectedAccount(value) - loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.loggingIn'), Lang.queryJS('login.success')) - $('.circle-loader').toggleClass('load-complete') - $('.checkmark').toggle() - setTimeout(() => { - switchView(VIEWS.login, loginViewOnSuccess, 500, 500, () => { - // Temporary workaround - if(loginViewOnSuccess === VIEWS.settings){ - prepareSettings() - } - loginViewOnSuccess = VIEWS.landing // Reset this for good measure. - loginCancelEnabled(false) // Reset this for good measure. - loginViewCancelHandler = null // Reset this for good measure. - loginUsername.value = '' - loginPassword.value = '' - $('.circle-loader').toggleClass('load-complete') - $('.checkmark').toggle() - loginLoading(false) - loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.success'), Lang.queryJS('login.login')) - formDisabled(false) - }) - }, 1000) - }).catch((err) => { - loginLoading(false) - const errF = resolveError(err) - setOverlayContent(errF.title, errF.desc, Lang.queryJS('login.tryAgain')) - setOverlayHandler(() => { - formDisabled(false) - toggleOverlay(false) - }) - toggleOverlay(true) - loggerLogin.log('Error while logging in.', err) - }) - +/** + * Script for login.ejs + */ +// Validation Regexes. +const validUsername = /^[a-zA-Z0-9_]{1,16}$/ +const basicEmail = /^\S+@\S+\.\S+$/ +//const validEmail = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i + +// Login Elements +const loginCancelContainer = document.getElementById('loginCancelContainer') +const loginCancelButton = document.getElementById('loginCancelButton') +const loginEmailError = document.getElementById('loginEmailError') +const loginUsername = document.getElementById('loginUsername') +const loginPasswordError = document.getElementById('loginPasswordError') +const loginPassword = document.getElementById('loginPassword') +const checkmarkContainer = document.getElementById('checkmarkContainer') +const loginRememberOption = document.getElementById('loginRememberOption') +const loginButton = document.getElementById('loginButton') +const loginForm = document.getElementById('loginForm') + +// Control variables. +let lu = false, lp = false + +const loggerLogin = LoggerUtil('%c[Login]', 'color: #000668; font-weight: bold') + + +/** + * Show a login error. + * + * @param {HTMLElement} element The element on which to display the error. + * @param {string} value The error text. + */ +function showError(element, value){ + element.innerHTML = value + element.style.opacity = 1 +} + +/** + * Shake a login error to add emphasis. + * + * @param {HTMLElement} element The element to shake. + */ +function shakeError(element){ + if(element.style.opacity == 1){ + element.classList.remove('shake') + void element.offsetWidth + element.classList.add('shake') + } +} + +/** + * Validate that an email field is neither empty nor invalid. + * + * @param {string} value The email value. + */ +function validateEmail(value){ + if(value){ + if(!basicEmail.test(value) && !validUsername.test(value)){ + showError(loginEmailError, Lang.queryJS('login.error.invalidValue')) + loginDisabled(true) + lu = false + } else { + loginEmailError.style.opacity = 0 + lu = true + if(lp){ + loginDisabled(false) + } + } + } else { + lu = false + showError(loginEmailError, Lang.queryJS('login.error.requiredValue')) + loginDisabled(true) + } +} + +/** + * Validate that the password field is not empty. + * + * @param {string} value The password value. + */ +function validatePassword(value){ + if(value){ + loginPasswordError.style.opacity = 0 + lp = true + if(lu){ + loginDisabled(false) + } + } else { + lp = false + showError(loginPasswordError, Lang.queryJS('login.error.invalidValue')) + loginDisabled(true) + } +} + +// Emphasize errors with shake when focus is lost. +loginUsername.addEventListener('focusout', (e) => { + validateEmail(e.target.value) + shakeError(loginEmailError) +}) +loginPassword.addEventListener('focusout', (e) => { + validatePassword(e.target.value) + shakeError(loginPasswordError) +}) + +// Validate input for each field. +loginUsername.addEventListener('input', (e) => { + validateEmail(e.target.value) +}) +loginPassword.addEventListener('input', (e) => { + validatePassword(e.target.value) +}) + +/** + * Enable or disable the login button. + * + * @param {boolean} v True to enable, false to disable. + */ +function loginDisabled(v){ + if(loginButton.disabled !== v){ + loginButton.disabled = v + } +} + +/** + * Enable or disable loading elements. + * + * @param {boolean} v True to enable, false to disable. + */ +function loginLoading(v){ + if(v){ + loginButton.setAttribute('loading', v) + loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.login'), Lang.queryJS('login.loggingIn')) + } else { + loginButton.removeAttribute('loading') + loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.loggingIn'), Lang.queryJS('login.login')) + } +} + +/** + * Enable or disable login form. + * + * @param {boolean} v True to enable, false to disable. + */ +function formDisabled(v){ + loginDisabled(v) + loginCancelButton.disabled = v + loginUsername.disabled = v + loginPassword.disabled = v + if(v){ + checkmarkContainer.setAttribute('disabled', v) + } else { + checkmarkContainer.removeAttribute('disabled') + } + loginRememberOption.disabled = v +} + +/** + * Parses an error and returns a user-friendly title and description + * for our error overlay. + * + * @param {Error | {cause: string, error: string, errorMessage: string}} err A Node.js + * error or Mojang error response. + */ +function resolveError(err){ + // Mojang Response => err.cause | err.error | err.errorMessage + // Node error => err.code | err.message + if(err.cause != null && err.cause === 'UserMigratedException') { + return { + title: Lang.queryJS('login.error.userMigrated.title'), + desc: Lang.queryJS('login.error.userMigrated.desc') + } + } else { + if(err.error != null){ + if(err.error === 'ForbiddenOperationException'){ + if(err.errorMessage != null){ + if(err.errorMessage === 'Invalid credentials. Invalid username or password.'){ + return { + title: Lang.queryJS('login.error.invalidCredentials.title'), + desc: Lang.queryJS('login.error.invalidCredentials.desc') + } + } else if(err.errorMessage === 'Invalid credentials.'){ + return { + title: Lang.queryJS('login.error.rateLimit.title'), + desc: Lang.queryJS('login.error.rateLimit.desc') + } + } + } + } + } else { + // Request errors (from Node). + if(err.code != null){ + if(err.code === 'ENOENT'){ + // No Internet. + return { + title: Lang.queryJS('login.error.noInternet.title'), + desc: Lang.queryJS('login.error.noInternet.desc') + } + } else if(err.code === 'ENOTFOUND'){ + // Could not reach server. + return { + title: Lang.queryJS('login.error.authDown.title'), + desc: Lang.queryJS('login.error.authDown.desc') + } + } + } + } + } + if(err.message != null){ + if(err.message === 'NotPaidAccount'){ + return { + title: Lang.queryJS('login.error.notPaid.title'), + desc: Lang.queryJS('login.error.notPaid.desc') + } + } else { + // Unknown error with request. + return { + title: Lang.queryJS('login.error.unknown.title'), + desc: err.message + } + } + } else { + // Unknown Mojang error. + return { + title: err.error, + desc: err.errorMessage + } + } +} + +let loginViewOnSuccess = VIEWS.landing +let loginViewOnCancel = VIEWS.settings +let loginViewCancelHandler + +function loginCancelEnabled(val){ + if(val){ + $(loginCancelContainer).show() + } else { + $(loginCancelContainer).hide() + } +} + +loginCancelButton.onclick = (e) => { + switchView(getCurrentView(), loginViewOnCancel, 500, 500, () => { + loginUsername.value = '' + loginPassword.value = '' + loginCancelEnabled(false) + if(loginViewCancelHandler != null){ + loginViewCancelHandler() + loginViewCancelHandler = null + } + }) +} + +// Disable default form behavior. +loginForm.onsubmit = () => { return false } + +// Bind login button behavior. +loginButton.addEventListener('click', () => { + // Disable form. + formDisabled(true) + + // Show loading stuff. + loginLoading(true) + + AuthManager.addAccount(loginUsername.value, loginPassword.value).then((value) => { + updateSelectedAccount(value) + loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.loggingIn'), Lang.queryJS('login.success')) + $('.circle-loader').toggleClass('load-complete') + $('.checkmark').toggle() + setTimeout(() => { + switchView(VIEWS.login, loginViewOnSuccess, 500, 500, () => { + // Temporary workaround + if(loginViewOnSuccess === VIEWS.settings){ + prepareSettings() + } + loginViewOnSuccess = VIEWS.landing // Reset this for good measure. + loginCancelEnabled(false) // Reset this for good measure. + loginViewCancelHandler = null // Reset this for good measure. + loginUsername.value = '' + loginPassword.value = '' + $('.circle-loader').toggleClass('load-complete') + $('.checkmark').toggle() + loginLoading(false) + loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.success'), Lang.queryJS('login.login')) + formDisabled(false) + }) + }, 1000) + }).catch((err) => { + loginLoading(false) + const errF = resolveError(err) + setOverlayContent(errF.title, errF.desc, Lang.queryJS('login.tryAgain')) + setOverlayHandler(() => { + formDisabled(false) + toggleOverlay(false) + }) + toggleOverlay(true) + loggerLogin.log('Error while logging in.', err) + }) + }) \ No newline at end of file diff --git a/app/assets/js/scripts/overlay.js b/app/assets/js/scripts/overlay.js index ed704047..df452d76 100644 --- a/app/assets/js/scripts/overlay.js +++ b/app/assets/js/scripts/overlay.js @@ -1,318 +1,318 @@ -/** - * Script for overlay.ejs - */ - -/* Overlay Wrapper Functions */ - -/** - * Check to see if the overlay is visible. - * - * @returns {boolean} Whether or not the overlay is visible. - */ -function isOverlayVisible(){ - return document.getElementById('main').hasAttribute('overlay') -} - -let overlayHandlerContent - -/** - * Overlay keydown handler for a non-dismissable overlay. - * - * @param {KeyboardEvent} e The keydown event. - */ -function overlayKeyHandler (e){ - if(e.key === 'Enter' || e.key === 'Escape'){ - document.getElementById(overlayHandlerContent).getElementsByClassName('overlayKeybindEnter')[0].click() - } -} -/** - * Overlay keydown handler for a dismissable overlay. - * - * @param {KeyboardEvent} e The keydown event. - */ -function overlayKeyDismissableHandler (e){ - if(e.key === 'Enter'){ - document.getElementById(overlayHandlerContent).getElementsByClassName('overlayKeybindEnter')[0].click() - } else if(e.key === 'Escape'){ - document.getElementById(overlayHandlerContent).getElementsByClassName('overlayKeybindEsc')[0].click() - } -} - -/** - * Bind overlay keydown listeners for escape and exit. - * - * @param {boolean} state Whether or not to add new event listeners. - * @param {string} content The overlay content which will be shown. - * @param {boolean} dismissable Whether or not the overlay is dismissable - */ -function bindOverlayKeys(state, content, dismissable){ - overlayHandlerContent = content - document.removeEventListener('keydown', overlayKeyHandler) - document.removeEventListener('keydown', overlayKeyDismissableHandler) - if(state){ - if(dismissable){ - document.addEventListener('keydown', overlayKeyDismissableHandler) - } else { - document.addEventListener('keydown', overlayKeyHandler) - } - } -} - -/** - * Toggle the visibility of the overlay. - * - * @param {boolean} toggleState True to display, false to hide. - * @param {boolean} dismissable Optional. True to show the dismiss option, otherwise false. - * @param {string} content Optional. The content div to be shown. - */ -function toggleOverlay(toggleState, dismissable = false, content = 'overlayContent'){ - if(toggleState == null){ - toggleState = !document.getElementById('main').hasAttribute('overlay') - } - if(typeof dismissable === 'string'){ - content = dismissable - dismissable = false - } - bindOverlayKeys(toggleState, content, dismissable) - if(toggleState){ - document.getElementById('main').setAttribute('overlay', true) - // Make things untabbable. - $('#main *').attr('tabindex', '-1') - $('#' + content).parent().children().hide() - $('#' + content).show() - if(dismissable){ - $('#overlayDismiss').show() - } else { - $('#overlayDismiss').hide() - } - $('#overlayContainer').fadeIn({ - duration: 250, - start: () => { - if(getCurrentView() === VIEWS.settings){ - document.getElementById('settingsContainer').style.backgroundColor = 'transparent' - } - } - }) - } else { - document.getElementById('main').removeAttribute('overlay') - // Make things tabbable. - $('#main *').removeAttr('tabindex') - $('#overlayContainer').fadeOut({ - duration: 250, - start: () => { - if(getCurrentView() === VIEWS.settings){ - document.getElementById('settingsContainer').style.backgroundColor = 'rgba(0, 0, 0, 0.50)' - } - }, - complete: () => { - $('#' + content).parent().children().hide() - $('#' + content).show() - if(dismissable){ - $('#overlayDismiss').show() - } else { - $('#overlayDismiss').hide() - } - } - }) - } -} - -function toggleServerSelection(toggleState){ - prepareServerSelectionList() - toggleOverlay(toggleState, true, 'serverSelectContent') -} - -/** - * Set the content of the overlay. - * - * @param {string} title Overlay title text. - * @param {string} description Overlay description text. - * @param {string} acknowledge Acknowledge button text. - * @param {string} dismiss Dismiss button text. - */ -function setOverlayContent(title, description, acknowledge, dismiss = 'Dismiss'){ - document.getElementById('overlayTitle').innerHTML = title - document.getElementById('overlayDesc').innerHTML = description - document.getElementById('overlayAcknowledge').innerHTML = acknowledge - document.getElementById('overlayDismiss').innerHTML = dismiss -} - -/** - * Set the onclick handler of the overlay acknowledge button. - * If the handler is null, a default handler will be added. - * - * @param {function} handler - */ -function setOverlayHandler(handler){ - if(handler == null){ - document.getElementById('overlayAcknowledge').onclick = () => { - toggleOverlay(false) - } - } else { - document.getElementById('overlayAcknowledge').onclick = handler - } -} - -/** - * Set the onclick handler of the overlay dismiss button. - * If the handler is null, a default handler will be added. - * - * @param {function} handler - */ -function setDismissHandler(handler){ - if(handler == null){ - document.getElementById('overlayDismiss').onclick = () => { - toggleOverlay(false) - } - } else { - document.getElementById('overlayDismiss').onclick = handler - } -} - -/* Server Select View */ - -document.getElementById('serverSelectConfirm').addEventListener('click', () => { - const listings = document.getElementsByClassName('serverListing') - for(let i=0; i 0){ - const serv = DistroManager.getDistribution().getServer(listings[i].getAttribute('servid')) - updateSelectedServer(serv) - toggleOverlay(false) - } -}) - -document.getElementById('accountSelectConfirm').addEventListener('click', () => { - const listings = document.getElementsByClassName('accountListing') - for(let i=0; i 0){ - const authAcc = ConfigManager.setSelectedAccount(listings[0].getAttribute('uuid')) - ConfigManager.save() - updateSelectedAccount(authAcc) - toggleOverlay(false) - validateSelectedAccount() - } -}) - -// Bind server select cancel button. -document.getElementById('serverSelectCancel').addEventListener('click', () => { - toggleOverlay(false) -}) - -document.getElementById('accountSelectCancel').addEventListener('click', () => { - $('#accountSelectContent').fadeOut(250, () => { - $('#overlayContent').fadeIn(250) - }) -}) - -function setServerListingHandlers(){ - const listings = Array.from(document.getElementsByClassName('serverListing')) - listings.map((val) => { - val.onclick = e => { - if(val.hasAttribute('selected')){ - return - } - const cListings = document.getElementsByClassName('serverListing') - for(let i=0; i { - val.onclick = e => { - if(val.hasAttribute('selected')){ - return - } - const cListings = document.getElementsByClassName('accountListing') - for(let i=0; i - -
- ${serv.getName()} - ${serv.getDescription()} -
-
${serv.getMinecraftVersion()}
-
${serv.getVersion()}
- ${serv.isMainServer() ? `
- - - - - - - - Main Server -
` : ''} -
-
- ` - } - document.getElementById('serverSelectListScrollable').innerHTML = htmlString - -} - -function populateAccountListings(){ - const accountsObj = ConfigManager.getAuthAccounts() - const accounts = Array.from(Object.keys(accountsObj), v=>accountsObj[v]) - let htmlString = '' - for(let i=0; i - -
${accounts[i].displayName}
- ` - } - document.getElementById('accountSelectListScrollable').innerHTML = htmlString - -} - -function prepareServerSelectionList(){ - populateServerListings() - setServerListingHandlers() -} - -function prepareAccountSelectionList(){ - populateAccountListings() - setAccountListingHandlers() +/** + * Script for overlay.ejs + */ + +/* Overlay Wrapper Functions */ + +/** + * Check to see if the overlay is visible. + * + * @returns {boolean} Whether or not the overlay is visible. + */ +function isOverlayVisible(){ + return document.getElementById('main').hasAttribute('overlay') +} + +let overlayHandlerContent + +/** + * Overlay keydown handler for a non-dismissable overlay. + * + * @param {KeyboardEvent} e The keydown event. + */ +function overlayKeyHandler (e){ + if(e.key === 'Enter' || e.key === 'Escape'){ + document.getElementById(overlayHandlerContent).getElementsByClassName('overlayKeybindEnter')[0].click() + } +} +/** + * Overlay keydown handler for a dismissable overlay. + * + * @param {KeyboardEvent} e The keydown event. + */ +function overlayKeyDismissableHandler (e){ + if(e.key === 'Enter'){ + document.getElementById(overlayHandlerContent).getElementsByClassName('overlayKeybindEnter')[0].click() + } else if(e.key === 'Escape'){ + document.getElementById(overlayHandlerContent).getElementsByClassName('overlayKeybindEsc')[0].click() + } +} + +/** + * Bind overlay keydown listeners for escape and exit. + * + * @param {boolean} state Whether or not to add new event listeners. + * @param {string} content The overlay content which will be shown. + * @param {boolean} dismissable Whether or not the overlay is dismissable + */ +function bindOverlayKeys(state, content, dismissable){ + overlayHandlerContent = content + document.removeEventListener('keydown', overlayKeyHandler) + document.removeEventListener('keydown', overlayKeyDismissableHandler) + if(state){ + if(dismissable){ + document.addEventListener('keydown', overlayKeyDismissableHandler) + } else { + document.addEventListener('keydown', overlayKeyHandler) + } + } +} + +/** + * Toggle the visibility of the overlay. + * + * @param {boolean} toggleState True to display, false to hide. + * @param {boolean} dismissable Optional. True to show the dismiss option, otherwise false. + * @param {string} content Optional. The content div to be shown. + */ +function toggleOverlay(toggleState, dismissable = false, content = 'overlayContent'){ + if(toggleState == null){ + toggleState = !document.getElementById('main').hasAttribute('overlay') + } + if(typeof dismissable === 'string'){ + content = dismissable + dismissable = false + } + bindOverlayKeys(toggleState, content, dismissable) + if(toggleState){ + document.getElementById('main').setAttribute('overlay', true) + // Make things untabbable. + $('#main *').attr('tabindex', '-1') + $('#' + content).parent().children().hide() + $('#' + content).show() + if(dismissable){ + $('#overlayDismiss').show() + } else { + $('#overlayDismiss').hide() + } + $('#overlayContainer').fadeIn({ + duration: 250, + start: () => { + if(getCurrentView() === VIEWS.settings){ + document.getElementById('settingsContainer').style.backgroundColor = 'transparent' + } + } + }) + } else { + document.getElementById('main').removeAttribute('overlay') + // Make things tabbable. + $('#main *').removeAttr('tabindex') + $('#overlayContainer').fadeOut({ + duration: 250, + start: () => { + if(getCurrentView() === VIEWS.settings){ + document.getElementById('settingsContainer').style.backgroundColor = 'rgba(0, 0, 0, 0.50)' + } + }, + complete: () => { + $('#' + content).parent().children().hide() + $('#' + content).show() + if(dismissable){ + $('#overlayDismiss').show() + } else { + $('#overlayDismiss').hide() + } + } + }) + } +} + +function toggleServerSelection(toggleState){ + prepareServerSelectionList() + toggleOverlay(toggleState, true, 'serverSelectContent') +} + +/** + * Set the content of the overlay. + * + * @param {string} title Overlay title text. + * @param {string} description Overlay description text. + * @param {string} acknowledge Acknowledge button text. + * @param {string} dismiss Dismiss button text. + */ +function setOverlayContent(title, description, acknowledge, dismiss = 'Dismiss'){ + document.getElementById('overlayTitle').innerHTML = title + document.getElementById('overlayDesc').innerHTML = description + document.getElementById('overlayAcknowledge').innerHTML = acknowledge + document.getElementById('overlayDismiss').innerHTML = dismiss +} + +/** + * Set the onclick handler of the overlay acknowledge button. + * If the handler is null, a default handler will be added. + * + * @param {function} handler + */ +function setOverlayHandler(handler){ + if(handler == null){ + document.getElementById('overlayAcknowledge').onclick = () => { + toggleOverlay(false) + } + } else { + document.getElementById('overlayAcknowledge').onclick = handler + } +} + +/** + * Set the onclick handler of the overlay dismiss button. + * If the handler is null, a default handler will be added. + * + * @param {function} handler + */ +function setDismissHandler(handler){ + if(handler == null){ + document.getElementById('overlayDismiss').onclick = () => { + toggleOverlay(false) + } + } else { + document.getElementById('overlayDismiss').onclick = handler + } +} + +/* Server Select View */ + +document.getElementById('serverSelectConfirm').addEventListener('click', () => { + const listings = document.getElementsByClassName('serverListing') + for(let i=0; i 0){ + const serv = DistroManager.getDistribution().getServer(listings[i].getAttribute('servid')) + updateSelectedServer(serv) + toggleOverlay(false) + } +}) + +document.getElementById('accountSelectConfirm').addEventListener('click', () => { + const listings = document.getElementsByClassName('accountListing') + for(let i=0; i 0){ + const authAcc = ConfigManager.setSelectedAccount(listings[0].getAttribute('uuid')) + ConfigManager.save() + updateSelectedAccount(authAcc) + toggleOverlay(false) + validateSelectedAccount() + } +}) + +// Bind server select cancel button. +document.getElementById('serverSelectCancel').addEventListener('click', () => { + toggleOverlay(false) +}) + +document.getElementById('accountSelectCancel').addEventListener('click', () => { + $('#accountSelectContent').fadeOut(250, () => { + $('#overlayContent').fadeIn(250) + }) +}) + +function setServerListingHandlers(){ + const listings = Array.from(document.getElementsByClassName('serverListing')) + listings.map((val) => { + val.onclick = e => { + if(val.hasAttribute('selected')){ + return + } + const cListings = document.getElementsByClassName('serverListing') + for(let i=0; i { + val.onclick = e => { + if(val.hasAttribute('selected')){ + return + } + const cListings = document.getElementsByClassName('accountListing') + for(let i=0; i + +
+ ${serv.getName()} + ${serv.getDescription()} +
+
${serv.getMinecraftVersion()}
+
${serv.getVersion()}
+ ${serv.isMainServer() ? `
+ + + + + + + + Main Server +
` : ''} +
+
+ ` + } + document.getElementById('serverSelectListScrollable').innerHTML = htmlString + +} + +function populateAccountListings(){ + const accountsObj = ConfigManager.getAuthAccounts() + const accounts = Array.from(Object.keys(accountsObj), v=>accountsObj[v]) + let htmlString = '' + for(let i=0; i + +
${accounts[i].displayName}
+ ` + } + document.getElementById('accountSelectListScrollable').innerHTML = htmlString + +} + +function prepareServerSelectionList(){ + populateServerListings() + setServerListingHandlers() +} + +function prepareAccountSelectionList(){ + populateAccountListings() + setAccountListingHandlers() } \ No newline at end of file diff --git a/app/assets/js/scripts/settings.js b/app/assets/js/scripts/settings.js index 23ef79c0..4a7a7dca 100644 --- a/app/assets/js/scripts/settings.js +++ b/app/assets/js/scripts/settings.js @@ -1,1349 +1,1349 @@ -// Requirements -const os = require('os') -const semver = require('semver') - -const { JavaGuard } = require('./assets/js/assetguard') -const DropinModUtil = require('./assets/js/dropinmodutil') - -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) { - 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 === '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() - saveDropinModConfiguration() - saveShaderpackSettings() - 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) - toggleOverlay(false) - switchView(getCurrentView(), VIEWS.login) - }) - 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) - if(authKeys.length === 0){ - return - } - 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') - -/** - * 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 += `
-
-
-
-
- ${mdl.getName()} - v${mdl.getVersion()} -
-
- -
- ${mdl.hasSubModules() ? `
- ${Object.values(parseModulesForUI(mdl.getSubModules(), true, servConf[mdl.getVersionlessID()])).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(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 += `
-
-
-
-
- ${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 = () => { - 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
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
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() - saveShaderpackSettings() - 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 = ` - -
- ${serv.getName()} - ${serv.getDescription()} -
-
${serv.getMinecraftVersion()}
-
${serv.getVersion()}
- ${serv.isMainServer() ? `
- - - - - - - - Main Server -
` : ''} -
-
- ` -} - -// 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(500, () => { - prepareModsTab() - $('#settingsTabMods').fadeIn(500) - }) -} - -/** - * 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/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' -} - -/** - * 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){ - if(v.version.major < 9) { - settingsJavaExecDetails.innerHTML = `Selected: Java ${v.version.major} Update ${v.version.update} (x${v.arch})` - } else { - settingsJavaExecDetails.innerHTML = `Selected: Java ${v.version.major}.${v.version.minor}.${v.version.revision} (x${v.arch})` - } - } 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) - 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. - */ -function prepareSettings(first = false) { - if(first){ - setupSettingsTabs() - initSettingsValidators() - prepareUpdateTab() - } else { - prepareModsTab() - } - initSettingsValues() - prepareAccountsTab() - prepareJavaTab() - prepareAboutTab() -} - -// Prepare the settings UI on startup. +// Requirements +const os = require('os') +const semver = require('semver') + +const { JavaGuard } = require('./assets/js/assetguard') +const DropinModUtil = require('./assets/js/dropinmodutil') + +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) { + 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 === '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() + saveDropinModConfiguration() + saveShaderpackSettings() + 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) + toggleOverlay(false) + switchView(getCurrentView(), VIEWS.login) + }) + 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) + if(authKeys.length === 0){ + return + } + 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') + +/** + * 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 += `
+
+
+
+
+ ${mdl.getName()} + v${mdl.getVersion()} +
+
+ +
+ ${mdl.hasSubModules() ? `
+ ${Object.values(parseModulesForUI(mdl.getSubModules(), true, servConf[mdl.getVersionlessID()])).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(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 += `
+
+
+
+
+ ${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 = () => { + 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
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
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() + saveShaderpackSettings() + 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 = ` + +
+ ${serv.getName()} + ${serv.getDescription()} +
+
${serv.getMinecraftVersion()}
+
${serv.getVersion()}
+ ${serv.isMainServer() ? `
+ + + + + + + + Main Server +
` : ''} +
+
+ ` +} + +// 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(500, () => { + prepareModsTab() + $('#settingsTabMods').fadeIn(500) + }) +} + +/** + * 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/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' +} + +/** + * 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){ + if(v.version.major < 9) { + settingsJavaExecDetails.innerHTML = `Selected: Java ${v.version.major} Update ${v.version.update} (x${v.arch})` + } else { + settingsJavaExecDetails.innerHTML = `Selected: Java ${v.version.major}.${v.version.minor}.${v.version.revision} (x${v.arch})` + } + } 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) + 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/MastermDEV/NemesisMC-Launcher/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. + */ +function prepareSettings(first = false) { + if(first){ + setupSettingsTabs() + initSettingsValidators() + prepareUpdateTab() + } else { + prepareModsTab() + } + initSettingsValues() + prepareAccountsTab() + prepareJavaTab() + prepareAboutTab() +} + +// Prepare the settings UI on startup. //prepareSettings(true) \ No newline at end of file diff --git a/app/assets/js/scripts/uibinder.js b/app/assets/js/scripts/uibinder.js index 0b080d1b..f16302d6 100644 --- a/app/assets/js/scripts/uibinder.js +++ b/app/assets/js/scripts/uibinder.js @@ -1,419 +1,419 @@ -/** - * Initialize UI functions which depend on internal modules. - * Loaded after core UI functions are initialized in uicore.js. - */ -// Requirements -const path = require('path') - -const AuthManager = require('./assets/js/authmanager') -const ConfigManager = require('./assets/js/configmanager') -const DistroManager = require('./assets/js/distromanager') -const Lang = require('./assets/js/langloader') - -let rscShouldLoad = false -let fatalStartupError = false - -// Mapping of each view to their container IDs. -const VIEWS = { - landing: '#landingContainer', - login: '#loginContainer', - settings: '#settingsContainer', - welcome: '#welcomeContainer' -} - -// The currently shown view container. -let currentView - -/** - * Switch launcher views. - * - * @param {string} current The ID of the current view container. - * @param {*} next The ID of the next view container. - * @param {*} currentFadeTime Optional. The fade out time for the current view. - * @param {*} nextFadeTime Optional. The fade in time for the next view. - * @param {*} onCurrentFade Optional. Callback function to execute when the current - * view fades out. - * @param {*} onNextFade Optional. Callback function to execute when the next view - * fades in. - */ -function switchView(current, next, currentFadeTime = 500, nextFadeTime = 500, onCurrentFade = () => {}, onNextFade = () => {}){ - currentView = next - $(`${current}`).fadeOut(currentFadeTime, () => { - onCurrentFade() - $(`${next}`).fadeIn(nextFadeTime, () => { - onNextFade() - }) - }) -} - -/** - * Get the currently shown view container. - * - * @returns {string} The currently shown view container. - */ -function getCurrentView(){ - return currentView -} - -function showMainUI(data){ - - if(!isDev){ - loggerAutoUpdater.log('Initializing..') - ipcRenderer.send('autoUpdateAction', 'initAutoUpdater', ConfigManager.getAllowPrerelease()) - } - - prepareSettings(true) - updateSelectedServer(data.getServer(ConfigManager.getSelectedServer())) - refreshServerStatus() - setTimeout(() => { - document.getElementById('frameBar').style.backgroundColor = 'rgba(0, 0, 0, 0.5)' - document.body.style.backgroundImage = `url('assets/images/backgrounds/${document.body.getAttribute('bkid')}.jpg')` - $('#main').show() - - const isLoggedIn = Object.keys(ConfigManager.getAuthAccounts()).length > 0 - - // If this is enabled in a development environment we'll get ratelimited. - // The relaunch frequency is usually far too high. - if(!isDev && isLoggedIn){ - validateSelectedAccount() - } - - if(ConfigManager.isFirstLaunch()){ - currentView = VIEWS.welcome - $(VIEWS.welcome).fadeIn(1000) - } else { - if(isLoggedIn){ - currentView = VIEWS.landing - $(VIEWS.landing).fadeIn(1000) - } else { - currentView = VIEWS.login - $(VIEWS.login).fadeIn(1000) - } - } - - setTimeout(() => { - $('#loadingContainer').fadeOut(500, () => { - $('#loadSpinnerImage').removeClass('rotating') - }) - }, 250) - - }, 750) - // Disable tabbing to the news container. - initNews().then(() => { - $('#newsContainer *').attr('tabindex', '-1') - }) -} - -function showFatalStartupError(){ - setTimeout(() => { - $('#loadingContainer').fadeOut(250, () => { - document.getElementById('overlayContainer').style.background = 'none' - setOverlayContent( - 'Fatal Error: Unable to Load Distribution Index', - 'A connection could not be established to our servers to download the distribution index. No local copies were available to load.

The distribution index is an essential file which provides the latest server information. The launcher is unable to start without it. Ensure you are connected to the internet and relaunch the application.', - 'Close' - ) - setOverlayHandler(() => { - const window = remote.getCurrentWindow() - window.close() - }) - toggleOverlay(true) - }) - }, 750) -} - -/** - * Common functions to perform after refreshing the distro index. - * - * @param {Object} data The distro index object. - */ -function onDistroRefresh(data){ - updateSelectedServer(data.getServer(ConfigManager.getSelectedServer())) - refreshServerStatus() - initNews() - syncModConfigurations(data) -} - -/** - * Sync the mod configurations with the distro index. - * - * @param {Object} data The distro index object. - */ -function syncModConfigurations(data){ - - const syncedCfgs = [] - - for(let serv of data.getServers()){ - - const id = serv.getID() - const mdls = serv.getModules() - const cfg = ConfigManager.getModConfiguration(id) - - if(cfg != null){ - - const modsOld = cfg.mods - const mods = {} - - for(let mdl of mdls){ - const type = mdl.getType() - - if(type === DistroManager.Types.ForgeMod || type === DistroManager.Types.LiteMod || type === DistroManager.Types.LiteLoader){ - if(!mdl.getRequired().isRequired()){ - const mdlID = mdl.getVersionlessID() - if(modsOld[mdlID] == null){ - mods[mdlID] = scanOptionalSubModules(mdl.getSubModules(), mdl) - } else { - mods[mdlID] = mergeModConfiguration(modsOld[mdlID], scanOptionalSubModules(mdl.getSubModules(), mdl), false) - } - } else { - if(mdl.hasSubModules()){ - const mdlID = mdl.getVersionlessID() - const v = scanOptionalSubModules(mdl.getSubModules(), mdl) - if(typeof v === 'object'){ - if(modsOld[mdlID] == null){ - mods[mdlID] = v - } else { - mods[mdlID] = mergeModConfiguration(modsOld[mdlID], v, true) - } - } - } - } - } - } - - syncedCfgs.push({ - id, - mods - }) - - } else { - - const mods = {} - - for(let mdl of mdls){ - const type = mdl.getType() - if(type === DistroManager.Types.ForgeMod || type === DistroManager.Types.LiteMod || type === DistroManager.Types.LiteLoader){ - if(!mdl.getRequired().isRequired()){ - mods[mdl.getVersionlessID()] = scanOptionalSubModules(mdl.getSubModules(), mdl) - } else { - if(mdl.hasSubModules()){ - const v = scanOptionalSubModules(mdl.getSubModules(), mdl) - if(typeof v === 'object'){ - mods[mdl.getVersionlessID()] = v - } - } - } - } - } - - syncedCfgs.push({ - id, - mods - }) - - } - } - - ConfigManager.setModConfigurations(syncedCfgs) - ConfigManager.save() -} - -/** - * Recursively scan for optional sub modules. If none are found, - * this function returns a boolean. If optional sub modules do exist, - * a recursive configuration object is returned. - * - * @returns {boolean | Object} The resolved mod configuration. - */ -function scanOptionalSubModules(mdls, origin){ - if(mdls != null){ - const mods = {} - - for(let mdl of mdls){ - const type = mdl.getType() - // Optional types. - if(type === DistroManager.Types.ForgeMod || type === DistroManager.Types.LiteMod || type === DistroManager.Types.LiteLoader){ - // It is optional. - if(!mdl.getRequired().isRequired()){ - mods[mdl.getVersionlessID()] = scanOptionalSubModules(mdl.getSubModules(), mdl) - } else { - if(mdl.hasSubModules()){ - const v = scanOptionalSubModules(mdl.getSubModules(), mdl) - if(typeof v === 'object'){ - mods[mdl.getVersionlessID()] = v - } - } - } - } - } - - if(Object.keys(mods).length > 0){ - const ret = { - mods - } - if(!origin.getRequired().isRequired()){ - ret.value = origin.getRequired().isDefault() - } - return ret - } - } - return origin.getRequired().isDefault() -} - -/** - * Recursively merge an old configuration into a new configuration. - * - * @param {boolean | Object} o The old configuration value. - * @param {boolean | Object} n The new configuration value. - * @param {boolean} nReq If the new value is a required mod. - * - * @returns {boolean | Object} The merged configuration. - */ -function mergeModConfiguration(o, n, nReq = false){ - if(typeof o === 'boolean'){ - if(typeof n === 'boolean') return o - else if(typeof n === 'object'){ - if(!nReq){ - n.value = o - } - return n - } - } else if(typeof o === 'object'){ - if(typeof n === 'boolean') return typeof o.value !== 'undefined' ? o.value : true - else if(typeof n === 'object'){ - if(!nReq){ - n.value = typeof o.value !== 'undefined' ? o.value : true - } - - const newMods = Object.keys(n.mods) - for(let i=0; i${selectedAcc.displayName}. Please ${accLen > 0 ? 'select another account or ' : ''} login again.`, - 'Login', - 'Select Another Account' - ) - setOverlayHandler(() => { - document.getElementById('loginUsername').value = selectedAcc.username - validateEmail(selectedAcc.username) - loginViewOnSuccess = getCurrentView() - loginViewOnCancel = getCurrentView() - if(accLen > 0){ - loginViewCancelHandler = () => { - ConfigManager.addAuthAccount(selectedAcc.uuid, selectedAcc.accessToken, selectedAcc.username, selectedAcc.displayName) - ConfigManager.save() - validateSelectedAccount() - } - loginCancelEnabled(true) - } - toggleOverlay(false) - switchView(getCurrentView(), VIEWS.login) - }) - setDismissHandler(() => { - if(accLen > 1){ - prepareAccountSelectionList() - $('#overlayContent').fadeOut(250, () => { - bindOverlayKeys(true, 'accountSelectContent', true) - $('#accountSelectContent').fadeIn(250) - }) - } else { - const accountsObj = ConfigManager.getAuthAccounts() - const accounts = Array.from(Object.keys(accountsObj), v => accountsObj[v]) - // This function validates the account switch. - setSelectedAccount(accounts[0].uuid) - toggleOverlay(false) - } - }) - toggleOverlay(true, accLen > 0) - } else { - return true - } - } else { - return true - } -} - -/** - * Temporary function to update the selected account along - * with the relevent UI elements. - * - * @param {string} uuid The UUID of the account. - */ -function setSelectedAccount(uuid){ - const authAcc = ConfigManager.setSelectedAccount(uuid) - ConfigManager.save() - updateSelectedAccount(authAcc) - validateSelectedAccount() -} - -// Synchronous Listener -document.addEventListener('readystatechange', function(){ - - if (document.readyState === 'interactive' || document.readyState === 'complete'){ - if(rscShouldLoad){ - rscShouldLoad = false - if(!fatalStartupError){ - const data = DistroManager.getDistribution() - showMainUI(data) - } else { - showFatalStartupError() - } - } - } - -}, false) - -// Actions that must be performed after the distribution index is downloaded. -ipcRenderer.on('distributionIndexDone', (event, res) => { - if(res) { - const data = DistroManager.getDistribution() - syncModConfigurations(data) - if(document.readyState === 'interactive' || document.readyState === 'complete'){ - showMainUI(data) - } else { - rscShouldLoad = true - } - } else { - fatalStartupError = true - if(document.readyState === 'interactive' || document.readyState === 'complete'){ - showFatalStartupError() - } else { - rscShouldLoad = true - } - } -}) +/** + * Initialize UI functions which depend on internal modules. + * Loaded after core UI functions are initialized in uicore.js. + */ +// Requirements +const path = require('path') + +const AuthManager = require('./assets/js/authmanager') +const ConfigManager = require('./assets/js/configmanager') +const DistroManager = require('./assets/js/distromanager') +const Lang = require('./assets/js/langloader') + +let rscShouldLoad = false +let fatalStartupError = false + +// Mapping of each view to their container IDs. +const VIEWS = { + landing: '#landingContainer', + login: '#loginContainer', + settings: '#settingsContainer', + welcome: '#welcomeContainer' +} + +// The currently shown view container. +let currentView + +/** + * Switch launcher views. + * + * @param {string} current The ID of the current view container. + * @param {*} next The ID of the next view container. + * @param {*} currentFadeTime Optional. The fade out time for the current view. + * @param {*} nextFadeTime Optional. The fade in time for the next view. + * @param {*} onCurrentFade Optional. Callback function to execute when the current + * view fades out. + * @param {*} onNextFade Optional. Callback function to execute when the next view + * fades in. + */ +function switchView(current, next, currentFadeTime = 500, nextFadeTime = 500, onCurrentFade = () => {}, onNextFade = () => {}){ + currentView = next + $(`${current}`).fadeOut(currentFadeTime, () => { + onCurrentFade() + $(`${next}`).fadeIn(nextFadeTime, () => { + onNextFade() + }) + }) +} + +/** + * Get the currently shown view container. + * + * @returns {string} The currently shown view container. + */ +function getCurrentView(){ + return currentView +} + +function showMainUI(data){ + + if(!isDev){ + loggerAutoUpdater.log('Initializing..') + ipcRenderer.send('autoUpdateAction', 'initAutoUpdater', ConfigManager.getAllowPrerelease()) + } + + prepareSettings(true) + updateSelectedServer(data.getServer(ConfigManager.getSelectedServer())) + refreshServerStatus() + setTimeout(() => { + document.getElementById('frameBar').style.backgroundColor = 'rgba(0, 0, 0, 0.5)' + document.body.style.backgroundImage = `url('assets/images/backgrounds/${document.body.getAttribute('bkid')}.jpg')` + $('#main').show() + + const isLoggedIn = Object.keys(ConfigManager.getAuthAccounts()).length > 0 + + // If this is enabled in a development environment we'll get ratelimited. + // The relaunch frequency is usually far too high. + if(!isDev && isLoggedIn){ + validateSelectedAccount() + } + + if(ConfigManager.isFirstLaunch()){ + currentView = VIEWS.welcome + $(VIEWS.welcome).fadeIn(1000) + } else { + if(isLoggedIn){ + currentView = VIEWS.landing + $(VIEWS.landing).fadeIn(1000) + } else { + currentView = VIEWS.login + $(VIEWS.login).fadeIn(1000) + } + } + + setTimeout(() => { + $('#loadingContainer').fadeOut(500, () => { + $('#loadSpinnerImage').removeClass('rotating') + }) + }, 250) + + }, 750) + // Disable tabbing to the news container. + initNews().then(() => { + $('#newsContainer *').attr('tabindex', '-1') + }) +} + +function showFatalStartupError(){ + setTimeout(() => { + $('#loadingContainer').fadeOut(250, () => { + document.getElementById('overlayContainer').style.background = 'none' + setOverlayContent( + 'Fatal Error: Unable to Load Distribution Index', + 'A connection could not be established to our servers to download the distribution index. No local copies were available to load.

The distribution index is an essential file which provides the latest server information. The launcher is unable to start without it. Ensure you are connected to the internet and relaunch the application.', + 'Close' + ) + setOverlayHandler(() => { + const window = remote.getCurrentWindow() + window.close() + }) + toggleOverlay(true) + }) + }, 750) +} + +/** + * Common functions to perform after refreshing the distro index. + * + * @param {Object} data The distro index object. + */ +function onDistroRefresh(data){ + updateSelectedServer(data.getServer(ConfigManager.getSelectedServer())) + refreshServerStatus() + initNews() + syncModConfigurations(data) +} + +/** + * Sync the mod configurations with the distro index. + * + * @param {Object} data The distro index object. + */ +function syncModConfigurations(data){ + + const syncedCfgs = [] + + for(let serv of data.getServers()){ + + const id = serv.getID() + const mdls = serv.getModules() + const cfg = ConfigManager.getModConfiguration(id) + + if(cfg != null){ + + const modsOld = cfg.mods + const mods = {} + + for(let mdl of mdls){ + const type = mdl.getType() + + if(type === DistroManager.Types.ForgeMod || type === DistroManager.Types.LiteMod || type === DistroManager.Types.LiteLoader){ + if(!mdl.getRequired().isRequired()){ + const mdlID = mdl.getVersionlessID() + if(modsOld[mdlID] == null){ + mods[mdlID] = scanOptionalSubModules(mdl.getSubModules(), mdl) + } else { + mods[mdlID] = mergeModConfiguration(modsOld[mdlID], scanOptionalSubModules(mdl.getSubModules(), mdl), false) + } + } else { + if(mdl.hasSubModules()){ + const mdlID = mdl.getVersionlessID() + const v = scanOptionalSubModules(mdl.getSubModules(), mdl) + if(typeof v === 'object'){ + if(modsOld[mdlID] == null){ + mods[mdlID] = v + } else { + mods[mdlID] = mergeModConfiguration(modsOld[mdlID], v, true) + } + } + } + } + } + } + + syncedCfgs.push({ + id, + mods + }) + + } else { + + const mods = {} + + for(let mdl of mdls){ + const type = mdl.getType() + if(type === DistroManager.Types.ForgeMod || type === DistroManager.Types.LiteMod || type === DistroManager.Types.LiteLoader){ + if(!mdl.getRequired().isRequired()){ + mods[mdl.getVersionlessID()] = scanOptionalSubModules(mdl.getSubModules(), mdl) + } else { + if(mdl.hasSubModules()){ + const v = scanOptionalSubModules(mdl.getSubModules(), mdl) + if(typeof v === 'object'){ + mods[mdl.getVersionlessID()] = v + } + } + } + } + } + + syncedCfgs.push({ + id, + mods + }) + + } + } + + ConfigManager.setModConfigurations(syncedCfgs) + ConfigManager.save() +} + +/** + * Recursively scan for optional sub modules. If none are found, + * this function returns a boolean. If optional sub modules do exist, + * a recursive configuration object is returned. + * + * @returns {boolean | Object} The resolved mod configuration. + */ +function scanOptionalSubModules(mdls, origin){ + if(mdls != null){ + const mods = {} + + for(let mdl of mdls){ + const type = mdl.getType() + // Optional types. + if(type === DistroManager.Types.ForgeMod || type === DistroManager.Types.LiteMod || type === DistroManager.Types.LiteLoader){ + // It is optional. + if(!mdl.getRequired().isRequired()){ + mods[mdl.getVersionlessID()] = scanOptionalSubModules(mdl.getSubModules(), mdl) + } else { + if(mdl.hasSubModules()){ + const v = scanOptionalSubModules(mdl.getSubModules(), mdl) + if(typeof v === 'object'){ + mods[mdl.getVersionlessID()] = v + } + } + } + } + } + + if(Object.keys(mods).length > 0){ + const ret = { + mods + } + if(!origin.getRequired().isRequired()){ + ret.value = origin.getRequired().isDefault() + } + return ret + } + } + return origin.getRequired().isDefault() +} + +/** + * Recursively merge an old configuration into a new configuration. + * + * @param {boolean | Object} o The old configuration value. + * @param {boolean | Object} n The new configuration value. + * @param {boolean} nReq If the new value is a required mod. + * + * @returns {boolean | Object} The merged configuration. + */ +function mergeModConfiguration(o, n, nReq = false){ + if(typeof o === 'boolean'){ + if(typeof n === 'boolean') return o + else if(typeof n === 'object'){ + if(!nReq){ + n.value = o + } + return n + } + } else if(typeof o === 'object'){ + if(typeof n === 'boolean') return typeof o.value !== 'undefined' ? o.value : true + else if(typeof n === 'object'){ + if(!nReq){ + n.value = typeof o.value !== 'undefined' ? o.value : true + } + + const newMods = Object.keys(n.mods) + for(let i=0; i${selectedAcc.displayName}. Please ${accLen > 0 ? 'select another account or ' : ''} login again.`, + 'Login', + 'Select Another Account' + ) + setOverlayHandler(() => { + document.getElementById('loginUsername').value = selectedAcc.username + validateEmail(selectedAcc.username) + loginViewOnSuccess = getCurrentView() + loginViewOnCancel = getCurrentView() + if(accLen > 0){ + loginViewCancelHandler = () => { + ConfigManager.addAuthAccount(selectedAcc.uuid, selectedAcc.accessToken, selectedAcc.username, selectedAcc.displayName) + ConfigManager.save() + validateSelectedAccount() + } + loginCancelEnabled(true) + } + toggleOverlay(false) + switchView(getCurrentView(), VIEWS.login) + }) + setDismissHandler(() => { + if(accLen > 1){ + prepareAccountSelectionList() + $('#overlayContent').fadeOut(250, () => { + bindOverlayKeys(true, 'accountSelectContent', true) + $('#accountSelectContent').fadeIn(250) + }) + } else { + const accountsObj = ConfigManager.getAuthAccounts() + const accounts = Array.from(Object.keys(accountsObj), v => accountsObj[v]) + // This function validates the account switch. + setSelectedAccount(accounts[0].uuid) + toggleOverlay(false) + } + }) + toggleOverlay(true, accLen > 0) + } else { + return true + } + } else { + return true + } +} + +/** + * Temporary function to update the selected account along + * with the relevent UI elements. + * + * @param {string} uuid The UUID of the account. + */ +function setSelectedAccount(uuid){ + const authAcc = ConfigManager.setSelectedAccount(uuid) + ConfigManager.save() + updateSelectedAccount(authAcc) + validateSelectedAccount() +} + +// Synchronous Listener +document.addEventListener('readystatechange', function(){ + + if (document.readyState === 'interactive' || document.readyState === 'complete'){ + if(rscShouldLoad){ + rscShouldLoad = false + if(!fatalStartupError){ + const data = DistroManager.getDistribution() + showMainUI(data) + } else { + showFatalStartupError() + } + } + } + +}, false) + +// Actions that must be performed after the distribution index is downloaded. +ipcRenderer.on('distributionIndexDone', (event, res) => { + if(res) { + const data = DistroManager.getDistribution() + syncModConfigurations(data) + if(document.readyState === 'interactive' || document.readyState === 'complete'){ + showMainUI(data) + } else { + rscShouldLoad = true + } + } else { + fatalStartupError = true + if(document.readyState === 'interactive' || document.readyState === 'complete'){ + showFatalStartupError() + } else { + rscShouldLoad = true + } + } +}) diff --git a/app/assets/js/scripts/uicore.js b/app/assets/js/scripts/uicore.js index 73e372aa..c99f2538 100644 --- a/app/assets/js/scripts/uicore.js +++ b/app/assets/js/scripts/uicore.js @@ -1,213 +1,213 @@ -/** - * Core UI functions are initialized in this file. This prevents - * unexpected errors from breaking the core features. Specifically, - * actions in this file should not require the usage of any internal - * modules, excluding dependencies. - */ -// Requirements -const $ = require('jquery') -const {ipcRenderer, remote, shell, webFrame} = require('electron') -const isDev = require('./assets/js/isdev') -const LoggerUtil = require('./assets/js/loggerutil') - -const loggerUICore = LoggerUtil('%c[UICore]', 'color: #000668; font-weight: bold') -const loggerAutoUpdater = LoggerUtil('%c[AutoUpdater]', 'color: #000668; font-weight: bold') -const loggerAutoUpdaterSuccess = LoggerUtil('%c[AutoUpdater]', 'color: #209b07; font-weight: bold') - -// Log deprecation and process warnings. -process.traceProcessWarnings = true -process.traceDeprecation = true - -// Disable eval function. -// eslint-disable-next-line -window.eval = global.eval = function () { - throw new Error('Sorry, this app does not support window.eval().') -} - -// Display warning when devtools window is opened. -remote.getCurrentWebContents().on('devtools-opened', () => { - console.log('%cThe console is dark and full of terrors.', 'color: white; -webkit-text-stroke: 4px #a02d2a; font-size: 60px; font-weight: bold') - console.log('%cIf you\'ve been told to paste something here, you\'re being scammed.', 'font-size: 16px') - console.log('%cUnless you know exactly what you\'re doing, close this window.', 'font-size: 16px') -}) - -// Disable zoom, needed for darwin. -webFrame.setZoomLevel(0) -webFrame.setVisualZoomLevelLimits(1, 1) - -// Initialize auto updates in production environments. -let updateCheckListener -if(!isDev){ - ipcRenderer.on('autoUpdateNotification', (event, arg, info) => { - switch(arg){ - case 'checking-for-update': - loggerAutoUpdater.log('Checking for update..') - settingsUpdateButtonStatus('Checking for Updates..', true) - break - case 'update-available': - loggerAutoUpdaterSuccess.log('New update available', info.version) - - if(process.platform === 'darwin'){ - info.darwindownload = `https://github.com/dscalzi/HeliosLauncher/releases/download/v${info.version}/helioslauncher-setup-${info.version}.dmg` - showUpdateUI(info) - } - - populateSettingsUpdateInformation(info) - break - case 'update-downloaded': - loggerAutoUpdaterSuccess.log('Update ' + info.version + ' ready to be installed.') - settingsUpdateButtonStatus('Install Now', false, () => { - if(!isDev){ - ipcRenderer.send('autoUpdateAction', 'installUpdateNow') - } - }) - showUpdateUI(info) - break - case 'update-not-available': - loggerAutoUpdater.log('No new update found.') - settingsUpdateButtonStatus('Check for Updates') - break - case 'ready': - updateCheckListener = setInterval(() => { - ipcRenderer.send('autoUpdateAction', 'checkForUpdate') - }, 1800000) - ipcRenderer.send('autoUpdateAction', 'checkForUpdate') - break - case 'realerror': - if(info != null && info.code != null){ - if(info.code === 'ERR_UPDATER_INVALID_RELEASE_FEED'){ - loggerAutoUpdater.log('No suitable releases found.') - } else if(info.code === 'ERR_XML_MISSED_ELEMENT'){ - loggerAutoUpdater.log('No releases found.') - } else { - loggerAutoUpdater.error('Error during update check..', info) - loggerAutoUpdater.debug('Error Code:', info.code) - } - } - break - default: - loggerAutoUpdater.log('Unknown argument', arg) - break - } - }) -} - -/** - * Send a notification to the main process changing the value of - * allowPrerelease. If we are running a prerelease version, then - * this will always be set to true, regardless of the current value - * of val. - * - * @param {boolean} val The new allow prerelease value. - */ -function changeAllowPrerelease(val){ - ipcRenderer.send('autoUpdateAction', 'allowPrereleaseChange', val) -} - -function showUpdateUI(info){ - //TODO Make this message a bit more informative `${info.version}` - document.getElementById('image_seal_container').setAttribute('update', true) - document.getElementById('image_seal_container').onclick = () => { - /*setOverlayContent('Update Available', 'A new update for the launcher is available. Would you like to install now?', 'Install', 'Later') - setOverlayHandler(() => { - if(!isDev){ - ipcRenderer.send('autoUpdateAction', 'installUpdateNow') - } else { - console.error('Cannot install updates in development environment.') - toggleOverlay(false) - } - }) - setDismissHandler(() => { - toggleOverlay(false) - }) - toggleOverlay(true, true)*/ - switchView(getCurrentView(), VIEWS.settings, 500, 500, () => { - settingsNavItemListener(document.getElementById('settingsNavUpdate'), false) - }) - } -} - -/* jQuery Example -$(function(){ - loggerUICore.log('UICore Initialized'); -})*/ - -document.addEventListener('readystatechange', function () { - if (document.readyState === 'interactive'){ - loggerUICore.log('UICore Initializing..') - - // Bind close button. - Array.from(document.getElementsByClassName('fCb')).map((val) => { - val.addEventListener('click', e => { - const window = remote.getCurrentWindow() - window.close() - }) - }) - - // Bind restore down button. - Array.from(document.getElementsByClassName('fRb')).map((val) => { - val.addEventListener('click', e => { - const window = remote.getCurrentWindow() - if(window.isMaximized()){ - window.unmaximize() - } else { - window.maximize() - } - document.activeElement.blur() - }) - }) - - // Bind minimize button. - Array.from(document.getElementsByClassName('fMb')).map((val) => { - val.addEventListener('click', e => { - const window = remote.getCurrentWindow() - window.minimize() - document.activeElement.blur() - }) - }) - - // Remove focus from social media buttons once they're clicked. - Array.from(document.getElementsByClassName('mediaURL')).map(val => { - val.addEventListener('click', e => { - document.activeElement.blur() - }) - }) - - } else if(document.readyState === 'complete'){ - - //266.01 - //170.8 - //53.21 - // Bind progress bar length to length of bot wrapper - //const targetWidth = document.getElementById("launch_content").getBoundingClientRect().width - //const targetWidth2 = document.getElementById("server_selection").getBoundingClientRect().width - //const targetWidth3 = document.getElementById("launch_button").getBoundingClientRect().width - - document.getElementById('launch_details').style.maxWidth = 266.01 - document.getElementById('launch_progress').style.width = 170.8 - document.getElementById('launch_details_right').style.maxWidth = 170.8 - document.getElementById('launch_progress_label').style.width = 53.21 - - } - -}, false) - -/** - * Open web links in the user's default browser. - */ -$(document).on('click', 'a[href^="http"]', function(event) { - event.preventDefault() - shell.openExternal(this.href) -}) - -/** - * Opens DevTools window if you hold (ctrl + shift + i). - * This will crash the program if you are using multiple - * DevTools, for example the chrome debugger in VS Code. - */ -document.addEventListener('keydown', function (e) { - if((e.key === 'I' || e.key === 'i') && e.ctrlKey && e.shiftKey){ - let window = remote.getCurrentWindow() - window.toggleDevTools() - } +/** + * Core UI functions are initialized in this file. This prevents + * unexpected errors from breaking the core features. Specifically, + * actions in this file should not require the usage of any internal + * modules, excluding dependencies. + */ +// Requirements +const $ = require('jquery') +const {ipcRenderer, remote, shell, webFrame} = require('electron') +const isDev = require('./assets/js/isdev') +const LoggerUtil = require('./assets/js/loggerutil') + +const loggerUICore = LoggerUtil('%c[UICore]', 'color: #000668; font-weight: bold') +const loggerAutoUpdater = LoggerUtil('%c[AutoUpdater]', 'color: #000668; font-weight: bold') +const loggerAutoUpdaterSuccess = LoggerUtil('%c[AutoUpdater]', 'color: #209b07; font-weight: bold') + +// Log deprecation and process warnings. +process.traceProcessWarnings = true +process.traceDeprecation = true + +// Disable eval function. +// eslint-disable-next-line +window.eval = global.eval = function () { + throw new Error('Sorry, this app does not support window.eval().') +} + +// Display warning when devtools window is opened. +remote.getCurrentWebContents().on('devtools-opened', () => { + console.log('%cThe console is dark and full of terrors.', 'color: white; -webkit-text-stroke: 4px #a02d2a; font-size: 60px; font-weight: bold') + console.log('%cIf you\'ve been told to paste something here, you\'re being scammed.', 'font-size: 16px') + console.log('%cUnless you know exactly what you\'re doing, close this window.', 'font-size: 16px') +}) + +// Disable zoom, needed for darwin. +webFrame.setZoomLevel(0) +webFrame.setVisualZoomLevelLimits(1, 1) + +// Initialize auto updates in production environments. +let updateCheckListener +if(!isDev){ + ipcRenderer.on('autoUpdateNotification', (event, arg, info) => { + switch(arg){ + case 'checking-for-update': + loggerAutoUpdater.log('Checking for update..') + settingsUpdateButtonStatus('Checking for Updates..', true) + break + case 'update-available': + loggerAutoUpdaterSuccess.log('New update available', info.version) + + if(process.platform === 'darwin'){ + info.darwindownload = `https://github.com/MastermDEV/NemesisMC-Launcher/releases/download/v${info.version}/nemesismclauncher-setup${info.version}.dmg` + showUpdateUI(info) + } + + populateSettingsUpdateInformation(info) + break + case 'update-downloaded': + loggerAutoUpdaterSuccess.log('Update ' + info.version + ' ready to be installed.') + settingsUpdateButtonStatus('Install Now', false, () => { + if(!isDev){ + ipcRenderer.send('autoUpdateAction', 'installUpdateNow') + } + }) + showUpdateUI(info) + break + case 'update-not-available': + loggerAutoUpdater.log('No new update found.') + settingsUpdateButtonStatus('Check for Updates') + break + case 'ready': + updateCheckListener = setInterval(() => { + ipcRenderer.send('autoUpdateAction', 'checkForUpdate') + }, 1800000) + ipcRenderer.send('autoUpdateAction', 'checkForUpdate') + break + case 'realerror': + if(info != null && info.code != null){ + if(info.code === 'ERR_UPDATER_INVALID_RELEASE_FEED'){ + loggerAutoUpdater.log('No suitable releases found.') + } else if(info.code === 'ERR_XML_MISSED_ELEMENT'){ + loggerAutoUpdater.log('No releases found.') + } else { + loggerAutoUpdater.error('Error during update check..', info) + loggerAutoUpdater.debug('Error Code:', info.code) + } + } + break + default: + loggerAutoUpdater.log('Unknown argument', arg) + break + } + }) +} + +/** + * Send a notification to the main process changing the value of + * allowPrerelease. If we are running a prerelease version, then + * this will always be set to true, regardless of the current value + * of val. + * + * @param {boolean} val The new allow prerelease value. + */ +function changeAllowPrerelease(val){ + ipcRenderer.send('autoUpdateAction', 'allowPrereleaseChange', val) +} + +function showUpdateUI(info){ + //TODO Make this message a bit more informative `${info.version}` + document.getElementById('image_seal_container').setAttribute('update', true) + document.getElementById('image_seal_container').onclick = () => { + /*setOverlayContent('Update Available', 'A new update for the launcher is available. Would you like to install now?', 'Install', 'Later') + setOverlayHandler(() => { + if(!isDev){ + ipcRenderer.send('autoUpdateAction', 'installUpdateNow') + } else { + console.error('Cannot install updates in development environment.') + toggleOverlay(false) + } + }) + setDismissHandler(() => { + toggleOverlay(false) + }) + toggleOverlay(true, true)*/ + switchView(getCurrentView(), VIEWS.settings, 500, 500, () => { + settingsNavItemListener(document.getElementById('settingsNavUpdate'), false) + }) + } +} + +/* jQuery Example +$(function(){ + loggerUICore.log('UICore Initialized'); +})*/ + +document.addEventListener('readystatechange', function () { + if (document.readyState === 'interactive'){ + loggerUICore.log('UICore Initializing..') + + // Bind close button. + Array.from(document.getElementsByClassName('fCb')).map((val) => { + val.addEventListener('click', e => { + const window = remote.getCurrentWindow() + window.close() + }) + }) + + // Bind restore down button. + Array.from(document.getElementsByClassName('fRb')).map((val) => { + val.addEventListener('click', e => { + const window = remote.getCurrentWindow() + if(window.isMaximized()){ + window.unmaximize() + } else { + window.maximize() + } + document.activeElement.blur() + }) + }) + + // Bind minimize button. + Array.from(document.getElementsByClassName('fMb')).map((val) => { + val.addEventListener('click', e => { + const window = remote.getCurrentWindow() + window.minimize() + document.activeElement.blur() + }) + }) + + // Remove focus from social media buttons once they're clicked. + Array.from(document.getElementsByClassName('mediaURL')).map(val => { + val.addEventListener('click', e => { + document.activeElement.blur() + }) + }) + + } else if(document.readyState === 'complete'){ + + //266.01 + //170.8 + //53.21 + // Bind progress bar length to length of bot wrapper + //const targetWidth = document.getElementById("launch_content").getBoundingClientRect().width + //const targetWidth2 = document.getElementById("server_selection").getBoundingClientRect().width + //const targetWidth3 = document.getElementById("launch_button").getBoundingClientRect().width + + document.getElementById('launch_details').style.maxWidth = 266.01 + document.getElementById('launch_progress').style.width = 170.8 + document.getElementById('launch_details_right').style.maxWidth = 170.8 + document.getElementById('launch_progress_label').style.width = 53.21 + + } + +}, false) + +/** + * Open web links in the user's default browser. + */ +$(document).on('click', 'a[href^="http"]', function(event) { + event.preventDefault() + shell.openExternal(this.href) +}) + +/** + * Opens DevTools window if you hold (ctrl + shift + i). + * This will crash the program if you are using multiple + * DevTools, for example the chrome debugger in VS Code. + */ +document.addEventListener('keydown', function (e) { + if((e.key === 'I' || e.key === 'i') && e.ctrlKey && e.shiftKey){ + let window = remote.getCurrentWindow() + window.toggleDevTools() + } }) \ No newline at end of file diff --git a/app/assets/js/scripts/welcome.js b/app/assets/js/scripts/welcome.js index e6ff6297..e430829a 100644 --- a/app/assets/js/scripts/welcome.js +++ b/app/assets/js/scripts/welcome.js @@ -1,6 +1,6 @@ -/** - * Script for welcome.ejs - */ -document.getElementById('welcomeButton').addEventListener('click', e => { - switchView(VIEWS.welcome, VIEWS.login) +/** + * Script for welcome.ejs + */ +document.getElementById('welcomeButton').addEventListener('click', e => { + switchView(VIEWS.welcome, VIEWS.login) }) \ No newline at end of file