Scaffold resource pack installer entry
Add a second Electron entry under src/installer-rp/ + installer-rp/ launched by `npm run installer:rp`. It is structurally separate from the music quiz installer (own tsconfig, own preload, own renderer), shares the existing styles via a relative link, and exposes a three-step UI: pick a 음악퀴즈, run the install, then a 완료 page that can open the resourcepacks folder or quit. The install IPC handler currently scaffolds the flow per docs/add.md (yt-dlp prep → music download → image normalize → zip build → place at %appdata%/.minecraft/resourcepacks/) but the actual work is still TODO. Cancel/cleanup of %appdata%/.mc_custom/.temp/ is wired up so that future iterations can plug each step in without rewiring. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
27
installer-rp/index.html
Normal file
27
installer-rp/index.html
Normal file
@@ -0,0 +1,27 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>마인크래프트 음악퀴즈 리소스팩 간편설치기</title>
|
||||
<link rel="stylesheet" href="../installer/styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="appHeader">
|
||||
<h1>마인크래프트 음악퀴즈 리소스팩 간편설치기</h1>
|
||||
<ol class="stepIndicator" id="stepIndicator">
|
||||
<li data-step="1">1. 음악퀴즈</li>
|
||||
<li data-step="2">2. 설치</li>
|
||||
<li data-step="3">3. 완료</li>
|
||||
</ol>
|
||||
</header>
|
||||
|
||||
<main id="pageHost"></main>
|
||||
|
||||
<aside class="logViewer" id="logViewer" hidden>
|
||||
<header><h2>설치 로그</h2><button type="button" id="logToggle">접기</button></header>
|
||||
<pre id="logBody"></pre>
|
||||
</aside>
|
||||
|
||||
<script src="./renderer.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
184
installer-rp/renderer.js
Normal file
184
installer-rp/renderer.js
Normal file
@@ -0,0 +1,184 @@
|
||||
'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')
|
||||
card.innerHTML =
|
||||
'<strong>' + escapeHtml(pack.name) + '</strong>' +
|
||||
'<small>음악 ' + 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 section = document.createElement('section')
|
||||
section.className = 'page'
|
||||
section.innerHTML =
|
||||
'<h2>2단계. 리소스팩 설치</h2>' +
|
||||
'<p class="formMessage">아래 "다음"을 누르면 음악·사진을 받아 리소스팩을 만들고 ' +
|
||||
'<code>%appdata%/.minecraft/resourcepacks/</code> 에 넣습니다.</p>' +
|
||||
'<div class="actionRow">' +
|
||||
' <button class="secondaryBtn" id="prev">이전</button>' +
|
||||
' <button class="primaryBtn" id="start">다음</button>' +
|
||||
' <button class="secondaryBtn" id="cancel" hidden>취소</button>' +
|
||||
'</div>'
|
||||
pageHost.appendChild(section)
|
||||
var prevBtn = section.querySelector('#prev')
|
||||
var startBtn = section.querySelector('#start')
|
||||
var cancelBtn = section.querySelector('#cancel')
|
||||
|
||||
prevBtn.addEventListener('click', function () {
|
||||
if (state.installing) return
|
||||
renderStep1()
|
||||
})
|
||||
|
||||
startBtn.addEventListener('click', function () {
|
||||
if (state.installing) return
|
||||
state.installing = true
|
||||
startBtn.disabled = true
|
||||
prevBtn.disabled = true
|
||||
cancelBtn.hidden = false
|
||||
logViewer.hidden = false
|
||||
|
||||
api.startInstall().then(function (result) {
|
||||
state.installing = false
|
||||
state.installed = true
|
||||
state.resourcepackPath = (result && result.resourcepackPath) || ''
|
||||
renderStep3()
|
||||
}).catch(function (err) {
|
||||
state.installing = false
|
||||
startBtn.disabled = false
|
||||
prevBtn.disabled = false
|
||||
cancelBtn.hidden = true
|
||||
alert('설치 실패: ' + (err.message || err))
|
||||
})
|
||||
})
|
||||
|
||||
cancelBtn.addEventListener('click', function () {
|
||||
api.cancelInstall()
|
||||
})
|
||||
}
|
||||
|
||||
// ── 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()
|
||||
Reference in New Issue
Block a user