418 lines
16 KiB
JavaScript
418 lines
16 KiB
JavaScript
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()
|