746 lines
30 KiB
JavaScript
746 lines
30 KiB
JavaScript
'use strict'
|
||
|
||
const installerApi = window.installer
|
||
|
||
// I18N 사전: locales/installer/ko-kr.json. 처음 한 번 메인 프로세스에서 받아오고
|
||
// 그 뒤로는 동기적으로 lookup. tt() 가 호출될 때 사전이 비어 있어도 키를 그대로 반환해
|
||
// 화면이 깨지지는 않는다.
|
||
var I18N = {}
|
||
function tt(key, params) {
|
||
var parts = key.split('.')
|
||
var cur = I18N
|
||
for (var i = 0; i < parts.length; i++) {
|
||
if (cur && typeof cur === 'object' && parts[i] in cur) cur = cur[parts[i]]
|
||
else { cur = null; break }
|
||
}
|
||
var tpl = (typeof cur === 'string') ? cur : key
|
||
if (!params) return tpl
|
||
return tpl.replace(/\{\{\s*(\w+)\s*\}\}/g, function (_m, name) {
|
||
return (name in params) ? String(params[name]) : ('{{' + name + '}}')
|
||
})
|
||
}
|
||
|
||
const state = {
|
||
packs: [],
|
||
selectedPackKey: null,
|
||
mode: null, // 'single' | 'multi'
|
||
serverInstall: {
|
||
path: '',
|
||
jdk: '',
|
||
eulaAccepted: false,
|
||
ram: null,
|
||
portStatus: null
|
||
},
|
||
client: {
|
||
installPlatform: true
|
||
},
|
||
finishToggles: {
|
||
desktopShortcut: true,
|
||
startServer: true,
|
||
startLauncher: true
|
||
},
|
||
stepDone: { 1: false, 2: false, 3: false, 4: false }
|
||
}
|
||
|
||
const pageHost = document.getElementById('pageHost')
|
||
const stepIndicator = document.getElementById('stepIndicator')
|
||
const logViewer = document.getElementById('logViewer')
|
||
const logBody = document.getElementById('logBody')
|
||
const logToggle = document.getElementById('logToggle')
|
||
|
||
function applyStaticI18n() {
|
||
document.title = tt('app.browserTitle')
|
||
var headerH1 = document.querySelector('.appHeader h1')
|
||
if (headerH1) headerH1.textContent = tt('app.headerTitle')
|
||
stepIndicator.querySelectorAll('li').forEach(function (item) {
|
||
var step = Number(item.getAttribute('data-step'))
|
||
item.textContent = tt('stepIndicator.step' + step)
|
||
})
|
||
var logHeader = logViewer.querySelector('h2')
|
||
if (logHeader) logHeader.textContent = tt('logViewer.title')
|
||
logToggle.textContent = tt('common.collapse')
|
||
}
|
||
|
||
logToggle.addEventListener('click', function () {
|
||
logViewer.classList.toggle('collapsed')
|
||
if (logViewer.classList.contains('collapsed')) {
|
||
logViewer.style.height = '36px'
|
||
logToggle.textContent = tt('common.expand')
|
||
} else {
|
||
logViewer.style.height = ''
|
||
logToggle.textContent = tt('common.collapse')
|
||
}
|
||
})
|
||
|
||
installerApi.onLog(function (line) {
|
||
logViewer.hidden = false
|
||
logBody.textContent += line + '\n'
|
||
logBody.scrollTop = logBody.scrollHeight
|
||
})
|
||
|
||
function setActiveStep(step) {
|
||
stepIndicator.querySelectorAll('li').forEach(function (item) {
|
||
var index = Number(item.getAttribute('data-step'))
|
||
item.classList.remove('active', 'done')
|
||
if (index < step) item.classList.add('done')
|
||
if (index === step) item.classList.add('active')
|
||
})
|
||
}
|
||
|
||
function clearPage() {
|
||
pageHost.innerHTML = ''
|
||
}
|
||
|
||
function renderStep1() {
|
||
setActiveStep(1)
|
||
clearPage()
|
||
var section = document.createElement('section')
|
||
section.className = 'page'
|
||
section.innerHTML =
|
||
'<h2>' + tt('step1.heading') + '</h2>' +
|
||
'<div id="packList" class="cardChoice"><p class="formMessage">' + tt('step1.loading') + '</p></div>' +
|
||
'<div class="actionRow"><span></span><button class="primaryBtn" id="next" disabled>' + tt('common.next') + '</button></div>'
|
||
pageHost.appendChild(section)
|
||
var listEl = section.querySelector('#packList')
|
||
var nextBtn = section.querySelector('#next')
|
||
|
||
function renderList() {
|
||
listEl.innerHTML = ''
|
||
if (state.packs.length === 0) {
|
||
listEl.innerHTML = '<p class="formMessage error">' + tt('step1.empty') + '</p>'
|
||
return
|
||
}
|
||
state.packs.forEach(function (pack) {
|
||
var btn = document.createElement('button')
|
||
btn.type = 'button'
|
||
btn.innerHTML = '<strong>' + pack.name + '</strong><br><small>' +
|
||
tt('step1.subtitle', { mc: pack.pack.mcVersion, platform: pack.pack.platform.type }) + '</small>'
|
||
if (state.selectedPackKey === pack.key) btn.classList.add('selected')
|
||
btn.addEventListener('click', function () {
|
||
state.selectedPackKey = pack.key
|
||
nextBtn.disabled = false
|
||
renderList()
|
||
})
|
||
listEl.appendChild(btn)
|
||
})
|
||
}
|
||
|
||
nextBtn.addEventListener('click', async function () {
|
||
if (!state.selectedPackKey) return
|
||
await installerApi.setSelectedPack(state.selectedPackKey)
|
||
state.stepDone[1] = true
|
||
renderStep2()
|
||
})
|
||
|
||
;(async function () {
|
||
try {
|
||
var packs = await installerApi.loadPacks()
|
||
state.packs = packs
|
||
renderList()
|
||
} catch (err) {
|
||
listEl.innerHTML = '<p class="formMessage error">' + tt('step1.fetchFailed', { message: err.message }) + '</p>'
|
||
}
|
||
})()
|
||
}
|
||
|
||
function renderStep2() {
|
||
setActiveStep(2)
|
||
clearPage()
|
||
var section = document.createElement('section')
|
||
section.className = 'page'
|
||
section.innerHTML =
|
||
'<h2>' + tt('step2.heading') + '</h2>' +
|
||
'<div class="cardChoice">' +
|
||
'<button id="single" type="button" data-mode="single"><strong>' + tt('step2.singleTitle') + '</strong><br><small>' + tt('step2.singleHint') + '</small></button>' +
|
||
'<button id="multi" type="button" data-mode="multi"><strong>' + tt('step2.multiTitle') + '</strong><br><small>' + tt('step2.multiHint') + '</small></button>' +
|
||
'</div>' +
|
||
'<div class="actionRow"><button class="secondaryBtn" id="back">' + tt('common.back') + '</button><button class="primaryBtn" id="next" disabled>' + tt('common.next') + '</button></div>'
|
||
pageHost.appendChild(section)
|
||
var nextBtn = section.querySelector('#next')
|
||
var modeButtons = section.querySelectorAll('[data-mode]')
|
||
|
||
function applySelection(mode) {
|
||
state.mode = mode
|
||
modeButtons.forEach(function (btn) {
|
||
if (btn.getAttribute('data-mode') === mode) btn.classList.add('selected')
|
||
else btn.classList.remove('selected')
|
||
})
|
||
nextBtn.disabled = false
|
||
}
|
||
|
||
modeButtons.forEach(function (btn) {
|
||
btn.addEventListener('click', function () {
|
||
applySelection(btn.getAttribute('data-mode'))
|
||
})
|
||
})
|
||
|
||
if (state.mode === 'single' || state.mode === 'multi') applySelection(state.mode)
|
||
|
||
nextBtn.addEventListener('click', function () {
|
||
if (!state.mode) return
|
||
state.stepDone[2] = true
|
||
if (state.mode === 'single') renderStep4()
|
||
else renderStep3()
|
||
})
|
||
section.querySelector('#back').addEventListener('click', renderStep1)
|
||
}
|
||
|
||
function renderStep3() {
|
||
setActiveStep(3)
|
||
clearPage()
|
||
var section = document.createElement('section')
|
||
section.className = 'page'
|
||
section.innerHTML =
|
||
'<h2>' + tt('step3.heading') + '</h2>' +
|
||
'<div class="subStep" id="subHost"></div>'
|
||
pageHost.appendChild(section)
|
||
var subHost = section.querySelector('#subHost')
|
||
|
||
function show31() { subHost.innerHTML = ''; renderSubStep31(subHost, renderStep2, show32) }
|
||
function show32() { subHost.innerHTML = ''; renderSubStep32(subHost, show31, show33) }
|
||
function show33() { subHost.innerHTML = ''; renderSubStep33(subHost, show32, show34) }
|
||
function show34() { subHost.innerHTML = ''; renderSubStep34(subHost, show33, show35) }
|
||
function show35() {
|
||
subHost.innerHTML = ''
|
||
renderSubStep35(subHost, show34, function () {
|
||
state.stepDone[3] = true
|
||
renderStep4()
|
||
})
|
||
}
|
||
show31()
|
||
}
|
||
|
||
function renderSubStep31(host, back, done) {
|
||
host.innerHTML =
|
||
'<h3>' + tt('step3.sub31.heading') + '</h3>' +
|
||
'<p class="formMessage">' + tt('step3.sub31.description') + '</p>' +
|
||
'<div class="fieldset"><label><input id="installPath" type="text" placeholder="C:\\MusicQuizServer" value="' + (state.serverInstall.path || '') + '" /></label>' +
|
||
'<button class="secondaryBtn" id="pickFolder">' + tt('step3.sub31.pickFolder') + '</button></div>' +
|
||
'<div class="formMessage" id="msg"></div>' +
|
||
'<div class="actionRow"><button class="secondaryBtn" id="back">' + tt('common.back') + '</button><button class="primaryBtn" id="next">' + tt('common.next') + '</button></div>'
|
||
var input = host.querySelector('#installPath')
|
||
var msg = host.querySelector('#msg')
|
||
host.querySelector('#pickFolder').addEventListener('click', async function () {
|
||
var picked = await installerApi.pickFolder()
|
||
if (picked) input.value = picked
|
||
})
|
||
host.querySelector('#back').addEventListener('click', back)
|
||
host.querySelector('#next').addEventListener('click', async function () {
|
||
var result = await installerApi.validateInstallPath(input.value.trim())
|
||
if (!result.ok) {
|
||
msg.textContent = result.message || tt('step3.sub31.invalidPath')
|
||
msg.classList.add('error')
|
||
return
|
||
}
|
||
msg.textContent = tt('step3.sub31.confirmed', { message: result.message })
|
||
msg.classList.remove('error')
|
||
msg.classList.add('success')
|
||
state.serverInstall.path = input.value.trim()
|
||
done()
|
||
})
|
||
}
|
||
|
||
function renderSubStep32(host, back, done) {
|
||
host.innerHTML =
|
||
'<h3>' + tt('step3.sub32.heading') + '</h3>' +
|
||
'<p class="formMessage">' + tt('step3.sub32.description') + '</p>' +
|
||
'<div class="fieldset"><label><input id="jdkPath" type="text" placeholder="C:\\Program Files\\Java\\jdk-17" value="' + (state.serverInstall.jdk || '') + '" /></label>' +
|
||
'<button class="secondaryBtn" id="pickJdk">' + tt('step3.sub32.pickFolder') + '</button>' +
|
||
'<button class="secondaryBtn" id="auto">' + tt('step3.sub32.auto') + '</button>' +
|
||
'<button class="secondaryBtn" id="install">' + tt('step3.sub32.install') + '</button></div>' +
|
||
'<div class="formMessage" id="msg"></div>' +
|
||
'<div class="actionRow"><button class="secondaryBtn" id="back">' + tt('common.back') + '</button><button class="primaryBtn" id="next">' + tt('common.next') + '</button></div>'
|
||
var input = host.querySelector('#jdkPath')
|
||
var msg = host.querySelector('#msg')
|
||
var installBtn = host.querySelector('#install')
|
||
var autoBtn = host.querySelector('#auto')
|
||
var pickBtn = host.querySelector('#pickJdk')
|
||
var nextBtn = host.querySelector('#next')
|
||
var installing = false
|
||
|
||
function setInstallingUi(on) {
|
||
installing = on
|
||
if (on) {
|
||
installBtn.textContent = tt('step3.sub32.installCancel')
|
||
installBtn.classList.remove('secondaryBtn')
|
||
installBtn.classList.add('dangerBtn')
|
||
autoBtn.disabled = true
|
||
pickBtn.disabled = true
|
||
nextBtn.disabled = true
|
||
input.disabled = true
|
||
} else {
|
||
installBtn.textContent = tt('step3.sub32.install')
|
||
installBtn.classList.remove('dangerBtn')
|
||
installBtn.classList.add('secondaryBtn')
|
||
autoBtn.disabled = false
|
||
pickBtn.disabled = false
|
||
nextBtn.disabled = false
|
||
input.disabled = false
|
||
}
|
||
}
|
||
|
||
autoBtn.addEventListener('click', async function () {
|
||
if (installing) return
|
||
var detect = await installerApi.detectJdk()
|
||
if (detect.found) {
|
||
input.value = detect.path
|
||
msg.textContent = tt('step3.sub32.found', { path: detect.path })
|
||
msg.classList.remove('error')
|
||
msg.classList.add('success')
|
||
} else {
|
||
msg.textContent = tt('step3.sub32.notFound')
|
||
msg.classList.remove('success')
|
||
msg.classList.add('error')
|
||
}
|
||
})
|
||
pickBtn.addEventListener('click', async function () {
|
||
if (installing) return
|
||
var picked = await installerApi.pickFolder()
|
||
if (picked) input.value = picked
|
||
})
|
||
installBtn.addEventListener('click', async function () {
|
||
if (installing) {
|
||
// 진행 중이면 취소.
|
||
msg.textContent = tt('step3.sub32.cancelRequested')
|
||
msg.classList.remove('success', 'error')
|
||
await installerApi.cancelJdkInstall()
|
||
return
|
||
}
|
||
setInstallingUi(true)
|
||
msg.classList.remove('success', 'error')
|
||
msg.textContent = tt('step3.sub32.downloading')
|
||
try {
|
||
var result = await installerApi.installJdk()
|
||
if (result.ok && result.path) {
|
||
input.value = result.path
|
||
state.serverInstall.jdk = result.path
|
||
msg.textContent = tt('step3.sub32.installComplete', { path: result.path })
|
||
msg.classList.add('success')
|
||
} else {
|
||
var raw = result.message || tt('common.unknownError')
|
||
msg.textContent = raw === '취소됨'
|
||
? tt('step3.sub32.installCanceled')
|
||
: tt('step3.sub32.installFailed', { message: raw })
|
||
msg.classList.add('error')
|
||
}
|
||
} catch (err) {
|
||
msg.textContent = tt('step3.sub32.installError', { message: (err && err.message) ? err.message : String(err) })
|
||
msg.classList.add('error')
|
||
} finally {
|
||
setInstallingUi(false)
|
||
}
|
||
})
|
||
host.querySelector('#back').addEventListener('click', function () {
|
||
if (installing) return
|
||
back()
|
||
})
|
||
nextBtn.addEventListener('click', function () {
|
||
if (installing) return
|
||
if (!input.value.trim()) {
|
||
msg.textContent = tt('step3.sub32.pathRequired')
|
||
msg.classList.add('error')
|
||
return
|
||
}
|
||
state.serverInstall.jdk = input.value.trim()
|
||
done()
|
||
})
|
||
;(async function () {
|
||
var detect = await installerApi.detectJdk()
|
||
if (detect.found && !input.value) {
|
||
input.value = detect.path
|
||
msg.textContent = tt('step3.sub32.autoDetected', { path: detect.path })
|
||
msg.classList.add('success')
|
||
} else if (!detect.found) {
|
||
msg.textContent = tt('step3.sub32.notFoundHint')
|
||
}
|
||
})()
|
||
}
|
||
|
||
function renderSubStep33(host, back, done) {
|
||
host.innerHTML =
|
||
'<h3>' + tt('step3.sub33.heading') + '</h3>' +
|
||
'<p class="formMessage">' + tt('step3.sub33.description') + '</p>' +
|
||
'<div class="formMessage" id="downloadStatus">' + tt('step3.sub33.waiting') + '</div>' +
|
||
'<div id="ramSection" hidden style="margin-top:14px;">' +
|
||
'<h4>' + tt('step3.sub33.ramHeading') + '</h4>' +
|
||
'<div class="formMessage" id="ramMsg">' + tt('step3.sub33.ramChecking') + '</div>' +
|
||
'</div>' +
|
||
'<div class="actionRow"><button class="secondaryBtn" id="back">' + tt('common.back') + '</button><button class="primaryBtn" id="next" disabled>' + tt('common.next') + '</button></div>'
|
||
|
||
var statusEl = host.querySelector('#downloadStatus')
|
||
var ramSection = host.querySelector('#ramSection')
|
||
var ramMsg = host.querySelector('#ramMsg')
|
||
var nextBtn = host.querySelector('#next')
|
||
|
||
host.querySelector('#back').addEventListener('click', back)
|
||
nextBtn.addEventListener('click', function () {
|
||
if (!state.serverInstall.eulaAccepted) return
|
||
done()
|
||
})
|
||
|
||
// 이미 통과했던 상태 복원: 사용자가 다음→이전으로 돌아왔을 때 재다운로드 강요하지 않는다.
|
||
if (state.serverInstall.eulaAccepted && state.serverInstall.ram) {
|
||
statusEl.textContent = tt('step3.sub33.doneSummary')
|
||
statusEl.classList.add('success')
|
||
showRamResult(state.serverInstall.ram)
|
||
nextBtn.disabled = false
|
||
return
|
||
}
|
||
|
||
// 페이지 진입 즉시 자동 다운로드
|
||
;(async function () {
|
||
state.serverInstall.eulaAccepted = false
|
||
nextBtn.disabled = true
|
||
statusEl.classList.remove('success', 'error')
|
||
statusEl.textContent = tt('step3.sub33.downloading')
|
||
try {
|
||
await installerApi.startServerInstall({
|
||
packKey: state.selectedPackKey,
|
||
installPath: state.serverInstall.path,
|
||
jdkPath: state.serverInstall.jdk
|
||
})
|
||
statusEl.textContent = tt('step3.sub33.eulaPrompt')
|
||
var accepted = await openEulaPopup(state.serverInstall.path)
|
||
if (!accepted) {
|
||
statusEl.textContent = tt('step3.sub33.eulaRejected')
|
||
statusEl.classList.add('error')
|
||
return
|
||
}
|
||
try {
|
||
await installerApi.acceptEula(state.serverInstall.path)
|
||
} catch (err) {
|
||
statusEl.textContent = tt('step3.sub33.eulaSaveFailed', { message: err.message })
|
||
statusEl.classList.add('error')
|
||
return
|
||
}
|
||
state.serverInstall.eulaAccepted = true
|
||
statusEl.textContent = tt('step3.sub33.doneSummary')
|
||
statusEl.classList.add('success')
|
||
var ram = await installerApi.checkRam(state.selectedPackKey)
|
||
state.serverInstall.ram = ram
|
||
showRamResult(ram)
|
||
if (ram.decision === 'tooLow') return
|
||
nextBtn.disabled = false
|
||
} catch (err) {
|
||
statusEl.textContent = tt('step3.sub33.downloadFailed', { message: (err && err.message) ? err.message : String(err) })
|
||
statusEl.classList.add('error')
|
||
}
|
||
})()
|
||
|
||
function showRamResult(result) {
|
||
ramSection.hidden = false
|
||
ramMsg.classList.remove('error', 'warn', 'success')
|
||
if (result.decision === 'tooLow') {
|
||
var pack = state.packs.find(function (p) { return p.key === state.selectedPackKey })
|
||
var minRam = pack ? pack.pack.serverMinRam : 0
|
||
ramMsg.innerHTML = tt('step3.sub33.ramTooLow', { system: result.systemRamMb, min: minRam })
|
||
ramMsg.classList.add('error')
|
||
} else if (result.decision === 'minOk') {
|
||
ramMsg.innerHTML = tt('step3.sub33.ramMinOk', { system: result.systemRamMb, applied: result.appliedRamMb })
|
||
ramMsg.classList.add('warn')
|
||
} else {
|
||
ramMsg.textContent = tt('step3.sub33.ramMaxOk', { system: result.systemRamMb, applied: result.appliedRamMb })
|
||
ramMsg.classList.add('success')
|
||
}
|
||
}
|
||
}
|
||
|
||
// EULA 동의 팝업. resolve(true) = 동의, resolve(false) = 비동의/창 닫힘.
|
||
async function openEulaPopup(installPath) {
|
||
var read = await installerApi.readEula(installPath)
|
||
var bodyHtml = ''
|
||
if (read.exists) {
|
||
bodyHtml = '<p class="formMessage">' + tt('step3.eulaModal.fromFile') + '</p>' +
|
||
'<pre class="eulaPre">' + escapeHtml(read.content) + '</pre>'
|
||
} else {
|
||
var fetched = await installerApi.fetchMinecraftEula()
|
||
if (fetched.html) {
|
||
bodyHtml = '<p class="formMessage">' + tt('step3.eulaModal.fromMojang', { url: fetched.url }) + '</p>' +
|
||
'<iframe class="eulaFrame" sandbox srcdoc="' + escapeAttr(fetched.html) + '"></iframe>'
|
||
} else {
|
||
bodyHtml = '<p class="formMessage error">' + tt('step3.eulaModal.loadFailed') + '</p>'
|
||
}
|
||
}
|
||
return new Promise(function (resolve) {
|
||
var overlay = document.createElement('div')
|
||
overlay.className = 'modalOverlay'
|
||
overlay.innerHTML =
|
||
'<div class="modalCard" role="dialog" aria-modal="true">' +
|
||
'<header><h3>' + tt('step3.eulaModal.title') + '</h3><button type="button" class="modalClose" aria-label="' + tt('common.close') + '">×</button></header>' +
|
||
'<div class="modalBody">' + bodyHtml + '</div>' +
|
||
'<footer class="actionRow">' +
|
||
'<button type="button" class="secondaryBtn" data-action="reject">' + tt('common.reject') + '</button>' +
|
||
'<button type="button" class="primaryBtn" data-action="accept">' + tt('common.agree') + '</button>' +
|
||
'</footer>' +
|
||
'</div>'
|
||
document.body.appendChild(overlay)
|
||
|
||
var settled = false
|
||
function close(result) {
|
||
if (settled) return
|
||
settled = true
|
||
overlay.remove()
|
||
resolve(result)
|
||
}
|
||
overlay.querySelector('[data-action="accept"]').addEventListener('click', function () { close(true) })
|
||
overlay.querySelector('[data-action="reject"]').addEventListener('click', function () { close(false) })
|
||
overlay.querySelector('.modalClose').addEventListener('click', function () { close(false) })
|
||
overlay.addEventListener('click', function (event) {
|
||
if (event.target === overlay) close(false)
|
||
})
|
||
})
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
return String(text).replace(/[&<>"']/g, function (ch) {
|
||
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ch]
|
||
})
|
||
}
|
||
|
||
function escapeAttr(text) {
|
||
return String(text).replace(/&/g, '&').replace(/"/g, '"')
|
||
}
|
||
|
||
function renderSubStep34(host, back, done) {
|
||
host.innerHTML =
|
||
'<h3>' + tt('step3.sub34.heading') + '</h3>' +
|
||
'<p class="formMessage">' + tt('step3.sub34.description') + '</p>' +
|
||
'<button class="secondaryBtn" id="open">' + tt('step3.sub34.open') + '</button>' +
|
||
'<div class="formMessage" id="editorMsg"></div>' +
|
||
'<div class="actionRow"><button class="secondaryBtn" id="back">' + tt('common.back') + '</button><button class="primaryBtn" id="next">' + tt('common.next') + '</button></div>'
|
||
host.querySelector('#open').addEventListener('click', async function () {
|
||
var msg = host.querySelector('#editorMsg')
|
||
try {
|
||
var result = await installerApi.startServerConfigEditor(state.serverInstall.path)
|
||
msg.innerHTML = tt('step3.sub34.openedAt', { url: result.url })
|
||
} catch (err) {
|
||
msg.textContent = tt('step3.sub34.openFailed', { message: err.message })
|
||
msg.classList.add('error')
|
||
}
|
||
})
|
||
host.querySelector('#back').addEventListener('click', back)
|
||
host.querySelector('#next').addEventListener('click', done)
|
||
}
|
||
|
||
function renderSubStep35(host, back, done) {
|
||
host.innerHTML =
|
||
'<h3>' + tt('step3.sub35.heading') + '</h3>' +
|
||
'<p class="formMessage">' + tt('step3.sub35.description') + '</p>' +
|
||
'<div class="fieldset"><label>' + tt('step3.sub35.portLabel') + ' <input id="port" type="text" value="25565" /></label></div>' +
|
||
'<button class="secondaryBtn" id="run">' + tt('step3.sub35.recheck') + '</button>' +
|
||
'<div class="formMessage" id="resultMsg"></div>' +
|
||
'<div class="actionRow"><button class="secondaryBtn" id="back">' + tt('common.back') + '</button><button class="primaryBtn" id="next" disabled>' + tt('common.next') + '</button></div>'
|
||
var resultMsg = host.querySelector('#resultMsg')
|
||
var nextBtn = host.querySelector('#next')
|
||
var runBtn = host.querySelector('#run')
|
||
host.querySelector('#back').addEventListener('click', back)
|
||
|
||
async function runCheck() {
|
||
runBtn.disabled = true
|
||
resultMsg.classList.remove('success', 'warn', 'error')
|
||
resultMsg.textContent = tt('step3.sub35.checking')
|
||
var port = Number(host.querySelector('#port').value) || 25565
|
||
try {
|
||
var result = await installerApi.checkPortForward(port)
|
||
state.serverInstall.portStatus = result
|
||
if (result.status === 'preForwarded') {
|
||
resultMsg.innerHTML = tt('step3.sub35.preForwarded', { ip: result.externalIp, port: result.port })
|
||
resultMsg.classList.add('success')
|
||
} else if (result.status === 'upnpOk') {
|
||
resultMsg.innerHTML = tt('step3.sub35.upnpOk', { ip: result.externalIp, port: result.port })
|
||
resultMsg.classList.add('success')
|
||
} else {
|
||
var ip = result.externalIp || tt('step3.sub35.ipUnknown')
|
||
resultMsg.innerHTML = (result.message || tt('step3.sub35.manualHint')) +
|
||
tt('step3.sub35.manualDetail', { ip: ip, port: result.port })
|
||
resultMsg.classList.add('warn')
|
||
}
|
||
nextBtn.disabled = false
|
||
} catch (err) {
|
||
resultMsg.textContent = tt('step3.sub35.checkFailed', { message: (err && err.message) ? err.message : String(err) })
|
||
resultMsg.classList.add('error')
|
||
} finally {
|
||
runBtn.disabled = false
|
||
}
|
||
}
|
||
|
||
runBtn.addEventListener('click', runCheck)
|
||
nextBtn.addEventListener('click', done)
|
||
// 페이지 진입 즉시 자동 점검
|
||
runCheck()
|
||
}
|
||
|
||
function renderStep4() {
|
||
setActiveStep(4)
|
||
clearPage()
|
||
var pack = state.packs.find(function (p) { return p.key === state.selectedPackKey })
|
||
var section = document.createElement('section')
|
||
section.className = 'page'
|
||
section.innerHTML =
|
||
'<h2>' + tt('step4.heading') + '</h2>' +
|
||
'<div class="subStep" id="subHost"></div>'
|
||
pageHost.appendChild(section)
|
||
var subHost = section.querySelector('#subHost')
|
||
function backToPrevStep() { if (state.mode === 'multi') renderStep3(); else renderStep2() }
|
||
|
||
function show41() { subHost.innerHTML = ''; renderSubStep41(subHost, pack, backToPrevStep, show42) }
|
||
function show42() { subHost.innerHTML = ''; renderSubStep42(subHost, show41, show43) }
|
||
function show43() {
|
||
subHost.innerHTML = ''
|
||
renderSubStep43(subHost, show42, function () {
|
||
state.stepDone[4] = true
|
||
renderStep5()
|
||
})
|
||
}
|
||
show41()
|
||
}
|
||
|
||
function renderSubStep41(host, pack, back, done) {
|
||
var platformType = pack ? pack.pack.platform.type : 'vanilla'
|
||
if (platformType === 'vanilla') {
|
||
state.client.installPlatform = false
|
||
host.innerHTML =
|
||
'<h3>' + tt('step4.sub41.heading') + '</h3>' +
|
||
'<p class="formMessage">' + tt('step4.sub41.vanillaInfo') + '</p>' +
|
||
'<p class="formMessage">' + tt('step4.sub41.vanillaNoInstall') + '</p>' +
|
||
'<div class="actionRow"><button class="secondaryBtn" id="back">' + tt('common.back') + '</button><button class="primaryBtn" id="next">' + tt('common.next') + '</button></div>'
|
||
host.querySelector('#back').addEventListener('click', back)
|
||
host.querySelector('#next').addEventListener('click', done)
|
||
return
|
||
}
|
||
|
||
host.innerHTML =
|
||
'<h3>' + tt('step4.sub41.heading') + '</h3>' +
|
||
'<p class="formMessage">' + tt('step4.sub41.info', { platform: platformType }) + '</p>' +
|
||
'<div class="cardChoice">' +
|
||
'<button type="button" data-choice="install"><strong>' + tt('step4.sub41.installTitle') + '</strong><br><small>' + tt('step4.sub41.installHint', { platform: platformType }) + '</small></button>' +
|
||
'<button type="button" data-choice="skip"><strong>' + tt('step4.sub41.skipTitle') + '</strong><br><small>' + tt('step4.sub41.skipHint') + '</small></button>' +
|
||
'</div>' +
|
||
'<div class="actionRow"><button class="secondaryBtn" id="back">' + tt('common.back') + '</button><button class="primaryBtn" id="next" disabled>' + tt('common.next') + '</button></div>'
|
||
|
||
var nextBtn = host.querySelector('#next')
|
||
var choiceButtons = host.querySelectorAll('[data-choice]')
|
||
|
||
function applyChoice(choice) {
|
||
state.client.installPlatform = choice === 'install'
|
||
choiceButtons.forEach(function (btn) {
|
||
if (btn.getAttribute('data-choice') === choice) btn.classList.add('selected')
|
||
else btn.classList.remove('selected')
|
||
})
|
||
nextBtn.disabled = false
|
||
}
|
||
|
||
choiceButtons.forEach(function (btn) {
|
||
btn.addEventListener('click', function () {
|
||
applyChoice(btn.getAttribute('data-choice'))
|
||
})
|
||
})
|
||
|
||
if (typeof state.client.installPlatform === 'boolean') {
|
||
applyChoice(state.client.installPlatform ? 'install' : 'skip')
|
||
}
|
||
|
||
host.querySelector('#back').addEventListener('click', back)
|
||
nextBtn.addEventListener('click', done)
|
||
}
|
||
|
||
function renderSubStep42(host, back, done) {
|
||
host.innerHTML =
|
||
'<h3>' + tt('step4.sub42.heading') + '</h3>' +
|
||
'<p class="formMessage">' + tt('step4.sub42.description') + '</p>' +
|
||
'<div class="formMessage" id="msg">' + tt('step4.sub42.installing') + '</div>' +
|
||
'<div class="actionRow"><button class="secondaryBtn" id="back">' + tt('common.back') + '</button><button class="primaryBtn" id="next" disabled>' + tt('common.next') + '</button></div>'
|
||
var msg = host.querySelector('#msg')
|
||
var nextBtn = host.querySelector('#next')
|
||
host.querySelector('#back').addEventListener('click', back)
|
||
nextBtn.addEventListener('click', done)
|
||
|
||
// 이미 설치됐다면 다시 돌리지 않음
|
||
if (state.client.clientInstalled) {
|
||
msg.textContent = tt('step4.sub42.done')
|
||
msg.classList.add('success')
|
||
nextBtn.disabled = false
|
||
return
|
||
}
|
||
|
||
// 페이지 진입 즉시 자동 설치
|
||
;(async function () {
|
||
try {
|
||
await installerApi.installClient({
|
||
packKey: state.selectedPackKey,
|
||
installPlatform: !!state.client.installPlatform
|
||
})
|
||
msg.textContent = tt('step4.sub42.done')
|
||
msg.classList.add('success')
|
||
state.client.clientInstalled = true
|
||
nextBtn.disabled = false
|
||
} catch (err) {
|
||
msg.textContent = tt('step4.sub42.failed', { message: (err && err.message) ? err.message : String(err) })
|
||
msg.classList.add('error')
|
||
}
|
||
})()
|
||
}
|
||
|
||
function renderSubStep43(host, back, done) {
|
||
host.innerHTML =
|
||
'<h3>' + tt('step4.sub43.heading') + '</h3>' +
|
||
'<p class="formMessage">' + tt('step4.sub43.description') + '</p>' +
|
||
'<div class="actionRow"><button class="secondaryBtn" id="back">' + tt('common.back') + '</button><button class="primaryBtn" id="next">' + tt('step4.sub43.goStep5') + '</button></div>'
|
||
host.querySelector('#back').addEventListener('click', back)
|
||
host.querySelector('#next').addEventListener('click', done)
|
||
}
|
||
|
||
function renderStep5() {
|
||
setActiveStep(5)
|
||
clearPage()
|
||
var section = document.createElement('section')
|
||
section.className = 'page'
|
||
var multi = state.mode === 'multi'
|
||
section.innerHTML =
|
||
'<h2>' + tt('step5.heading') + '</h2>' +
|
||
'<p>' + tt('step5.summary') + '</p>' +
|
||
(multi ? '<div class="subStep">' +
|
||
'<h3>' + tt('step5.serverHeading') + '</h3>' +
|
||
'<button class="secondaryBtn" id="openFolder">' + tt('step5.openServerFolder') + '</button>' +
|
||
'<label class="toggleRow"><input type="checkbox" id="shortcut" checked /> ' + tt('step5.shortcut') + '</label>' +
|
||
'<label class="toggleRow"><input type="checkbox" id="startServer" checked /> ' + tt('step5.startServer') + '</label>' +
|
||
'</div>' : '') +
|
||
'<div class="subStep">' +
|
||
'<h3>' + tt('step5.launcherHeading') + '</h3>' +
|
||
'<label class="toggleRow"><input type="checkbox" id="startLauncher" checked /> ' + tt('step5.startLauncher') + '</label>' +
|
||
'</div>' +
|
||
'<div class="actionRow"><button class="secondaryBtn" id="back">' + tt('common.back') + '</button><button class="primaryBtn" id="finish">' + tt('step5.finish') + '</button></div>'
|
||
pageHost.appendChild(section)
|
||
section.querySelector('#back').addEventListener('click', renderStep4)
|
||
if (multi) {
|
||
section.querySelector('#openFolder').addEventListener('click', function () {
|
||
installerApi.openServerFolder()
|
||
})
|
||
}
|
||
section.querySelector('#finish').addEventListener('click', async function () {
|
||
var finishBtn = section.querySelector('#finish')
|
||
finishBtn.disabled = true
|
||
finishBtn.textContent = tt('step5.finishing')
|
||
try {
|
||
if (multi) {
|
||
if (section.querySelector('#shortcut').checked) await installerApi.createDesktopShortcut()
|
||
if (section.querySelector('#startServer').checked) await installerApi.startServer()
|
||
}
|
||
if (section.querySelector('#startLauncher').checked) await installerApi.startMinecraftLauncher()
|
||
} catch (err) {
|
||
// 마무리 액션 실패는 무시하고 종료 진행
|
||
}
|
||
finishBtn.textContent = tt('step5.finished')
|
||
if (installerApi.quitApp) installerApi.quitApp()
|
||
})
|
||
}
|
||
|
||
// 시작 진입점: 사전을 먼저 받아서 정적 텍스트 갱신 후 첫 페이지 렌더.
|
||
;(async function () {
|
||
try {
|
||
I18N = (await installerApi.loadLocale()) || {}
|
||
} catch (_) { I18N = {} }
|
||
applyStaticI18n()
|
||
renderStep1()
|
||
})()
|