Add launcher catalog workflow and smoke tests
This commit is contained in:
307
app/assets/js/scripts/install.js
Normal file
307
app/assets/js/scripts/install.js
Normal file
@@ -0,0 +1,307 @@
|
||||
const CatalogManager = require('./assets/js/catalogmanager')
|
||||
const ConfigManager = require('./assets/js/configmanager')
|
||||
const ProfileAssetManager = require('./assets/js/profileassetmanager')
|
||||
|
||||
const installCatalogList = document.getElementById('installCatalogList')
|
||||
const installDetailTitle = document.getElementById('installDetailTitle')
|
||||
const installDetailSummary = document.getElementById('installDetailSummary')
|
||||
const installDetailMeta = document.getElementById('installDetailMeta')
|
||||
const installDetailInfo = document.getElementById('installDetailInfo')
|
||||
const installDetailBody = document.getElementById('installDetailBody')
|
||||
const installDetailAddButton = document.getElementById('installDetailAddButton')
|
||||
|
||||
let selectedProfileId = null
|
||||
let latestCatalog = null
|
||||
|
||||
function describeProfileKind(kind){
|
||||
switch(kind){
|
||||
case 'map':
|
||||
return '오리지널 맵'
|
||||
case 'server-pack':
|
||||
return '플러그인 맵 + 서버팩'
|
||||
case 'modpack':
|
||||
default:
|
||||
return '모드팩'
|
||||
}
|
||||
}
|
||||
|
||||
function createInstallBadge(text){
|
||||
const badge = document.createElement('span')
|
||||
badge.className = 'launcherBadge'
|
||||
badge.textContent = text
|
||||
return badge
|
||||
}
|
||||
|
||||
function createInfoLine(label, value){
|
||||
const line = document.createElement('div')
|
||||
line.className = 'launcherInfoLine'
|
||||
|
||||
const labelElement = document.createElement('span')
|
||||
labelElement.className = 'launcherInfoLabel'
|
||||
labelElement.textContent = label
|
||||
|
||||
const valueElement = document.createElement('span')
|
||||
valueElement.className = 'launcherInfoValue'
|
||||
valueElement.textContent = value
|
||||
|
||||
line.appendChild(labelElement)
|
||||
line.appendChild(valueElement)
|
||||
return line
|
||||
}
|
||||
|
||||
function showInstallMessage(title, message){
|
||||
if(typeof setOverlayContent === 'function'){
|
||||
setOverlayContent(title, message, '확인')
|
||||
setOverlayHandler(() => toggleOverlay(false))
|
||||
toggleOverlay(true)
|
||||
}
|
||||
}
|
||||
|
||||
function buildDetailText(profile){
|
||||
if(typeof profile.details === 'string' && profile.details.trim().length > 0){
|
||||
return profile.details.trim()
|
||||
}
|
||||
|
||||
switch(profile.kind){
|
||||
case 'map':
|
||||
return '이 프로필은 싱글플레이 월드를 바로 실행하기 위한 항목입니다. 필요한 클라이언트 배포 파일과 월드 자료는 관리자가 미리 등록해둡니다.'
|
||||
case 'server-pack':
|
||||
return '이 프로필은 서버 실행/접속 흐름을 함께 다루는 항목입니다. 클라이언트 파일과 서버 번들은 관리자가 미리 등록하며, 사용자는 라이브러리에서 실행과 접속만 진행합니다.'
|
||||
case 'modpack':
|
||||
default:
|
||||
return '이 프로필은 일반 모드팩 클라이언트입니다. 필요한 배포 파일은 관리자가 미리 등록하며, 사용자는 라이브러리에 추가한 뒤 실행만 하면 됩니다.'
|
||||
}
|
||||
}
|
||||
|
||||
function renderDetailPanel(profile){
|
||||
const installedIds = new Set(
|
||||
ConfigManager.getInstalledLibraryProfiles().map((installedProfile) => installedProfile.id)
|
||||
)
|
||||
const installed = installedIds.has(profile.id)
|
||||
|
||||
installDetailTitle.textContent = profile.name
|
||||
installDetailSummary.textContent = profile.description || '설명이 없습니다.'
|
||||
installDetailMeta.innerHTML = ''
|
||||
installDetailMeta.appendChild(createInstallBadge(describeProfileKind(profile.kind)))
|
||||
if(installed){
|
||||
installDetailMeta.appendChild(createInstallBadge('라이브러리 보유'))
|
||||
}
|
||||
if(!profile.launchReady){
|
||||
installDetailMeta.appendChild(createInstallBadge('실행 준비 필요'))
|
||||
}
|
||||
if(profile.kind === 'server-pack' && profile.hostReady){
|
||||
installDetailMeta.appendChild(createInstallBadge('로컬 호스팅 가능'))
|
||||
}
|
||||
|
||||
installDetailInfo.innerHTML = ''
|
||||
installDetailInfo.appendChild(createInfoLine('프로필 ID', profile.id))
|
||||
installDetailInfo.appendChild(createInfoLine('종류', describeProfileKind(profile.kind)))
|
||||
installDetailInfo.appendChild(createInfoLine('실행 준비', profile.launchReady ? '완료' : '추가 설정 필요'))
|
||||
|
||||
if(profile.defaultServerAddress){
|
||||
installDetailInfo.appendChild(createInfoLine('기본 주소', profile.defaultServerAddress))
|
||||
}
|
||||
if(profile.kind === 'map' && profile.worldDirectoryName){
|
||||
installDetailInfo.appendChild(createInfoLine('월드 폴더', profile.worldDirectoryName))
|
||||
}
|
||||
if(profile.kind === 'server-pack'){
|
||||
installDetailInfo.appendChild(createInfoLine('로컬 호스팅', profile.hostReady ? '가능' : '관리자 설정 필요'))
|
||||
}
|
||||
if(profile.launchIssues.length > 0){
|
||||
installDetailInfo.appendChild(createInfoLine('확인 필요', profile.launchIssues.join(' / ')))
|
||||
} else if(profile.hostIssues.length > 0){
|
||||
installDetailInfo.appendChild(createInfoLine('호스팅 참고', profile.hostIssues.join(' / ')))
|
||||
}
|
||||
|
||||
installDetailBody.textContent = buildDetailText(profile)
|
||||
installDetailAddButton.disabled = installed || !profile.launchReady
|
||||
installDetailAddButton.textContent = installed ? '이미 라이브러리에 있음' : '라이브러리에 추가'
|
||||
installDetailAddButton.onclick = async () => {
|
||||
try {
|
||||
const installedProfile = await CatalogManager.installProfile(profile.id)
|
||||
await ProfileAssetManager.prefetchProfileAssets(installedProfile)
|
||||
if(installedProfile.kind === 'server-pack' && installedProfile.hostReady){
|
||||
await ProfileAssetManager.ensureServerBundleInstalled(installedProfile)
|
||||
}
|
||||
renderDetailPanel(profile)
|
||||
await renderInstallView()
|
||||
if(typeof refreshLibraryView === 'function'){
|
||||
await refreshLibraryView()
|
||||
}
|
||||
showInstallMessage('추가 완료', `${profile.name} 프로필을 라이브러리에 추가했습니다.`)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
const message = error instanceof Error ? error.message : '프로필 설치 중 오류가 발생했습니다.'
|
||||
showInstallMessage('설치 실패', message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderEmptyDetailPanel(){
|
||||
installDetailTitle.textContent = '프로필을 선택하세요'
|
||||
installDetailSummary.textContent = '왼쪽 목록에서 모드팩, 맵, 서버팩을 고르면 자세한 설명과 설치 조건을 볼 수 있습니다.'
|
||||
installDetailMeta.innerHTML = ''
|
||||
installDetailInfo.innerHTML = ''
|
||||
installDetailBody.textContent = '관리자가 등록한 프로필 상세 설명이 여기에 표시됩니다.'
|
||||
installDetailAddButton.disabled = true
|
||||
installDetailAddButton.textContent = '라이브러리에 추가'
|
||||
installDetailAddButton.onclick = null
|
||||
}
|
||||
|
||||
function selectProfile(profileId){
|
||||
selectedProfileId = profileId
|
||||
if(latestCatalog == null){
|
||||
renderEmptyDetailPanel()
|
||||
return
|
||||
}
|
||||
|
||||
const profile = latestCatalog.profiles.find((entry) => entry.id === profileId)
|
||||
if(profile == null){
|
||||
renderEmptyDetailPanel()
|
||||
return
|
||||
}
|
||||
|
||||
renderDetailPanel(profile)
|
||||
}
|
||||
|
||||
async function renderInstallView(){
|
||||
installCatalogList.innerHTML = ''
|
||||
|
||||
try {
|
||||
const catalog = await CatalogManager.loadCatalog()
|
||||
latestCatalog = catalog
|
||||
const installedIds = new Set(
|
||||
ConfigManager.getInstalledLibraryProfiles().map((profile) => profile.id)
|
||||
)
|
||||
|
||||
if(catalog.sourceError != null){
|
||||
const warningCard = document.createElement('article')
|
||||
warningCard.className = 'launcherCard'
|
||||
warningCard.innerHTML = '<h3 class="launcherCardTitle">카탈로그 로드 경고</h3><p class="launcherCardDescription">관리자가 등록한 카탈로그를 읽지 못했습니다. 현재 보이는 목록이 없다면 배포 주소 또는 로컬 카탈로그 파일을 관리자 측에서 확인해야 합니다.</p>'
|
||||
installCatalogList.appendChild(warningCard)
|
||||
}
|
||||
|
||||
for(const profile of catalog.profiles){
|
||||
const card = document.createElement('article')
|
||||
card.className = 'launcherCard'
|
||||
if(profile.id === selectedProfileId){
|
||||
card.setAttribute('selected', 'true')
|
||||
}
|
||||
|
||||
const header = document.createElement('div')
|
||||
header.className = 'launcherCardHeader'
|
||||
|
||||
const titleGroup = document.createElement('div')
|
||||
titleGroup.className = 'launcherCardTitleGroup'
|
||||
|
||||
const title = document.createElement('h3')
|
||||
title.className = 'launcherCardTitle'
|
||||
title.textContent = profile.name
|
||||
|
||||
const meta = document.createElement('div')
|
||||
meta.className = 'launcherCardMeta'
|
||||
meta.appendChild(createInstallBadge(describeProfileKind(profile.kind)))
|
||||
if(installedIds.has(profile.id)){
|
||||
meta.appendChild(createInstallBadge('설치됨'))
|
||||
}
|
||||
if(profile.kind === 'server-pack' && profile.hostReady){
|
||||
meta.appendChild(createInstallBadge('호스팅 가능'))
|
||||
}
|
||||
|
||||
titleGroup.appendChild(title)
|
||||
titleGroup.appendChild(meta)
|
||||
header.appendChild(titleGroup)
|
||||
|
||||
const description = document.createElement('p')
|
||||
description.className = 'launcherCardDescription'
|
||||
description.textContent = profile.description || '설명이 없습니다.'
|
||||
|
||||
const actions = document.createElement('div')
|
||||
actions.className = 'launcherCardActions'
|
||||
|
||||
const detailButton = document.createElement('button')
|
||||
detailButton.className = 'launcherSecondaryButton'
|
||||
detailButton.textContent = '자세히 보기'
|
||||
detailButton.addEventListener('click', () => {
|
||||
selectProfile(profile.id)
|
||||
renderInstallView()
|
||||
})
|
||||
|
||||
const installButton = document.createElement('button')
|
||||
installButton.className = 'launcherPrimaryButton'
|
||||
installButton.textContent = installedIds.has(profile.id) ? '설치됨' : '라이브러리에 추가'
|
||||
installButton.disabled = installedIds.has(profile.id) || !profile.launchReady
|
||||
installButton.addEventListener('click', async () => {
|
||||
try {
|
||||
const installedProfile = await CatalogManager.installProfile(profile.id)
|
||||
await ProfileAssetManager.prefetchProfileAssets(installedProfile)
|
||||
if(installedProfile.kind === 'server-pack' && installedProfile.hostReady){
|
||||
await ProfileAssetManager.ensureServerBundleInstalled(installedProfile)
|
||||
}
|
||||
selectProfile(profile.id)
|
||||
await renderInstallView()
|
||||
if(typeof refreshLibraryView === 'function'){
|
||||
await refreshLibraryView()
|
||||
}
|
||||
showInstallMessage('추가 완료', `${profile.name} 프로필을 라이브러리에 추가했습니다.`)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
const message = error instanceof Error ? error.message : '프로필 설치 중 오류가 발생했습니다.'
|
||||
showInstallMessage('설치 실패', message)
|
||||
}
|
||||
})
|
||||
|
||||
actions.appendChild(detailButton)
|
||||
actions.appendChild(installButton)
|
||||
|
||||
card.appendChild(header)
|
||||
card.appendChild(description)
|
||||
card.appendChild(actions)
|
||||
installCatalogList.appendChild(card)
|
||||
}
|
||||
|
||||
if(catalog.profiles.length === 0){
|
||||
renderEmptyDetailPanel()
|
||||
return
|
||||
}
|
||||
|
||||
const selectedProfileStillExists = catalog.profiles.some((profile) => profile.id === selectedProfileId)
|
||||
if(!selectedProfileStillExists){
|
||||
selectedProfileId = catalog.profiles[0].id
|
||||
}
|
||||
|
||||
selectProfile(selectedProfileId)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
latestCatalog = null
|
||||
renderEmptyDetailPanel()
|
||||
|
||||
const errorCard = document.createElement('article')
|
||||
errorCard.className = 'launcherCard'
|
||||
errorCard.innerHTML = '<h3 class="launcherCardTitle">카탈로그 로드 실패</h3><p class="launcherCardDescription">관리자가 등록한 카탈로그를 읽지 못했습니다.</p>'
|
||||
installCatalogList.appendChild(errorCard)
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('installOpenSettingsButton').addEventListener('click', async () => {
|
||||
await prepareSettings()
|
||||
switchView(getCurrentView(), VIEWS.settings)
|
||||
})
|
||||
|
||||
document.getElementById('installBackToLibraryButton').addEventListener('click', async () => {
|
||||
if(typeof refreshLibraryView === 'function'){
|
||||
await refreshLibraryView()
|
||||
}
|
||||
switchView(getCurrentView(), VIEWS.library)
|
||||
})
|
||||
|
||||
document.getElementById('installDetailOpenLibraryButton').addEventListener('click', async () => {
|
||||
if(typeof refreshLibraryView === 'function'){
|
||||
await refreshLibraryView()
|
||||
}
|
||||
switchView(getCurrentView(), VIEWS.library)
|
||||
})
|
||||
|
||||
window.refreshInstallView = renderInstallView
|
||||
renderEmptyDetailPanel()
|
||||
renderInstallView()
|
||||
1040
app/assets/js/scripts/landing.js
Normal file
1040
app/assets/js/scripts/landing.js
Normal file
File diff suppressed because it is too large
Load Diff
417
app/assets/js/scripts/library.js
Normal file
417
app/assets/js/scripts/library.js
Normal file
@@ -0,0 +1,417 @@
|
||||
const { clipboard } = require('electron')
|
||||
|
||||
const CatalogManager = require('./assets/js/catalogmanager')
|
||||
const ConfigManager = require('./assets/js/configmanager')
|
||||
const ProfileAssetManager = require('./assets/js/profileassetmanager')
|
||||
const ServerRuntime = require('./assets/js/serverruntime')
|
||||
const { DistroAPI } = require('./assets/js/distromanager')
|
||||
|
||||
const libraryList = document.getElementById('libraryList')
|
||||
const libraryEmptyState = document.getElementById('libraryEmptyState')
|
||||
|
||||
function renderLibraryEmptyState(isEmpty){
|
||||
libraryEmptyState.style.display = isEmpty ? 'flex' : 'none'
|
||||
}
|
||||
|
||||
function createBadge(text){
|
||||
const badge = document.createElement('span')
|
||||
badge.className = 'launcherBadge'
|
||||
badge.textContent = text
|
||||
return badge
|
||||
}
|
||||
|
||||
function describeProfileKind(kind){
|
||||
switch(kind){
|
||||
case 'map':
|
||||
return '맵'
|
||||
case 'server-pack':
|
||||
return '서버팩'
|
||||
case 'modpack':
|
||||
default:
|
||||
return '모드팩'
|
||||
}
|
||||
}
|
||||
|
||||
function createParagraph(className, text){
|
||||
const element = document.createElement('p')
|
||||
element.className = className
|
||||
element.textContent = text
|
||||
return element
|
||||
}
|
||||
|
||||
function createInfoLine(label, value){
|
||||
const line = document.createElement('div')
|
||||
line.className = 'launcherInfoLine'
|
||||
|
||||
const labelElement = document.createElement('span')
|
||||
labelElement.className = 'launcherInfoLabel'
|
||||
labelElement.textContent = label
|
||||
|
||||
const valueElement = document.createElement('span')
|
||||
valueElement.className = 'launcherInfoValue'
|
||||
valueElement.textContent = value
|
||||
|
||||
line.appendChild(labelElement)
|
||||
line.appendChild(valueElement)
|
||||
return line
|
||||
}
|
||||
|
||||
function showLibraryMessage(title, message){
|
||||
if(typeof setOverlayContent === 'function'){
|
||||
setOverlayContent(title, message, '확인')
|
||||
setOverlayHandler(() => toggleOverlay(false))
|
||||
toggleOverlay(true)
|
||||
}
|
||||
}
|
||||
|
||||
function describeAssetState(profile){
|
||||
const state = ConfigManager.getLibraryProfileAssetState(profile.id)
|
||||
|
||||
if(profile.kind === 'map'){
|
||||
if(state.worldInstalledAt){
|
||||
return `맵 설치 완료 · ${profile.worldDirectoryName}`
|
||||
}
|
||||
if(profile.worldArchiveUrl){
|
||||
return '맵 아카이브 준비 필요'
|
||||
}
|
||||
}
|
||||
|
||||
if(profile.kind === 'server-pack'){
|
||||
if(state.serverBundleInstalledAt){
|
||||
return '서버 번들 설치 완료'
|
||||
}
|
||||
if(profile.serverBundleUrl){
|
||||
return '서버 번들 준비 필요'
|
||||
}
|
||||
}
|
||||
|
||||
return '추가 자산 없음'
|
||||
}
|
||||
|
||||
async function prepareProfileAssets(profile){
|
||||
try {
|
||||
await ProfileAssetManager.prefetchProfileAssets(profile)
|
||||
if(profile.kind === 'server-pack' && profile.hostReady){
|
||||
await ProfileAssetManager.ensureServerBundleInstalled(profile)
|
||||
}
|
||||
await renderLibraryView()
|
||||
showLibraryMessage('자료 준비 완료', `${profile.name} 자료를 준비했습니다.`)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
showLibraryMessage('자료 준비 실패', '프로필 자료를 내려받거나 해제하는 중 오류가 발생했습니다.')
|
||||
}
|
||||
}
|
||||
|
||||
async function activateProfile(profile, launchNow = false){
|
||||
if(!profile.configured){
|
||||
const firstIssue = profile.launchIssues?.[0] ?? '이 프로필은 아직 실행 조건이 충족되지 않았습니다.'
|
||||
showLibraryMessage('프로필 설정 필요', firstIssue)
|
||||
return
|
||||
}
|
||||
|
||||
CatalogManager.selectProfile(profile.id)
|
||||
CatalogManager.applyConfiguredProfile()
|
||||
|
||||
try {
|
||||
const distro = await DistroAPI.refreshDistributionOrFallback()
|
||||
if(distro == null){
|
||||
throw new Error('Distribution refresh returned null.')
|
||||
}
|
||||
|
||||
const currentServer = distro.getServerById(ConfigManager.getSelectedServer())
|
||||
if(currentServer == null && typeof distro.getMainServer === 'function'){
|
||||
const mainServer = distro.getMainServer()
|
||||
if(mainServer != null){
|
||||
ConfigManager.setSelectedServer(mainServer.rawServer.id)
|
||||
ConfigManager.save()
|
||||
}
|
||||
}
|
||||
|
||||
const selectedServerId = ConfigManager.getSelectedServer()
|
||||
if(selectedServerId != null){
|
||||
await ProfileAssetManager.prepareProfileForLaunch(profile, selectedServerId)
|
||||
}
|
||||
|
||||
onDistroRefresh(distro)
|
||||
|
||||
if(getCurrentView() === VIEWS.landing){
|
||||
if(launchNow){
|
||||
document.getElementById('launch_button').click()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switchView(getCurrentView(), VIEWS.landing, 250, 250, () => {}, () => {
|
||||
if(launchNow){
|
||||
document.getElementById('launch_button').click()
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
showLibraryMessage('프로필 로드 실패', '선택한 프로필의 distribution.json 또는 부가 자산을 불러오지 못했습니다.')
|
||||
}
|
||||
}
|
||||
|
||||
function appendAddressOverrideField(profile, fieldGroup){
|
||||
if(!profile.allowCustomServerAddress){
|
||||
return
|
||||
}
|
||||
|
||||
const label = document.createElement('label')
|
||||
label.className = 'launcherFieldLabel'
|
||||
label.textContent = '접속 주소'
|
||||
|
||||
const input = document.createElement('input')
|
||||
input.className = 'launcherFieldInput'
|
||||
input.type = 'text'
|
||||
input.placeholder = profile.defaultServerAddress || 'example.com:25565'
|
||||
input.value = ConfigManager.getLibraryServerAddressOverride(profile.id) ?? ''
|
||||
input.addEventListener('change', () => {
|
||||
CatalogManager.setServerAddressOverride(profile.id, input.value)
|
||||
})
|
||||
|
||||
fieldGroup.appendChild(label)
|
||||
fieldGroup.appendChild(input)
|
||||
}
|
||||
|
||||
function appendPublishedAddressField(profile, hostState, fieldGroup){
|
||||
if(!hostState.publishedAddress){
|
||||
return
|
||||
}
|
||||
|
||||
const label = document.createElement('label')
|
||||
label.className = 'launcherFieldLabel'
|
||||
label.textContent = '호스트 공개 주소'
|
||||
|
||||
const row = document.createElement('div')
|
||||
row.className = 'launcherInlineField'
|
||||
|
||||
const input = document.createElement('input')
|
||||
input.className = 'launcherFieldInput'
|
||||
input.type = 'text'
|
||||
input.readOnly = true
|
||||
input.value = hostState.publishedAddress
|
||||
|
||||
const copyButton = document.createElement('button')
|
||||
copyButton.className = 'launcherSecondaryButton'
|
||||
copyButton.textContent = '주소 복사'
|
||||
copyButton.addEventListener('click', () => {
|
||||
clipboard.writeText(hostState.publishedAddress)
|
||||
})
|
||||
|
||||
row.appendChild(input)
|
||||
row.appendChild(copyButton)
|
||||
fieldGroup.appendChild(label)
|
||||
fieldGroup.appendChild(row)
|
||||
}
|
||||
|
||||
async function renderLibraryView(){
|
||||
libraryList.innerHTML = ''
|
||||
|
||||
try {
|
||||
const installedProfiles = await CatalogManager.getInstalledProfiles()
|
||||
const selectedProfileId = CatalogManager.getSelectedProfileId()
|
||||
|
||||
renderLibraryEmptyState(installedProfiles.length === 0)
|
||||
|
||||
for(const profile of installedProfiles){
|
||||
const hostState = ServerRuntime.getHostedProfileState(profile.id)
|
||||
const card = document.createElement('article')
|
||||
card.className = 'launcherCard'
|
||||
if(profile.id === selectedProfileId){
|
||||
card.setAttribute('selected', 'true')
|
||||
}
|
||||
|
||||
const header = document.createElement('div')
|
||||
header.className = 'launcherCardHeader'
|
||||
|
||||
const titleGroup = document.createElement('div')
|
||||
titleGroup.className = 'launcherCardTitleGroup'
|
||||
|
||||
const title = document.createElement('h3')
|
||||
title.className = 'launcherCardTitle'
|
||||
title.textContent = profile.name
|
||||
|
||||
const meta = document.createElement('div')
|
||||
meta.className = 'launcherCardMeta'
|
||||
meta.appendChild(createBadge(describeProfileKind(profile.kind)))
|
||||
if(profile.isCustom){
|
||||
meta.appendChild(createBadge('커스텀'))
|
||||
}
|
||||
if(profile.id === selectedProfileId){
|
||||
meta.appendChild(createBadge('선택됨'))
|
||||
}
|
||||
if(profile.kind === 'map' && profile.worldDirectoryName){
|
||||
meta.appendChild(createBadge(profile.worldDirectoryName))
|
||||
}
|
||||
if(profile.kind === 'map' && !profile.launchReady){
|
||||
meta.appendChild(createBadge('맵 설정 필요'))
|
||||
}
|
||||
if(profile.kind === 'server-pack' && !profile.hostReady){
|
||||
meta.appendChild(createBadge('호스팅 설정 필요'))
|
||||
}
|
||||
if(hostState.running){
|
||||
meta.appendChild(createBadge(hostState.tunneling ? '서버+터널' : '서버 실행 중'))
|
||||
}
|
||||
|
||||
titleGroup.appendChild(title)
|
||||
titleGroup.appendChild(meta)
|
||||
header.appendChild(titleGroup)
|
||||
|
||||
const description = createParagraph('launcherCardDescription', profile.description || '설명이 없습니다.')
|
||||
|
||||
const infoBlock = document.createElement('div')
|
||||
infoBlock.className = 'launcherInfoBlock'
|
||||
infoBlock.appendChild(createInfoLine('자료 상태', describeAssetState(profile)))
|
||||
infoBlock.appendChild(createInfoLine('실행 준비', profile.launchReady ? '완료' : '추가 설정 필요'))
|
||||
if(profile.defaultServerAddress){
|
||||
infoBlock.appendChild(createInfoLine('기본 주소', profile.defaultServerAddress))
|
||||
}
|
||||
if(profile.kind === 'server-pack'){
|
||||
infoBlock.appendChild(createInfoLine('로컬 호스팅', profile.hostReady ? '가능' : '서버 번들 필요'))
|
||||
}
|
||||
if(hostState.running){
|
||||
infoBlock.appendChild(createInfoLine('호스트 상태', hostState.tunneling ? '터널 연결 중' : '로컬 서버 실행 중'))
|
||||
}
|
||||
if(profile.launchIssues?.length > 0){
|
||||
infoBlock.appendChild(createInfoLine('확인 필요', profile.launchIssues[0]))
|
||||
} else if(profile.hostIssues?.length > 0){
|
||||
infoBlock.appendChild(createInfoLine('호스팅 확인', profile.hostIssues[0]))
|
||||
}
|
||||
|
||||
const fieldGroup = document.createElement('div')
|
||||
fieldGroup.className = 'launcherFieldGroup'
|
||||
appendAddressOverrideField(profile, fieldGroup)
|
||||
appendPublishedAddressField(profile, hostState, fieldGroup)
|
||||
|
||||
const actions = document.createElement('div')
|
||||
actions.className = 'launcherCardActions'
|
||||
|
||||
const prepareButton = document.createElement('button')
|
||||
prepareButton.className = 'launcherSecondaryButton'
|
||||
prepareButton.textContent = '자료 준비'
|
||||
prepareButton.addEventListener('click', async () => {
|
||||
await prepareProfileAssets(profile)
|
||||
})
|
||||
|
||||
const selectButton = document.createElement('button')
|
||||
selectButton.className = 'launcherSecondaryButton'
|
||||
selectButton.textContent = '프로필 선택'
|
||||
selectButton.disabled = !profile.configured
|
||||
selectButton.addEventListener('click', async () => {
|
||||
CatalogManager.selectProfile(profile.id)
|
||||
CatalogManager.applyConfiguredProfile()
|
||||
await renderLibraryView()
|
||||
})
|
||||
|
||||
const openButton = document.createElement('button')
|
||||
openButton.className = 'launcherSecondaryButton'
|
||||
openButton.textContent = '실행 화면'
|
||||
openButton.disabled = !profile.configured
|
||||
openButton.addEventListener('click', async () => {
|
||||
await activateProfile(profile, false)
|
||||
})
|
||||
|
||||
const launchButton = document.createElement('button')
|
||||
launchButton.className = 'launcherPrimaryButton'
|
||||
launchButton.textContent = profile.kind === 'map' ? '맵 실행' : '바로 실행'
|
||||
launchButton.disabled = !profile.configured
|
||||
launchButton.addEventListener('click', async () => {
|
||||
await activateProfile(profile, true)
|
||||
})
|
||||
|
||||
actions.appendChild(prepareButton)
|
||||
actions.appendChild(selectButton)
|
||||
actions.appendChild(openButton)
|
||||
actions.appendChild(launchButton)
|
||||
|
||||
if(profile.kind === 'server-pack'){
|
||||
const startHostButton = document.createElement('button')
|
||||
startHostButton.className = 'launcherSecondaryButton'
|
||||
startHostButton.textContent = '서버 실행'
|
||||
startHostButton.disabled = hostState.running || !profile.hostReady
|
||||
startHostButton.addEventListener('click', async () => {
|
||||
try {
|
||||
await ServerRuntime.startHostedProfile(profile)
|
||||
await renderLibraryView()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
showLibraryMessage('서버 실행 실패', '서버 번들이 준비되지 않았거나 시작 명령을 찾지 못했습니다.')
|
||||
}
|
||||
})
|
||||
|
||||
const stopHostButton = document.createElement('button')
|
||||
stopHostButton.className = 'launcherGhostButton'
|
||||
stopHostButton.textContent = '서버 중지'
|
||||
stopHostButton.disabled = !hostState.running
|
||||
stopHostButton.addEventListener('click', async () => {
|
||||
ServerRuntime.stopHostedProfile(profile.id)
|
||||
await renderLibraryView()
|
||||
})
|
||||
|
||||
actions.appendChild(startHostButton)
|
||||
actions.appendChild(stopHostButton)
|
||||
}
|
||||
|
||||
const removeButton = document.createElement('button')
|
||||
removeButton.className = 'launcherGhostButton'
|
||||
removeButton.textContent = '제거'
|
||||
removeButton.addEventListener('click', async () => {
|
||||
ServerRuntime.stopHostedProfile(profile.id)
|
||||
CatalogManager.removeProfile(profile.id)
|
||||
await renderLibraryView()
|
||||
if(typeof refreshInstallView === 'function'){
|
||||
await refreshInstallView()
|
||||
}
|
||||
})
|
||||
|
||||
actions.appendChild(removeButton)
|
||||
|
||||
card.appendChild(header)
|
||||
card.appendChild(description)
|
||||
card.appendChild(infoBlock)
|
||||
if(fieldGroup.childNodes.length > 0){
|
||||
card.appendChild(fieldGroup)
|
||||
}
|
||||
card.appendChild(actions)
|
||||
libraryList.appendChild(card)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
renderLibraryEmptyState(false)
|
||||
const errorCard = document.createElement('article')
|
||||
errorCard.className = 'launcherCard'
|
||||
errorCard.innerHTML = '<h3 class="launcherCardTitle">라이브러리 로드 실패</h3><p class="launcherCardDescription">선택한 카탈로그를 읽지 못했습니다. 설치 페이지에서 카탈로그 경로를 다시 확인하세요.</p>'
|
||||
libraryList.appendChild(errorCard)
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('libraryOpenInstallButton').addEventListener('click', async () => {
|
||||
if(typeof refreshInstallView === 'function'){
|
||||
await refreshInstallView()
|
||||
}
|
||||
switchView(getCurrentView(), VIEWS.install)
|
||||
})
|
||||
|
||||
document.getElementById('libraryOpenSettingsButton').addEventListener('click', async () => {
|
||||
await prepareSettings()
|
||||
switchView(getCurrentView(), VIEWS.settings)
|
||||
})
|
||||
|
||||
document.getElementById('libraryOpenLaunchButton').addEventListener('click', async () => {
|
||||
const selectedProfile = CatalogManager.getSelectedProfileSync()
|
||||
if(selectedProfile == null){
|
||||
switchView(getCurrentView(), VIEWS.install)
|
||||
return
|
||||
}
|
||||
await activateProfile(selectedProfile, false)
|
||||
})
|
||||
|
||||
setInterval(() => {
|
||||
if(getCurrentView() === VIEWS.library && ServerRuntime.hasRunningProfiles()){
|
||||
renderLibraryView()
|
||||
}
|
||||
}, 3000)
|
||||
|
||||
window.refreshLibraryView = renderLibraryView
|
||||
renderLibraryView()
|
||||
234
app/assets/js/scripts/login.js
Normal file
234
app/assets/js/scripts/login.js
Normal file
@@ -0,0 +1,234 @@
|
||||
/**
|
||||
* 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
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
let loginViewOnSuccess = VIEWS.library
|
||||
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.addMojangAccount(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, async () => {
|
||||
// Temporary workaround
|
||||
if(loginViewOnSuccess === VIEWS.settings){
|
||||
await prepareSettings()
|
||||
}
|
||||
loginViewOnSuccess = VIEWS.library // 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((displayableError) => {
|
||||
loginLoading(false)
|
||||
|
||||
let actualDisplayableError
|
||||
if(isDisplayableError(displayableError)) {
|
||||
msftLoginLogger.error('Error while logging in.', displayableError)
|
||||
actualDisplayableError = displayableError
|
||||
} else {
|
||||
// Uh oh.
|
||||
msftLoginLogger.error('Unhandled error during login.', displayableError)
|
||||
actualDisplayableError = Lang.queryJS('login.error.unknown')
|
||||
}
|
||||
|
||||
setOverlayContent(actualDisplayableError.title, actualDisplayableError.desc, Lang.queryJS('login.tryAgain'))
|
||||
setOverlayHandler(() => {
|
||||
formDisabled(false)
|
||||
toggleOverlay(false)
|
||||
})
|
||||
toggleOverlay(true)
|
||||
})
|
||||
|
||||
})
|
||||
50
app/assets/js/scripts/loginOptions.js
Normal file
50
app/assets/js/scripts/loginOptions.js
Normal file
@@ -0,0 +1,50 @@
|
||||
const loginOptionsCancelContainer = document.getElementById('loginOptionCancelContainer')
|
||||
const loginOptionMicrosoft = document.getElementById('loginOptionMicrosoft')
|
||||
const loginOptionMojang = document.getElementById('loginOptionMojang')
|
||||
const loginOptionsCancelButton = document.getElementById('loginOptionCancelButton')
|
||||
|
||||
let loginOptionsCancellable = false
|
||||
|
||||
let loginOptionsViewOnLoginSuccess
|
||||
let loginOptionsViewOnLoginCancel
|
||||
let loginOptionsViewOnCancel
|
||||
let loginOptionsViewCancelHandler
|
||||
|
||||
function loginOptionsCancelEnabled(val){
|
||||
if(val){
|
||||
$(loginOptionsCancelContainer).show()
|
||||
} else {
|
||||
$(loginOptionsCancelContainer).hide()
|
||||
}
|
||||
}
|
||||
|
||||
loginOptionMicrosoft.onclick = (e) => {
|
||||
switchView(getCurrentView(), VIEWS.waiting, 500, 500, () => {
|
||||
ipcRenderer.send(
|
||||
MSFT_OPCODE.OPEN_LOGIN,
|
||||
loginOptionsViewOnLoginSuccess,
|
||||
loginOptionsViewOnLoginCancel
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
loginOptionMojang.onclick = (e) => {
|
||||
switchView(getCurrentView(), VIEWS.login, 500, 500, () => {
|
||||
loginViewOnSuccess = loginOptionsViewOnLoginSuccess
|
||||
loginViewOnCancel = loginOptionsViewOnLoginCancel
|
||||
loginCancelEnabled(true)
|
||||
})
|
||||
}
|
||||
|
||||
loginOptionsCancelButton.onclick = (e) => {
|
||||
switchView(getCurrentView(), loginOptionsViewOnCancel, 500, 500, () => {
|
||||
// Clear login values (Mojang login)
|
||||
// No cleanup needed for Microsoft.
|
||||
loginUsername.value = ''
|
||||
loginPassword.value = ''
|
||||
if(loginOptionsViewCancelHandler != null){
|
||||
loginOptionsViewCancelHandler()
|
||||
loginOptionsViewCancelHandler = null
|
||||
}
|
||||
})
|
||||
}
|
||||
324
app/assets/js/scripts/overlay.js
Normal file
324
app/assets/js/scripts/overlay.js
Normal file
@@ -0,0 +1,324 @@
|
||||
/**
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleServerSelection(toggleState){
|
||||
await 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 = Lang.queryJS('overlay.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', async () => {
|
||||
const listings = document.getElementsByClassName('serverListing')
|
||||
for(let i=0; i<listings.length; i++){
|
||||
if(listings[i].hasAttribute('selected')){
|
||||
const serv = (await DistroAPI.getDistribution()).getServerById(listings[i].getAttribute('servid'))
|
||||
updateSelectedServer(serv)
|
||||
refreshServerStatus(true)
|
||||
toggleOverlay(false)
|
||||
return
|
||||
}
|
||||
}
|
||||
// None are selected? Not possible right? Meh, handle it.
|
||||
if(listings.length > 0){
|
||||
const serv = (await DistroAPI.getDistribution()).getServerById(listings[i].getAttribute('servid'))
|
||||
updateSelectedServer(serv)
|
||||
toggleOverlay(false)
|
||||
}
|
||||
})
|
||||
|
||||
document.getElementById('accountSelectConfirm').addEventListener('click', async () => {
|
||||
const listings = document.getElementsByClassName('accountListing')
|
||||
for(let i=0; i<listings.length; i++){
|
||||
if(listings[i].hasAttribute('selected')){
|
||||
const authAcc = ConfigManager.setSelectedAccount(listings[i].getAttribute('uuid'))
|
||||
ConfigManager.save()
|
||||
updateSelectedAccount(authAcc)
|
||||
if(getCurrentView() === VIEWS.settings) {
|
||||
await prepareSettings()
|
||||
}
|
||||
toggleOverlay(false)
|
||||
validateSelectedAccount()
|
||||
return
|
||||
}
|
||||
}
|
||||
// None are selected? Not possible right? Meh, handle it.
|
||||
if(listings.length > 0){
|
||||
const authAcc = ConfigManager.setSelectedAccount(listings[0].getAttribute('uuid'))
|
||||
ConfigManager.save()
|
||||
updateSelectedAccount(authAcc)
|
||||
if(getCurrentView() === VIEWS.settings) {
|
||||
await prepareSettings()
|
||||
}
|
||||
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<cListings.length; i++){
|
||||
if(cListings[i].hasAttribute('selected')){
|
||||
cListings[i].removeAttribute('selected')
|
||||
}
|
||||
}
|
||||
val.setAttribute('selected', '')
|
||||
document.activeElement.blur()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function setAccountListingHandlers(){
|
||||
const listings = Array.from(document.getElementsByClassName('accountListing'))
|
||||
listings.map((val) => {
|
||||
val.onclick = e => {
|
||||
if(val.hasAttribute('selected')){
|
||||
return
|
||||
}
|
||||
const cListings = document.getElementsByClassName('accountListing')
|
||||
for(let i=0; i<cListings.length; i++){
|
||||
if(cListings[i].hasAttribute('selected')){
|
||||
cListings[i].removeAttribute('selected')
|
||||
}
|
||||
}
|
||||
val.setAttribute('selected', '')
|
||||
document.activeElement.blur()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function populateServerListings(){
|
||||
const distro = await DistroAPI.getDistribution()
|
||||
const giaSel = ConfigManager.getSelectedServer()
|
||||
const servers = distro.servers
|
||||
let htmlString = ''
|
||||
for(const serv of servers){
|
||||
htmlString += `<button class="serverListing" servid="${serv.rawServer.id}" ${serv.rawServer.id === giaSel ? 'selected' : ''}>
|
||||
<img class="serverListingImg" src="${serv.rawServer.icon}"/>
|
||||
<div class="serverListingDetails">
|
||||
<span class="serverListingName">${serv.rawServer.name}</span>
|
||||
<span class="serverListingDescription">${serv.rawServer.description}</span>
|
||||
<div class="serverListingInfo">
|
||||
<div class="serverListingVersion">${serv.rawServer.minecraftVersion}</div>
|
||||
<div class="serverListingRevision">${serv.rawServer.version}</div>
|
||||
${serv.rawServer.mainServer ? `<div class="serverListingStarWrapper">
|
||||
<svg id="Layer_1" viewBox="0 0 107.45 104.74" width="20px" height="20px">
|
||||
<defs>
|
||||
<style>.cls-1{fill:#fff;}.cls-2{fill:none;stroke:#fff;stroke-miterlimit:10;}</style>
|
||||
</defs>
|
||||
<path class="cls-1" d="M100.93,65.54C89,62,68.18,55.65,63.54,52.13c2.7-5.23,18.8-19.2,28-27.55C81.36,31.74,63.74,43.87,58.09,45.3c-2.41-5.37-3.61-26.52-4.37-39-.77,12.46-2,33.64-4.36,39-5.7-1.46-23.3-13.57-33.49-20.72,9.26,8.37,25.39,22.36,28,27.55C39.21,55.68,18.47,62,6.52,65.55c12.32-2,33.63-6.06,39.34-4.9-.16,5.87-8.41,26.16-13.11,37.69,6.1-10.89,16.52-30.16,21-33.9,4.5,3.79,14.93,23.09,21,34C70,86.84,61.73,66.48,61.59,60.65,67.36,59.49,88.64,63.52,100.93,65.54Z"/>
|
||||
<circle class="cls-2" cx="53.73" cy="53.9" r="38"/>
|
||||
</svg>
|
||||
<span class="serverListingStarTooltip">${Lang.queryJS('settings.serverListing.mainServer')}</span>
|
||||
</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</button>`
|
||||
}
|
||||
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.length; i++){
|
||||
htmlString += `<button class="accountListing" uuid="${accounts[i].uuid}" ${i===0 ? 'selected' : ''}>
|
||||
<img src="https://mc-heads.net/head/${accounts[i].uuid}/40">
|
||||
<div class="accountListingName">${accounts[i].displayName}</div>
|
||||
</button>`
|
||||
}
|
||||
document.getElementById('accountSelectListScrollable').innerHTML = htmlString
|
||||
|
||||
}
|
||||
|
||||
async function prepareServerSelectionList(){
|
||||
await populateServerListings()
|
||||
setServerListingHandlers()
|
||||
}
|
||||
|
||||
function prepareAccountSelectionList(){
|
||||
populateAccountListings()
|
||||
setAccountListingHandlers()
|
||||
}
|
||||
1583
app/assets/js/scripts/settings.js
Normal file
1583
app/assets/js/scripts/settings.js
Normal file
File diff suppressed because it is too large
Load Diff
468
app/assets/js/scripts/uibinder.js
Normal file
468
app/assets/js/scripts/uibinder.js
Normal file
@@ -0,0 +1,468 @@
|
||||
/**
|
||||
* 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',
|
||||
library: '#libraryContainer',
|
||||
install: '#installContainer',
|
||||
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')}.png')`
|
||||
$('#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.library
|
||||
$(VIEWS.library).fadeIn(1000)
|
||||
} else {
|
||||
loginOptionsCancelEnabled(false)
|
||||
loginOptionsViewOnLoginSuccess = VIEWS.library
|
||||
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 || type === Type.FabricMod){
|
||||
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 || type === Type.FabricMod){
|
||||
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 || type === Type.FabricMod){
|
||||
// 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)
|
||||
}
|
||||
213
app/assets/js/scripts/uicore.js
Normal file
213
app/assets/js/scripts/uicore.js
Normal file
@@ -0,0 +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, shell, webFrame} = require('electron')
|
||||
const remote = require('@electron/remote')
|
||||
const isDev = require('./assets/js/isdev')
|
||||
const { LoggerUtil } = require('helios-core')
|
||||
const Lang = require('./assets/js/langloader')
|
||||
|
||||
const loggerUICore = LoggerUtil.getLogger('UICore')
|
||||
const loggerAutoUpdater = LoggerUtil.getLogger('AutoUpdater')
|
||||
|
||||
// Log deprecation and process warnings.
|
||||
process.traceProcessWarnings = true
|
||||
process.traceDeprecation = true
|
||||
|
||||
// Disable eval function.
|
||||
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.info('Checking for update..')
|
||||
settingsUpdateButtonStatus(Lang.queryJS('uicore.autoUpdate.checkingForUpdateButton'), true)
|
||||
break
|
||||
case 'update-available':
|
||||
loggerAutoUpdater.info('New update available', info.version)
|
||||
|
||||
if(process.platform === 'darwin'){
|
||||
info.darwindownload = `https://github.com/peunsu/MRSLauncher/releases/download/v${info.version}/MRS-Launcher-setup-${info.version}${process.arch === 'arm64' ? '-arm64' : '-x64'}.dmg`
|
||||
showUpdateUI(info)
|
||||
}
|
||||
|
||||
populateSettingsUpdateInformation(info)
|
||||
break
|
||||
case 'update-downloaded':
|
||||
loggerAutoUpdater.info('Update ' + info.version + ' ready to be installed.')
|
||||
settingsUpdateButtonStatus(Lang.queryJS('uicore.autoUpdate.installNowButton'), false, () => {
|
||||
if(!isDev){
|
||||
ipcRenderer.send('autoUpdateAction', 'installUpdateNow')
|
||||
}
|
||||
})
|
||||
showUpdateUI(info)
|
||||
break
|
||||
case 'update-not-available':
|
||||
loggerAutoUpdater.info('No new update found.')
|
||||
settingsUpdateButtonStatus(Lang.queryJS('uicore.autoUpdate.checkForUpdatesButton'))
|
||||
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.info('No suitable releases found.')
|
||||
} else if(info.code === 'ERR_XML_MISSED_ELEMENT'){
|
||||
loggerAutoUpdater.info('No releases found.')
|
||||
} else {
|
||||
loggerAutoUpdater.error('Error during update check..', info)
|
||||
loggerAutoUpdater.debug('Error Code:', info.code)
|
||||
}
|
||||
}
|
||||
break
|
||||
default:
|
||||
loggerAutoUpdater.info('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.info('UICore Initialized');
|
||||
})*/
|
||||
|
||||
document.addEventListener('readystatechange', function () {
|
||||
if (document.readyState === 'interactive'){
|
||||
loggerUICore.info('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()
|
||||
}
|
||||
})
|
||||
9
app/assets/js/scripts/welcome.js
Normal file
9
app/assets/js/scripts/welcome.js
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Script for welcome.ejs
|
||||
*/
|
||||
document.getElementById('welcomeButton').addEventListener('click', e => {
|
||||
loginOptionsCancelEnabled(false) // False by default, be explicit.
|
||||
loginOptionsViewOnLoginSuccess = VIEWS.library
|
||||
loginOptionsViewOnLoginCancel = VIEWS.loginOptions
|
||||
switchView(VIEWS.welcome, VIEWS.loginOptions)
|
||||
})
|
||||
Reference in New Issue
Block a user