HeliosLauncher/app/assets/js/scripts/uibinder.js
Kamesuta 9d80d3b1d5
Localize HeliosLauncher UI using lang files (#301)
* First step to use Language .json file in ejs

* i18n for landing.ejs

* i18n for login.ejs

* i18n for loginOptions.ejs

* i18n for overlay.ejs

* i18n for settings.ejs

* i18n for waiting.ejs

* i18n for welcome.ejs

* langloader.js placeholder support

* i18n for landing.js

* i18n for login.js

* i18n for overlay.js

* i18n for settings.js

* i18n for uibinder.js

* i18n for uicore.js

* remove html language replacement

* use toml for i18n

* Fix mojang/microsoft status icon is undefined

* cascadable langloader

* separate lang file for customization

* move some placeholder text to _placeholder.toml

* Update

* Reduce package lock diff.

* Remove another placeholder.

* Checkbox does not require translation.

* Icons don't need translation.

* Leave placeholders inline.

* Fix translation for news pages.

* Remove more unneeded translations.

---------

Co-authored-by: Daniel Scalzi <d_scalzi@yahoo.com>
2023-10-05 15:26:32 -04:00

467 lines
16 KiB
JavaScript

/**
* Initialize UI functions which depend on internal modules.
* Loaded after core UI functions are initialized in uicore.js.
*/
// Requirements
const path = require('path')
const { Type } = require('helios-distribution-types')
const AuthManager = require('./assets/js/authmanager')
const ConfigManager = require('./assets/js/configmanager')
const { DistroAPI } = require('./assets/js/distromanager')
let rscShouldLoad = false
let fatalStartupError = false
// Mapping of each view to their container IDs.
const VIEWS = {
landing: '#landingContainer',
loginOptions: '#loginOptionsContainer',
login: '#loginContainer',
settings: '#settingsContainer',
welcome: '#welcomeContainer',
waiting: '#waitingContainer'
}
// 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, async () => {
await onCurrentFade()
$(`${next}`).fadeIn(nextFadeTime, async () => {
await onNextFade()
})
})
}
/**
* Get the currently shown view container.
*
* @returns {string} The currently shown view container.
*/
function getCurrentView(){
return currentView
}
async function showMainUI(data){
if(!isDev){
loggerAutoUpdater.info('Initializing..')
ipcRenderer.send('autoUpdateAction', 'initAutoUpdater', ConfigManager.getAllowPrerelease())
}
await prepareSettings(true)
updateSelectedServer(data.getServerById(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 {
loginOptionsCancelEnabled(false)
loginOptionsViewOnLoginSuccess = VIEWS.landing
loginOptionsViewOnLoginCancel = VIEWS.loginOptions
currentView = VIEWS.loginOptions
$(VIEWS.loginOptions).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(
Lang.queryJS('uibinder.startup.fatalErrorTitle'),
Lang.queryJS('uibinder.startup.fatalErrorMessage'),
Lang.queryJS('uibinder.startup.closeButton')
)
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.getServerById(ConfigManager.getSelectedServer()))
refreshServerStatus()
initNews()
syncModConfigurations(data)
ensureJavaSettings(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.servers){
const id = serv.rawServer.id
const mdls = serv.modules
const cfg = ConfigManager.getModConfiguration(id)
if(cfg != null){
const modsOld = cfg.mods
const mods = {}
for(let mdl of mdls){
const type = mdl.rawModule.type
if(type === Type.ForgeMod || type === Type.LiteMod || type === Type.LiteLoader){
if(!mdl.getRequired().value){
const mdlID = mdl.getVersionlessMavenIdentifier()
if(modsOld[mdlID] == null){
mods[mdlID] = scanOptionalSubModules(mdl.subModules, mdl)
} else {
mods[mdlID] = mergeModConfiguration(modsOld[mdlID], scanOptionalSubModules(mdl.subModules, mdl), false)
}
} else {
if(mdl.subModules.length > 0){
const mdlID = mdl.getVersionlessMavenIdentifier()
const v = scanOptionalSubModules(mdl.subModules, 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.rawModule.type
if(type === Type.ForgeMod || type === Type.LiteMod || type === Type.LiteLoader){
if(!mdl.getRequired().value){
mods[mdl.getVersionlessMavenIdentifier()] = scanOptionalSubModules(mdl.subModules, mdl)
} else {
if(mdl.subModules.length > 0){
const v = scanOptionalSubModules(mdl.subModules, mdl)
if(typeof v === 'object'){
mods[mdl.getVersionlessMavenIdentifier()] = v
}
}
}
}
}
syncedCfgs.push({
id,
mods
})
}
}
ConfigManager.setModConfigurations(syncedCfgs)
ConfigManager.save()
}
/**
* Ensure java configurations are present for the available servers.
*
* @param {Object} data The distro index object.
*/
function ensureJavaSettings(data) {
// Nothing too fancy for now.
for(const serv of data.servers){
ConfigManager.ensureJavaConfig(serv.rawServer.id, serv.effectiveJavaOptions, serv.rawServer.javaOptions?.ram)
}
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.rawModule.type
// Optional types.
if(type === Type.ForgeMod || type === Type.LiteMod || type === Type.LiteLoader){
// It is optional.
if(!mdl.getRequired().value){
mods[mdl.getVersionlessMavenIdentifier()] = scanOptionalSubModules(mdl.subModules, mdl)
} else {
if(mdl.hasSubModules()){
const v = scanOptionalSubModules(mdl.subModules, mdl)
if(typeof v === 'object'){
mods[mdl.getVersionlessMavenIdentifier()] = v
}
}
}
}
}
if(Object.keys(mods).length > 0){
const ret = {
mods
}
if(!origin.getRequired().value){
ret.value = origin.getRequired().def
}
return ret
}
}
return origin.getRequired().def
}
/**
* 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<newMods.length; i++){
const mod = newMods[i]
if(o.mods[mod] != null){
n.mods[mod] = mergeModConfiguration(o.mods[mod], n.mods[mod])
}
}
return n
}
}
// If for some reason we haven't been able to merge,
// wipe the old value and use the new one. Just to be safe
return n
}
async function validateSelectedAccount(){
const selectedAcc = ConfigManager.getSelectedAccount()
if(selectedAcc != null){
const val = await AuthManager.validateSelected()
if(!val){
ConfigManager.removeAuthAccount(selectedAcc.uuid)
ConfigManager.save()
const accLen = Object.keys(ConfigManager.getAuthAccounts()).length
setOverlayContent(
Lang.queryJS('uibinder.validateAccount.failedMessageTitle'),
accLen > 0
? Lang.queryJS('uibinder.validateAccount.failedMessage', { 'account': selectedAcc.displayName })
: Lang.queryJS('uibinder.validateAccount.failedMessageSelectAnotherAccount', { 'account': selectedAcc.displayName }),
Lang.queryJS('uibinder.validateAccount.loginButton'),
Lang.queryJS('uibinder.validateAccount.selectAnotherAccountButton')
)
setOverlayHandler(() => {
const isMicrosoft = selectedAcc.type === 'microsoft'
if(isMicrosoft) {
// Empty for now
} else {
// Mojang
// For convenience, pre-populate the username of the account.
document.getElementById('loginUsername').value = selectedAcc.username
validateEmail(selectedAcc.username)
}
loginOptionsViewOnLoginSuccess = getCurrentView()
loginOptionsViewOnLoginCancel = VIEWS.loginOptions
if(accLen > 0) {
loginOptionsViewOnCancel = getCurrentView()
loginOptionsViewCancelHandler = () => {
if(isMicrosoft) {
ConfigManager.addMicrosoftAuthAccount(
selectedAcc.uuid,
selectedAcc.accessToken,
selectedAcc.username,
selectedAcc.expiresAt,
selectedAcc.microsoft.access_token,
selectedAcc.microsoft.refresh_token,
selectedAcc.microsoft.expires_at
)
} else {
ConfigManager.addMojangAuthAccount(selectedAcc.uuid, selectedAcc.accessToken, selectedAcc.username, selectedAcc.displayName)
}
ConfigManager.save()
validateSelectedAccount()
}
loginOptionsCancelEnabled(true)
} else {
loginOptionsCancelEnabled(false)
}
toggleOverlay(false)
switchView(getCurrentView(), VIEWS.loginOptions)
})
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', async () => {
if (document.readyState === 'interactive' || document.readyState === 'complete'){
if(rscShouldLoad){
rscShouldLoad = false
if(!fatalStartupError){
const data = await DistroAPI.getDistribution()
await showMainUI(data)
} else {
showFatalStartupError()
}
}
}
}, false)
// Actions that must be performed after the distribution index is downloaded.
ipcRenderer.on('distributionIndexDone', async (event, res) => {
if(res) {
const data = await DistroAPI.getDistribution()
syncModConfigurations(data)
ensureJavaSettings(data)
if(document.readyState === 'interactive' || document.readyState === 'complete'){
await showMainUI(data)
} else {
rscShouldLoad = true
}
} else {
fatalStartupError = true
if(document.readyState === 'interactive' || document.readyState === 'complete'){
showFatalStartupError()
} else {
rscShouldLoad = true
}
}
})
// Util for development
async function devModeToggle() {
DistroAPI.toggleDevMode(true)
const data = await DistroAPI.refreshDistributionOrFallback()
ensureJavaSettings(data)
updateSelectedServer(data.servers[0])
syncModConfigurations(data)
}