/** * Script for landing.ejs */ // Requirements const cp = require('child_process') const crypto = require('crypto') const { URL } = require('url') const { MojangRestAPI, getServerStatus } = require('helios-core/mojang') const { RestResponseStatus, isDisplayableError, mcVersionAtLeast } = require('helios-core/common') const { FullRepair, DistributionIndexProcessor, MojangIndexProcessor } = require('helios-core/dl') // Internal Requirements const DiscordWrapper = require('./assets/js/discordwrapper') const ProcessBuilder = require('./assets/js/processbuilder') // 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.getLogger('Landing') /* 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} percent Percentage (0-100) */ function setLaunchPercentage(percent){ launch_progress.setAttribute('max', 100) launch_progress.setAttribute('value', percent) launch_progress_label.innerHTML = percent + '%' } /** * Set the value of the OS progress bar and display that on the UI. * * @param {number} percent Percentage (0-100) */ function setDownloadPercentage(percent){ remote.getCurrentWindow().setProgressBar(percent/100) setLaunchPercentage(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', async (e) => { loggerLanding.info('Launching game..') const mcVersion = (await DistroAPI.getDistribution()).getServerById(ConfigManager.getSelectedServer()).rawServer.minecraftVersion const jExe = ConfigManager.getJavaExecutable(ConfigManager.getSelectedServer()) 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(async v => { loggerLanding.info('Java version meta', v) if(v.valid){ await dlAsync() } else { asyncSystemScan(mcVersion) } }) } }) // Bind settings button document.getElementById('settingsMediaButton').onclick = async e => { await prepareSettings() switchView(getCurrentView(), VIEWS.settings) } // Bind avatar overlay button. document.getElementById('avatarOverlay').onclick = async e => { await 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://mc-heads.net/body/${authUser.uuid}/right')` } } user_text.innerHTML = username } updateSelectedAccount(ConfigManager.getSelectedAccount()) // Bind selected server function updateSelectedServer(serv){ if(getCurrentView() === VIEWS.settings){ fullSettingsSave() } ConfigManager.setSelectedServer(serv != null ? serv.rawServer.id : null) ConfigManager.save() server_selection_button.innerHTML = '\u2022 ' + (serv != null ? serv.rawServer.name : 'No Server Selected') if(getCurrentView() === VIEWS.settings){ animateSettingsTabRefresh() } setLaunchEnabled(serv != null) } // Real text is set in uibinder.js on distributionIndexDone. server_selection_button.innerHTML = '\u2022 Loading..' server_selection_button.onclick = async e => { e.target.blur() await toggleServerSelection(true) } // Update Mojang Status Color const refreshMojangStatuses = async function(){ loggerLanding.info('Refreshing Mojang Statuses..') let status = 'grey' let tooltipEssentialHTML = '' let tooltipNonEssentialHTML = '' const response = await MojangRestAPI.status() let statuses if(response.responseStatus === RestResponseStatus.SUCCESS) { statuses = response.data } else { loggerLanding.warn('Unable to refresh Mojang service status.') statuses = MojangRestAPI.getDefaultStatuses() } 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' } } document.getElementById('mojangStatusEssentialContainer').innerHTML = tooltipEssentialHTML document.getElementById('mojangStatusNonEssentialContainer').innerHTML = tooltipNonEssentialHTML document.getElementById('mojang_status_icon').style.color = MojangRestAPI.statusToHex(status) } const refreshServerStatus = async (fade = false) => { loggerLanding.info('Refreshing Server Status') const serv = (await DistroAPI.getDistribution()).getServerById(ConfigManager.getSelectedServer()) let pLabel = 'SERVER' let pVal = 'OFFLINE' try { const servStat = await getServerStatus(47, serv.hostname, serv.port) console.log(servStat) pLabel = 'PLAYERS' pVal = servStat.players.online + '/' + servStat.players.max } 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. // Refresh statuses every hour. The status page itself refreshes every day so... let mojangStatusListener = setInterval(() => refreshMojangStatuses(true), 60*60*1000) // Set refresh rate to once every 5 minutes. 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 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) => { console.log(`\x1b[32m[SysAEx]\x1b[0m ${data}`) }) // Stderr sysAEx.stdio[2].setEncoding('utf8') sysAEx.stdio[2].on('data', (data) => { console.log(`\x1b[31m[SysAEx]\x1b[0m ${data}`) }) const javaVer = mcVersionAtLeast('1.17', mcVersion) ? '17' : '8' sysAEx.on('message', async (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 ${javaVer}. Would you like us to install a copy?`, 'Install Java', 'Install Manually' ) setOverlayHandler(() => { setLaunchDetails('Preparing Java Download..') sysAEx.send({task: 'changeContext', class: 'AssetGuard', args: [ConfigManager.getCommonDirectory(),ConfigManager.getJavaExecutable(ConfigManager.getSelectedServer())]}) sysAEx.send({task: 'execute', function: '_enqueueOpenJDK', argsArr: [ConfigManager.getDataDirectory(), mcVersion]}) toggleOverlay(false) }) setDismissHandler(() => { $('#overlayContent').fadeOut(250, () => { //$('#overlayDismiss').toggle(false) setOverlayContent( 'Java is Required
to Launch', `A valid x64 installation of Java ${javaVer} 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(ConfigManager.getSelectedServer(), 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 await populateJavaExecDetails(settingsJavaExecVal.value) if(launchAfter){ await 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(ConfigManager.getSelectedServer(), m.args[0]) ConfigManager.save() if(extractListener != null){ clearInterval(extractListener) extractListener = null } setLaunchDetails('Java Installed!') if(launchAfter){ await 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 // Change this if your server uses something different. const GAME_JOINED_REGEX = /\[.+\]: Sound engine started/ const GAME_LAUNCH_REGEX = /^\[.+\]: (?:MinecraftForge .+ Initialized|ModLauncher .+ starting: .+)$/ const MIN_LINGER = 5000 async function dlAsync(login = true) { // Login parameter is temporary for debug purposes. Allows testing the validation/downloads without // launching the game. const loggerLaunchSuite = LoggerUtil.getLogger('LaunchSuite') setLaunchDetails('Loading server information..') let distro try { distro = await DistroAPI.refreshDistributionOrFallback() onDistroRefresh(distro) } catch(err) { loggerLaunchSuite.error('Unable to refresh distribution index.', err) showLaunchFailure('Fatal Error', 'Could not load a copy of the distribution index. See the console (CTRL + Shift + i) for more details.') return } const serv = distro.getServerById(ConfigManager.getSelectedServer()) 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 fullRepairModule = new FullRepair( ConfigManager.getCommonDirectory(), ConfigManager.getLauncherDirectory(), ConfigManager.getSelectedServer(), DistroAPI.isDevMode() ) fullRepairModule.spawnReceiver() fullRepairModule.childProcess.on('error', (err) => { loggerLaunchSuite.error('Error during launch', err) showLaunchFailure('Error During Launch', err.message || 'See console (CTRL + Shift + i) for more details.') }) fullRepairModule.childProcess.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.') } }) loggerLaunchSuite.info('Validating files.') setLaunchDetails('Validating file integrity..') const invalidFileCount = await fullRepairModule.verifyFiles(percent => { setLaunchPercentage(percent) }) setLaunchPercentage(100) if(invalidFileCount > 0) { loggerLaunchSuite.info('Downloading files.') setLaunchDetails('Downloading files..') await fullRepairModule.download(percent => { setDownloadPercentage(percent) }) setDownloadPercentage(100) } else { loggerLaunchSuite.info('No invalid files, skipping download.') } // Remove download bar. remote.getCurrentWindow().setProgressBar(-1) fullRepairModule.destroyReceiver() setLaunchDetails('Preparing to launch..') const mojangIndexProcessor = new MojangIndexProcessor( ConfigManager.getCommonDirectory(), serv.rawServer.minecraftVersion) const distributionIndexProcessor = new DistributionIndexProcessor( ConfigManager.getCommonDirectory(), distro, serv.rawServer.id ) // TODO need to load these. const forgeData = await distributionIndexProcessor.loadForgeVersionJson(serv) const versionData = await mojangIndexProcessor.getVersionJson() if(login) { const authUser = ConfigManager.getSelectedAccount() loggerLaunchSuite.info(`Sending selected account (${authUser.displayName}) to ProcessBuilder.`) let pb = new ProcessBuilder(serv, versionData, forgeData, authUser, remote.app.getVersion()) setLaunchDetails('Launching game..') // const SERVER_JOINED_REGEX = /\[.+\]: \[CHAT\] [a-zA-Z0-9_]{1,16} joined the game/ const SERVER_JOINED_REGEX = new RegExp(`\\[.+\\]: \\[CHAT\\] ${authUser.displayName} joined the 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 if(distro.rawDistribution.discord != null && serv.rawServerdiscord != null){ DiscordWrapper.initRPC(distro.rawDistribution.discord, serv.rawServer.discord) hasRPC = true proc.on('close', (code, signal) => { loggerLaunchSuite.info('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.') } } } /** * 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. */ async function loadNews(){ const distroData = await DistroAPI.getDistribution() const promise = new Promise((resolve, reject) => { const newsFeed = distroData.rawDistribution.rss 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 }) }) }) return await promise }