Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e80933d21e | |||
| df5947f8bb | |||
| 3c404ecd8c | |||
| e30d7418df |
@@ -1133,20 +1133,47 @@ function downloadFinalResourcepack() {
|
||||
section.innerHTML =
|
||||
'<h2>' + tt('resourcepack.downloadHeading') + '</h2>' +
|
||||
'<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>'
|
||||
pageHost.appendChild(section)
|
||||
var msg = section.querySelector('#rpMsg')
|
||||
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) {
|
||||
stopProgress()
|
||||
if (res && res.ok) {
|
||||
bar.style.width = '100%'
|
||||
percentEl.textContent = '100%'
|
||||
msg.textContent = tt('resourcepack.downloadDone')
|
||||
msg.classList.add('success')
|
||||
actions.innerHTML = '<button class="primaryBtn" id="rpNext">' + tt('common.next') + '</button>'
|
||||
actions.querySelector('#rpNext').addEventListener('click', proceed)
|
||||
} else {
|
||||
barWrap.hidden = true
|
||||
percentEl.hidden = true
|
||||
msg.textContent = tt('resourcepack.downloadFailed', { message: (res && res.message) || 'unknown' })
|
||||
msg.classList.add('error')
|
||||
actions.innerHTML =
|
||||
@@ -1157,6 +1184,9 @@ function downloadFinalResourcepack() {
|
||||
}
|
||||
actions.hidden = false
|
||||
}).catch(function (err) {
|
||||
stopProgress()
|
||||
barWrap.hidden = true
|
||||
percentEl.hidden = true
|
||||
msg.textContent = tt('resourcepack.downloadFailed', { message: (err && err.message) ? err.message : String(err) })
|
||||
msg.classList.add('error')
|
||||
actions.innerHTML =
|
||||
|
||||
@@ -241,6 +241,7 @@
|
||||
"modDownload": "모드 다운로드: {{file}}",
|
||||
"skipResourcepack": "resourcepackPath가 비어 있어 리소스팩 다운로드를 건너뜁니다.",
|
||||
"resourcepackDownload": "리소스팩 다운로드: {{url}}",
|
||||
"skipBaseForFinal": "최종 리소스팩이 등록돼 있어 베이스 리소스팩은 건너뜁니다(설치 마지막에 최종 리소스팩만 받음).",
|
||||
"finalResourcepackDownload": "최종 리소스팩 다운로드: {{url}}",
|
||||
"finalResourcepackSaved": "최종 리소스팩 저장: {{path}}",
|
||||
"serverInstallPath": "서버 설치 경로: {{path}}",
|
||||
@@ -289,7 +290,8 @@
|
||||
"settingCopyFail": "설정 복사 실패 ({{name}}): {{message}}",
|
||||
"settingCopySummary": "기존 마인크래프트 설정 복사: 새로 복사 {{copied}}개 / 동기화(options 류 덮어쓰기) {{synced}}개 / 보존(이미 존재) {{skipped}}개.",
|
||||
"settingCopyError": "기존 설정 복사 중 오류: {{message}}",
|
||||
"runtimeDirMissing": ".minecraft/{{dir}} 가 없습니다. 마인크래프트 런처를 한 번 실행한 뒤 다시 시도해주세요.",
|
||||
"runtimeDirMissing": ".minecraft/{{dir}} 가 없어 공용 링크를 만들 수 없습니다(런처가 이 폴더에 직접 받게 됩니다).",
|
||||
"runtimeDirCreated": ".mc_custom/{{dir}} 폴더를 새로 만들었습니다(런처가 여기에 직접 다운로드).",
|
||||
"runtimeDirExists": ".mc_custom/{{dir}} 가 실제 폴더로 이미 존재 — 건너뜀.",
|
||||
"runtimeLinkCreated": "링크 생성: .mc_custom/{{dir}} → .minecraft/{{dir}}",
|
||||
"runtimeLinkFail": "링크 생성 실패 ({{dir}}): {{message}}",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "minecraft-music-quiz-installer",
|
||||
"version": "0.4.17",
|
||||
"version": "0.4.21",
|
||||
"description": "마인크래프트 음악퀴즈 간편설치기 + 관리 사이트",
|
||||
"main": "dist/installer/main.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -285,16 +285,31 @@ ipcMain.handle('finalResourcepack:install', async (): Promise<{ ok: boolean; pat
|
||||
if (!pack) return { ok: false, message: t('errors.packNotFound2') }
|
||||
const finalName = pack.pack.finalResourcepackPath
|
||||
if (!finalName || finalName === '.') return { ok: false, message: 'no final resourcepack' }
|
||||
let dest = ''
|
||||
try {
|
||||
const cleaned = path.basename(finalName.replace(/^\/+/, ''))
|
||||
const url = `${state.baseUrl}/file/resourcepacks/outputs/${encodeURIComponent(cleaned)}`
|
||||
const destDir = path.join(getAppDataDir(), getMcCustomDirName(), 'resourcepacks')
|
||||
const dest = path.join(destDir, cleaned)
|
||||
dest = path.join(destDir, cleaned)
|
||||
await fsp.mkdir(destDir, { recursive: true })
|
||||
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 }))
|
||||
return { ok: true, path: dest }
|
||||
} catch (error) {
|
||||
// 실패 시 부분/0바이트 zip 이 마인크래프트 리소스팩 목록에 깨진 채로 남지 않도록 삭제.
|
||||
if (dest) await fsp.rm(dest, { force: true }).catch(() => {})
|
||||
return { ok: false, message: (error as Error).message }
|
||||
}
|
||||
})
|
||||
@@ -1340,7 +1355,13 @@ ipcMain.handle('client:install', async (_event, payload: ClientInstallPayload) =
|
||||
}
|
||||
|
||||
await downloadModsFolder(pack.pack, customRoot)
|
||||
await downloadResourcepackZip(pack.pack, customRoot)
|
||||
// 최종 리소스팩이 등록돼 있으면(finalResourcepackPath !== "."), 완성본을 설치 마지막에
|
||||
// 따로 받으므로 베이스 리소스팩은 받지 않는다("최종 리소스팩만 다운로드").
|
||||
if (pack.pack.finalResourcepackPath && pack.pack.finalResourcepackPath !== '.') {
|
||||
sendLog(t('log.skipBaseForFinal'))
|
||||
} else {
|
||||
await downloadResourcepackZip(pack.pack, customRoot)
|
||||
}
|
||||
|
||||
if (payload.skipMap) {
|
||||
// 참가자 모드: 이전 설치 흐름에서 설치러가 풀어둔 맵이 있다면 제거한다.
|
||||
@@ -1734,7 +1755,19 @@ async function linkMinecraftRuntimeDirs(customRoot: string): Promise<void> {
|
||||
const src = path.join(mcRoot, dir)
|
||||
const dst = path.join(customRoot, dir)
|
||||
if (!fs.existsSync(src)) {
|
||||
// .minecraft 에 원본이 없으면(바닐라를 한 번도 안 받았거나 런처가 다른 위치를 쓰는
|
||||
// 환경) 링크를 만들 수 없다. 이때 그냥 건너뛰면 .mc_custom/<dir> 이 아예 없어
|
||||
// 런처가 "Unable to prepare assets for download" 로 실패한다. 빈 폴더라도 만들어
|
||||
// 런처가 그 gameDir 에 직접 받아갈 수 있게 한다(없을 때만).
|
||||
sendLog(t('log.runtimeDirMissing', { dir }))
|
||||
try {
|
||||
if (!fs.existsSync(dst)) {
|
||||
await fsp.mkdir(dst, { recursive: true })
|
||||
sendLog(t('log.runtimeDirCreated', { dir }))
|
||||
}
|
||||
} catch (err) {
|
||||
sendLog(t('log.runtimeLinkFail', { dir, message: (err as Error).message }))
|
||||
}
|
||||
continue
|
||||
}
|
||||
let existing: import('node:fs').Stats | null = null
|
||||
|
||||
@@ -74,6 +74,15 @@ const api = {
|
||||
const listener = (_event: unknown, line: string) => handler(line)
|
||||
ipcRenderer.on('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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user