'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() })()