'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 () { renderAgreement() }).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 || '' })) + '

' }) } // 약관 동의 페이지: 1단계 직후, 2단계 설치 진입 전에 노출. // v0.3.4~ : 사이트의 visibility 토글에 따라 표시할 약관이 결정된다. 명시적으로 빈 목록(terms:[]) // 정상 응답일 때만 단계를 건너뛰고, 네트워크/서버 오류는 차단 후 다시 시도 UI를 보여준다. function renderAgreement() { setActiveStep(1) clearPage() var loadingSection = document.createElement('section') loadingSection.className = 'page' loadingSection.innerHTML = '

' + escapeHtml(tt('agreement.heading')) + '

' + '

' + escapeHtml(tt('agreement.loading')) + '

' pageHost.appendChild(loadingSection) api.getTermsList().then(function (res) { if (!res || !res.ok) { showAgreementError((res && res.message) || 'unknown') return } var terms = (res.terms || []).map(function (t) { return { id: t.kind, tab: t.label } }) if (terms.length === 0) { renderStep2() return } clearPage() renderAgreementWithKinds(terms) }).catch(function (err) { showAgreementError(err && err.message ? err.message : 'unknown') }) } // 약관 목록을 못 받아왔을 때: 사용자에게 오류 + 다시 시도 옵션. 동의 없이 설치 단계로 // 자동 진입하지 않도록 next 버튼을 두지 않는다. function showAgreementError(message) { clearPage() var section = document.createElement('section') section.className = 'page' section.innerHTML = '

' + escapeHtml(tt('agreement.heading')) + '

' + '

' + escapeHtml(tt('agreement.listLoadFailed', { message: message })) + '

' + '
' + '' + '' + '
' pageHost.appendChild(section) section.querySelector('#back').addEventListener('click', renderStep1) section.querySelector('#retry').addEventListener('click', renderAgreement) } // 약관을 한 건씩 페이지로 보여준다. 각 약관마다 "해당 약관에 동의합니다" 체크가 있고, // 끝까지 스크롤해 읽어야 체크할 수 있으며, [다음] 을 누르면 다음 약관 페이지가 화면 // 맨 위에서부터 나온다. 마지막 약관까지 동의하면 다음 단계(step2)로 진행한다. function renderAgreementWithKinds(KINDS) { var idx = 0 var accepted = {} // kind -> true (뒤로 갔다 와도 동의 상태 유지) var cache = {} // kind -> 렌더된 HTML var section = document.createElement('section') section.className = 'page' pageHost.appendChild(section) function renderCurrent() { var k = KINDS[idx] var isLast = idx === KINDS.length - 1 section.innerHTML = '

' + escapeHtml(tt('agreement.heading')) + '

' + '

' + escapeHtml(tt('agreement.stepLabel', { current: idx + 1, total: KINDS.length, label: k.tab })) + '

' + '
' + escapeHtml(tt('agreement.loading')) + '
' + '
' + escapeHtml(tt('agreement.readToBottom')) + '
' + '' + '
' + '
' + ' ' + ' ' + '
' var body = section.querySelector('#agBody') var accept = section.querySelector('#agAccept') var nextBtn = section.querySelector('#next') var msg = section.querySelector('#agMsg') var hint = section.querySelector('#agHint') // 새 약관 페이지는 화면 맨 위에서부터 보이도록 스크롤을 올린다. if (pageHost) pageHost.scrollTop = 0 // 약관 본문이 정상 로드된 뒤에만 동의를 허용한다. 로드 실패 상태에서는 // 스크롤을 해도 동의 체크가 켜지지 않는다("무조건 약관을 읽도록"). var termLoaded = false function markReadable() { if (!termLoaded || !accept.disabled) return accept.disabled = false hint.textContent = '' } function checkScrolled() { if (body.scrollHeight - body.scrollTop - body.clientHeight <= 6) markReadable() } body.addEventListener('scroll', checkScrolled) accept.addEventListener('change', function () { accepted[k.id] = accept.checked nextBtn.disabled = !accept.checked if (accept.checked) { msg.textContent = ''; msg.classList.remove('error') } }) nextBtn.addEventListener('click', function () { if (!accept.checked) { msg.textContent = tt('agreement.agreeRequired') msg.classList.add('error') return } if (isLast) renderStep2() else { idx++; renderCurrent() } }) section.querySelector('#back').addEventListener('click', function () { if (idx === 0) renderStep1() else { idx--; renderCurrent() } }) function afterLoad() { termLoaded = true body.scrollTop = 0 if (accepted[k.id]) { accept.disabled = false accept.checked = true nextBtn.disabled = false hint.textContent = '' } setTimeout(function () { if (body.scrollHeight - body.clientHeight <= 6) markReadable() else checkScrolled() }, 0) } // 약관 본문 로드 실패: 동의 불가 상태를 유지하고 다시 시도만 허용한다. function showLoadError(message) { termLoaded = false accept.disabled = true accept.checked = false accepted[k.id] = false nextBtn.disabled = true hint.textContent = tt('agreement.readToBottom') body.innerHTML = '

' + escapeHtml(tt('agreement.loadFailed', { message: message || '' })) + '

' + '
' var retry = section.querySelector('#agRetry') if (retry) retry.addEventListener('click', loadTerm) } function loadTerm() { if (cache[k.id]) { body.innerHTML = cache[k.id] afterLoad() return } body.textContent = tt('agreement.loading') api.getTerm(k.id).then(function (res) { if (!res.ok) { showLoadError(res.message || '') return } var html = renderTermsMarkdown(res.content || '') cache[k.id] = html body.innerHTML = html afterLoad() }).catch(function (err) { showLoadError(err && err.message ? err.message : '') }) } loadTerm() } renderCurrent() } // 인스톨러용 미니 markdown 렌더러. 사이트 termsEditor 와 같은 규칙을 처리한다. function renderTermsMarkdown(src) { function escHtml(s) { return s.replace(/&/g, '&').replace(//g, '>') } function inline(s) { s = escHtml(s) s = s.replace(/`([^`]+)`/g, '$1') s = s.replace(/\*\*([^*]+)\*\*/g, '$1') s = s.replace(/(^|\W)\*([^*\n]+)\*(?=\W|$)/g, '$1$2') s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') s = s.replace(/(^|[\s(])(https?:\/\/[^\s)]+)/g, function (m, p, u) { return p + '' + u + '' }) return s } var lines = src.replace(/\r\n/g, '\n').split('\n') var out = [] var i = 0 var stack = null function closeList() { if (stack) { out.push(''); stack = null } } while (i < lines.length) { var line = lines[i] var fence = /^```(\w*)\s*$/.exec(line) if (fence) { closeList() var code = []; i += 1 while (i < lines.length && !/^```\s*$/.test(lines[i])) { code.push(lines[i]); i += 1 } if (i < lines.length) i += 1 out.push('
' + escHtml(code.join('\n')) + '
') continue } var togStart = /^:::toggle\s+(.+)$/.exec(line) if (togStart) { closeList() var summary = togStart[1]; var body2 = []; i += 1 while (i < lines.length && !/^:::\s*$/.test(lines[i])) { body2.push(lines[i]); i += 1 } if (i < lines.length) i += 1 out.push('
' + inline(summary) + '' + renderTermsMarkdown(body2.join('\n')) + '
') continue } var h = /^(#{1,6})\s+(.*)$/.exec(line) if (h) { closeList() out.push('' + inline(h[2]) + '') i += 1; continue } if (/^---+\s*$/.test(line)) { closeList(); out.push('
'); i += 1; continue } if (/^>\s?/.test(line)) { closeList() var q = [] while (i < lines.length && /^>\s?/.test(lines[i])) { q.push(lines[i].replace(/^>\s?/, '')); i += 1 } out.push('
' + renderTermsMarkdown(q.join('\n')) + '
') continue } var ol = /^\s*\d+\.\s+(.*)$/.exec(line) if (ol) { if (stack !== 'ol') { closeList(); out.push('
    '); stack = 'ol' } out.push('
  1. ' + inline(ol[1]) + '
  2. '); i += 1; continue } var ul = /^\s*[-*]\s+(.*)$/.exec(line) if (ul) { if (stack !== 'ul') { closeList(); out.push('