리소스팩 간편설치기: - 베이스 리소스팩 다운로드 URL 에 encodeURIComponent 적용. "Puzzle Resource Pack (basic).zip" 같이 공백·괄호가 들어간 파일명 정상 처리. - 출력 경로를 %appdata%/.minecraft/resourcepacks/ → %appdata%/.mc_custom/ resourcepacks/ 로 변경 (renderer 안내문, openFolder, 빌드 출력 일괄). - 로드 직후 각 음악퀴즈의 베이스 등록 여부를 로그에 노출 (디버그용). - 베이스 다운로드 시 실제 URL 도 로그에 출력. 음악퀴즈 간편설치기: - mergeRamArgs: -Xms 가 기존에 없으면 추가하지 않도록 수정. clientMinRam 은 "유저 PC 사양 최소 요구치" 의미이지 JVM 초기 힙이 아님. -Xmx 는 계속 추천 RAM 으로 강제 갱신. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
272 lines
9.6 KiB
JavaScript
272 lines
9.6 KiB
JavaScript
'use strict'
|
|
|
|
const api = window.rpInstaller
|
|
|
|
const state = {
|
|
packs: [],
|
|
selectedKey: null,
|
|
installing: false,
|
|
installed: false,
|
|
resourcepackPath: ''
|
|
}
|
|
|
|
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 = '펼치기'
|
|
} else {
|
|
logViewer.style.height = ''
|
|
logToggle.textContent = '접기'
|
|
}
|
|
})
|
|
|
|
api.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 = '' }
|
|
|
|
// ── 1단계: 음악퀴즈 선택 ────────────────────────────
|
|
function renderStep1() {
|
|
setActiveStep(1)
|
|
clearPage()
|
|
var section = document.createElement('section')
|
|
section.className = 'page'
|
|
section.innerHTML =
|
|
'<h2>1단계. 음악퀴즈 선택</h2>' +
|
|
'<div id="packList" class="cardChoice"><p class="formMessage">목록을 불러오는 중...</p></div>' +
|
|
'<div class="actionRow"><span></span><button class="primaryBtn" id="next" disabled>다음</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">등록된 음악퀴즈가 없습니다.</p>'
|
|
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('active')
|
|
var verLabel = pack.mcVersion ? '마인크래프트 ' + escapeHtml(pack.mcVersion) + ' · ' : ''
|
|
card.innerHTML =
|
|
'<strong>' + escapeHtml(pack.name) + '</strong>' +
|
|
'<small>' + verLabel +
|
|
'음악 ' + pack.list.music.length + '곡 · 사진 ' + pack.list.images.length + '장</small>'
|
|
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 || '선택 실패')
|
|
})
|
|
})
|
|
|
|
api.loadPacks().then(function (packs) {
|
|
state.packs = packs || []
|
|
renderList()
|
|
}).catch(function (err) {
|
|
listEl.innerHTML = '<p class="formMessage error">목록 로드 실패: ' + escapeHtml(err.message || '') + '</p>'
|
|
})
|
|
}
|
|
|
|
// ── 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 =
|
|
'<h2>2단계. 리소스팩 설치</h2>' +
|
|
'<p class="formMessage">음악·사진을 받아 리소스팩을 만들고 ' +
|
|
'<code>%appdata%/.mc_custom/resourcepacks/</code> 에 자동 설치합니다.</p>' +
|
|
'<div class="prepRow">' +
|
|
' <span class="prepChip" id="chip-ytdlp">yt-dlp 준비</span>' +
|
|
' <span class="prepChip" id="chip-ffmpeg">ffmpeg 준비</span>' +
|
|
'</div>' +
|
|
'<div class="progressSection">' +
|
|
' <h3>음악 다운로드</h3>' +
|
|
' <div class="sectionSub" id="music-sub">' + musicTotal + '곡</div>' +
|
|
' <div class="progressGrid" id="musicGrid"></div>' +
|
|
'</div>' +
|
|
'<div class="progressSection">' +
|
|
' <h3>사진 다운로드</h3>' +
|
|
' <div class="sectionSub" id="image-sub">' + imageTotal + '장</div>' +
|
|
' <div class="progressGrid" id="imageGrid"></div>' +
|
|
'</div>' +
|
|
'<div class="progressSection">' +
|
|
' <h3>리소스팩 빌드</h3>' +
|
|
' <div class="sectionSub" id="pkg-sub">대기 중…</div>' +
|
|
'</div>' +
|
|
'<div class="actionRow">' +
|
|
' <span></span>' +
|
|
' <button class="dangerBtn" id="cancel">취소</button>' +
|
|
'</div>'
|
|
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 =
|
|
'<div class="cardTop"><span class="label">' + idx + '</span><span class="icon">○</span></div>' +
|
|
'<div class="bar"><span></span></div>' +
|
|
'<div class="pct">대기</div>'
|
|
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 = '완료'
|
|
if (icon) icon.textContent = '✓'
|
|
if (bar) bar.style.width = '100%'
|
|
} else if (status === 'error') {
|
|
if (pct) pct.textContent = '실패'
|
|
if (icon) icon.textContent = '✕'
|
|
} else if (status === 'running') {
|
|
if (pct) pct.textContent = Math.round(percent) + '%'
|
|
if (icon) icon.textContent = '⏳'
|
|
} else {
|
|
if (pct) pct.textContent = '대기'
|
|
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 ? '설치 완료' : (payload.message || '빌드 중…')
|
|
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('설치 실패: ' + ((err && err.message) || err))
|
|
renderStep1()
|
|
})
|
|
}
|
|
|
|
// ── 3단계: 완료 ────────────────────────────────────
|
|
function renderStep3() {
|
|
setActiveStep(3)
|
|
clearPage()
|
|
var section = document.createElement('section')
|
|
section.className = 'page'
|
|
section.innerHTML =
|
|
'<h2>3단계. 완료</h2>' +
|
|
'<p class="formMessage">리소스팩 설치를 완료했습니다.</p>' +
|
|
(state.resourcepackPath
|
|
? '<p class="formMessage"><code>' + escapeHtml(state.resourcepackPath) + '</code></p>'
|
|
: '') +
|
|
'<div class="actionRow">' +
|
|
' <button class="secondaryBtn" id="openFolder">리소스팩 폴더 열기</button>' +
|
|
' <button class="primaryBtn" id="finish">확인</button>' +
|
|
'</div>'
|
|
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 === '"' ? '"' : '''
|
|
})
|
|
}
|
|
|
|
renderStep1()
|