feat(installer): 최종 리소스팩 다운로드 진행률(퍼센트) 표시

"예" 선택 후 최종 리소스팩 다운로드 시 퍼센트/진행바를 보여준다.
- finalResourcepack:install 을 downloadFile → downloadStream 으로 바꿔 content-length
  기반 진행률을 계산하고 finalResourcepack:progress 이벤트로 렌더러에 전송.
- preload onFinalResourcepackProgress 구독 추가, 렌더러에 진행바+% 표시(완료 100%,
  실패 시 바 숨김). 0.4.17→0.4.18.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 00:11:19 +09:00
parent bce87f1362
commit e30d7418df
4 changed files with 54 additions and 3 deletions

View File

@@ -1133,20 +1133,47 @@ function downloadFinalResourcepack() {
section.innerHTML = section.innerHTML =
'<h2>' + tt('resourcepack.downloadHeading') + '</h2>' + '<h2>' + tt('resourcepack.downloadHeading') + '</h2>' +
'<div class="formMessage" id="rpMsg">' + tt('resourcepack.downloading') + '</div>' + '<div class="formMessage" id="rpMsg">' + tt('resourcepack.downloading') + '</div>' +
'<div id="rpBarWrap" style="margin-top:10px;height:16px;background:rgba(255,255,255,0.08);border:1px solid var(--border,#30363d);border-radius:8px;overflow:hidden;">' +
' <div id="rpBar" style="height:100%;width:0%;background:var(--accent,#6cf);transition:width 0.15s;"></div>' +
'</div>' +
'<div class="formMessage" id="rpPercent" style="margin-top:6px;">0%</div>' +
'<div class="actionRow" id="rpActions" hidden></div>' '<div class="actionRow" id="rpActions" hidden></div>'
pageHost.appendChild(section) pageHost.appendChild(section)
var msg = section.querySelector('#rpMsg') var msg = section.querySelector('#rpMsg')
var actions = section.querySelector('#rpActions') var actions = section.querySelector('#rpActions')
var bar = section.querySelector('#rpBar')
var barWrap = section.querySelector('#rpBarWrap')
var percentEl = section.querySelector('#rpPercent')
function proceed() { state.finalRpHandled = true; renderStep5() } // 다운로드 진행률(퍼센트) 구독. 완료/실패 시 해제.
var unsubscribe = null
if (installerApi.onFinalResourcepackProgress) {
unsubscribe = installerApi.onFinalResourcepackProgress(function (info) {
if (info && info.percent >= 0) {
bar.style.width = info.percent + '%'
percentEl.textContent = info.percent + '%'
} else if (info) {
// 전체 크기를 모르면 받은 용량(MB)만 표시.
percentEl.textContent = (info.loaded / 1048576).toFixed(1) + ' MB'
}
})
}
function stopProgress() { if (unsubscribe) { unsubscribe(); unsubscribe = null } }
function proceed() { stopProgress(); state.finalRpHandled = true; renderStep5() }
installerApi.installFinalResourcepack().then(function (res) { installerApi.installFinalResourcepack().then(function (res) {
stopProgress()
if (res && res.ok) { if (res && res.ok) {
bar.style.width = '100%'
percentEl.textContent = '100%'
msg.textContent = tt('resourcepack.downloadDone') msg.textContent = tt('resourcepack.downloadDone')
msg.classList.add('success') msg.classList.add('success')
actions.innerHTML = '<button class="primaryBtn" id="rpNext">' + tt('common.next') + '</button>' actions.innerHTML = '<button class="primaryBtn" id="rpNext">' + tt('common.next') + '</button>'
actions.querySelector('#rpNext').addEventListener('click', proceed) actions.querySelector('#rpNext').addEventListener('click', proceed)
} else { } else {
barWrap.hidden = true
percentEl.hidden = true
msg.textContent = tt('resourcepack.downloadFailed', { message: (res && res.message) || 'unknown' }) msg.textContent = tt('resourcepack.downloadFailed', { message: (res && res.message) || 'unknown' })
msg.classList.add('error') msg.classList.add('error')
actions.innerHTML = actions.innerHTML =
@@ -1157,6 +1184,9 @@ function downloadFinalResourcepack() {
} }
actions.hidden = false actions.hidden = false
}).catch(function (err) { }).catch(function (err) {
stopProgress()
barWrap.hidden = true
percentEl.hidden = true
msg.textContent = tt('resourcepack.downloadFailed', { message: (err && err.message) ? err.message : String(err) }) msg.textContent = tt('resourcepack.downloadFailed', { message: (err && err.message) ? err.message : String(err) })
msg.classList.add('error') msg.classList.add('error')
actions.innerHTML = actions.innerHTML =

View File

@@ -1,6 +1,6 @@
{ {
"name": "minecraft-music-quiz-installer", "name": "minecraft-music-quiz-installer",
"version": "0.4.17", "version": "0.4.18",
"description": "마인크래프트 음악퀴즈 간편설치기 + 관리 사이트", "description": "마인크래프트 음악퀴즈 간편설치기 + 관리 사이트",
"main": "dist/installer/main.js", "main": "dist/installer/main.js",
"scripts": { "scripts": {

View File

@@ -290,8 +290,20 @@ ipcMain.handle('finalResourcepack:install', async (): Promise<{ ok: boolean; pat
const url = `${state.baseUrl}/file/resourcepacks/outputs/${encodeURIComponent(cleaned)}` const url = `${state.baseUrl}/file/resourcepacks/outputs/${encodeURIComponent(cleaned)}`
const destDir = path.join(getAppDataDir(), getMcCustomDirName(), 'resourcepacks') const destDir = path.join(getAppDataDir(), getMcCustomDirName(), 'resourcepacks')
const dest = path.join(destDir, cleaned) const dest = path.join(destDir, cleaned)
await fsp.mkdir(destDir, { recursive: true })
sendLog(t('log.finalResourcepackDownload', { url })) sendLog(t('log.finalResourcepackDownload', { url }))
await downloadFile(url, dest) // 진행률(퍼센트)을 렌더러로 흘려보내며 스트리밍 다운로드.
let lastPct = -1
const controller = new AbortController()
await downloadStream(url, dest, controller.signal, (loaded, total) => {
const percent = total > 0 ? Math.min(100, Math.max(0, Math.floor((loaded / total) * 100))) : -1
if (percent !== lastPct) {
lastPct = percent
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('finalResourcepack:progress', { loaded, total, percent })
}
}
})
sendLog(t('log.finalResourcepackSaved', { path: dest })) sendLog(t('log.finalResourcepackSaved', { path: dest }))
return { ok: true, path: dest } return { ok: true, path: dest }
} catch (error) { } catch (error) {

View File

@@ -74,6 +74,15 @@ const api = {
const listener = (_event: unknown, line: string) => handler(line) const listener = (_event: unknown, line: string) => handler(line)
ipcRenderer.on('log', listener) ipcRenderer.on('log', listener)
return () => ipcRenderer.removeListener('log', listener) return () => ipcRenderer.removeListener('log', listener)
},
// 최종 리소스팩 다운로드 진행률 구독. 반환값을 호출하면 구독 해제.
onFinalResourcepackProgress: (
handler: (info: { loaded: number; total: number; percent: number }) => void
): (() => void) => {
const listener = (_event: unknown, info: { loaded: number; total: number; percent: number }) => handler(info)
ipcRenderer.on('finalResourcepack:progress', listener)
return () => ipcRenderer.removeListener('finalResourcepack:progress', listener)
} }
} }