'use strict'
const api = window.rpInstaller
const state = {
packs: [],
selectedKey: null,
installing: false,
installed: false,
resourcepackPath: ''
}
let I18N = {}
function tt(key, params) {
var parts = String(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 {
return key
}
}
if (typeof cur !== 'string') return key
if (!params) return cur
return cur.replace(/\{\{\s*(\w+)\s*\}\}/g, function (_m, name) {
return name in params ? String(params[name]) : '{{' + name + '}}'
})
}
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')
logToggle.addEventListener('click', function () {
logViewer.classList.toggle('collapsed')
if (logViewer.classList.contains('collapsed')) {
logViewer.style.height = '36px'
logToggle.textContent = tt('logViewer.expand')
} else {
logViewer.style.height = ''
logToggle.textContent = tt('logViewer.collapse')
}
})
api.onLog(function (line) {
logViewer.hidden = false
logBody.textContent += line + '\n'
logBody.scrollTop = logBody.scrollHeight
})
function applyStaticI18n() {
document.title = tt('app.title')
var h1 = document.querySelector('.appHeader h1')
if (h1) h1.textContent = tt('app.title')
var stepLis = stepIndicator.querySelectorAll('li')
stepLis.forEach(function (item) {
var idx = item.getAttribute('data-step')
if (idx === '1') item.textContent = tt('stepIndicator.step1')
else if (idx === '2') item.textContent = tt('stepIndicator.step2')
else if (idx === '3') item.textContent = tt('stepIndicator.step3')
})
var logH2 = logViewer.querySelector('header h2')
if (logH2) logH2.textContent = tt('logViewer.heading')
logToggle.textContent = tt('logViewer.collapse')
}
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 = '' }
// ── 1단계: 음악퀴즈 선택 ────────────────────────────
function renderStep1() {
setActiveStep(1)
clearPage()
var section = document.createElement('section')
section.className = 'page'
section.innerHTML =
'
' + escapeHtml(tt('step1.heading')) + '
' +
'' + escapeHtml(tt('common.loading')) + '
' +
'
'
pageHost.appendChild(section)
var listEl = section.querySelector('#packList')
var nextBtn = section.querySelector('#next')
function renderList() {
listEl.innerHTML = ''
if (state.packs.length === 0) {
listEl.innerHTML = '' + escapeHtml(tt('common.noPacks')) + '
'
return
}
state.packs.forEach(function (pack) {
var card = document.createElement('button')
card.type = 'button'
card.className = 'choiceCard'
if (state.selectedKey === pack.key) card.classList.add('selected')
var verLabel = pack.mcVersion
? escapeHtml(tt('common.mcVersionLabel', { version: pack.mcVersion }))
: ''
card.innerHTML =
'' + escapeHtml(pack.name) + '' +
'' + verLabel +
escapeHtml(tt('common.trackImageCount', { music: pack.list.music.length, image: pack.list.images.length })) +
''
card.addEventListener('click', function () {
state.selectedKey = pack.key
nextBtn.disabled = false
renderList()
})
listEl.appendChild(card)
})
}
nextBtn.addEventListener('click', function () {
if (!state.selectedKey) return
api.selectPack(state.selectedKey).then(function () {
renderStep2()
}).catch(function (err) {
alert(err.message || tt('common.selectFailed'))
})
})
api.loadPacks().then(function (packs) {
state.packs = packs || []
renderList()
}).catch(function (err) {
listEl.innerHTML = '' +
escapeHtml(tt('common.listLoadFailed', { message: err.message || '' })) +
'
'
})
}
// ── 2단계: 설치 진행 ────────────────────────────────
function renderStep2() {
setActiveStep(2)
clearPage()
var pack = null
for (var i = 0; i < state.packs.length; i++) {
if (state.packs[i].key === state.selectedKey) { pack = state.packs[i]; break }
}
var musicTotal = pack ? pack.list.music.length : 0
var imageTotal = pack ? pack.list.images.length : 0
var section = document.createElement('section')
section.className = 'page'
section.innerHTML =
'' + escapeHtml(tt('step2.heading')) + '
' +
'' + tt('step2.description') + '
' +
'' +
' ' + escapeHtml(tt('step2.chipYtdlp')) + '' +
' ' + escapeHtml(tt('step2.chipFfmpeg')) + '' +
'
' +
'' +
'
' + escapeHtml(tt('step2.musicHeading')) + '
' +
'
' + escapeHtml(tt('step2.musicSub', { count: musicTotal })) + '
' +
'
' +
'
' +
'' +
'
' + escapeHtml(tt('step2.imageHeading')) + '
' +
'
' + escapeHtml(tt('step2.imageSub', { count: imageTotal })) + '
' +
'
' +
'
' +
'' +
'
' + escapeHtml(tt('step2.packageHeading')) + '
' +
'
' + escapeHtml(tt('step2.packageWaiting')) + '
' +
'
' +
'' +
' ' +
' ' +
'
'
pageHost.appendChild(section)
var musicGrid = section.querySelector('#musicGrid')
var imageGrid = section.querySelector('#imageGrid')
var chipYtdlp = section.querySelector('#chip-ytdlp')
var chipFfmpeg = section.querySelector('#chip-ffmpeg')
var pkgSub = section.querySelector('#pkg-sub')
var cancelBtn = section.querySelector('#cancel')
function buildCard(idx) {
var card = document.createElement('div')
card.className = 'progressCard pending'
card.setAttribute('data-idx', String(idx))
card.innerHTML =
'' + idx + '○
' +
'
' +
'' + escapeHtml(tt('step2.cardWaiting')) + '
'
return card
}
for (var m = 1; m <= musicTotal; m++) musicGrid.appendChild(buildCard(m))
for (var k = 1; k <= imageTotal; k++) imageGrid.appendChild(buildCard(k))
function updateCard(grid, index, percent, status) {
var card = grid.querySelector('[data-idx="' + index + '"]')
if (!card) return
card.classList.remove('pending', 'running', 'done', 'error')
card.classList.add(status)
var bar = card.querySelector('.bar > span')
if (bar) bar.style.width = Math.max(0, Math.min(100, percent)) + '%'
var pct = card.querySelector('.pct')
var icon = card.querySelector('.icon')
if (status === 'done') {
if (pct) pct.textContent = tt('step2.cardDone')
if (icon) icon.textContent = '✓'
if (bar) bar.style.width = '100%'
} else if (status === 'error') {
if (pct) pct.textContent = tt('step2.cardError')
if (icon) icon.textContent = '✕'
} else if (status === 'running') {
if (pct) pct.textContent = Math.round(percent) + '%'
if (icon) icon.textContent = '⏳'
} else {
if (pct) pct.textContent = tt('step2.cardWaiting')
if (icon) icon.textContent = '○'
}
}
var stopProgress = api.onProgress(function (payload) {
if (!payload || typeof payload !== 'object') return
if (payload.phase === 'prep') {
if (payload.done) {
chipYtdlp.classList.remove('active'); chipYtdlp.classList.add('done')
chipFfmpeg.classList.remove('active'); chipFfmpeg.classList.add('done')
return
}
if (payload.message && payload.message.indexOf('yt-dlp') >= 0) {
chipYtdlp.classList.add('active')
} else if (payload.message && payload.message.indexOf('ffmpeg') >= 0) {
chipYtdlp.classList.remove('active'); chipYtdlp.classList.add('done')
chipFfmpeg.classList.add('active')
}
return
}
if (payload.phase === 'item') {
var grid = payload.kind === 'music' ? musicGrid : imageGrid
updateCard(grid, payload.index, payload.percent || 0, payload.status)
return
}
if (payload.phase === 'package') {
pkgSub.textContent = payload.done
? tt('step2.packageDone')
: (payload.message || tt('step2.packageBuilding'))
return
}
})
cancelBtn.addEventListener('click', function () {
if (!state.installing) return
cancelBtn.disabled = true
api.cancelInstall()
})
// 페이지 진입 즉시 설치 시작
state.installing = true
logViewer.hidden = false
api.startInstall().then(function (result) {
state.installing = false
state.installed = true
state.resourcepackPath = (result && result.resourcepackPath) || ''
if (stopProgress) stopProgress()
renderStep3()
}).catch(function (err) {
state.installing = false
if (stopProgress) stopProgress()
alert(tt('common.installFailed', { message: (err && err.message) || err }))
renderStep1()
})
}
// ── 3단계: 완료 ────────────────────────────────────
function renderStep3() {
setActiveStep(3)
clearPage()
var section = document.createElement('section')
section.className = 'page'
section.innerHTML =
'' + escapeHtml(tt('step3.heading')) + '
' +
'' + escapeHtml(tt('step3.message')) + '
' +
(state.resourcepackPath
? '' + escapeHtml(state.resourcepackPath) + '
'
: '') +
'' +
' ' +
' ' +
'
'
pageHost.appendChild(section)
section.querySelector('#openFolder').addEventListener('click', function () {
api.openResourcepackFolder()
})
section.querySelector('#finish').addEventListener('click', function () {
api.quit()
})
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, function (c) {
return c === '&' ? '&' : c === '<' ? '<' : c === '>' ? '>' : c === '"' ? '"' : '''
})
}
;(async function () {
try { I18N = (await api.loadLocale()) || {} } catch (_) { I18N = {} }
applyStaticI18n()
renderStep1()
})()