terms: agreement pages + site Notion-style editor + rp cancel fix
- 5종 약관(map/resourcepack/mod/installer/installer-rp) markdown 시드 + manifest/terms/ 노출 - 사이트 /op/agreement 목록 + Notion 스타일 markdown 에디터 (슬래시 명령어, 미리보기) - 메인 installer: 음악퀴즈 선택 직후 약관 동의 페이지(맵·모드·설치기) 추가 - rp installer: 음악퀴즈 선택 직후 약관 동의 페이지(리소스팩·설치기) 추가 - rp installer 취소 버그 수정: buildResourcepackZip 단계간 + archive.abort() 폴링 - rp installer 취소 UX: 즉시 "취소 중…" 표시, 취소 시 installFailed 알림 생략 - 0.2.6 → 0.3.0 (큰 기능) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -124,7 +124,7 @@ function renderStep1() {
|
||||
nextBtn.addEventListener('click', function () {
|
||||
if (!state.selectedKey) return
|
||||
api.selectPack(state.selectedKey).then(function () {
|
||||
renderStep2()
|
||||
renderAgreement()
|
||||
}).catch(function (err) {
|
||||
alert(err.message || tt('common.selectFailed'))
|
||||
})
|
||||
@@ -140,6 +140,170 @@ function renderStep1() {
|
||||
})
|
||||
}
|
||||
|
||||
// 약관 동의 페이지: 1단계 직후, 2단계 설치 진입 전에 노출.
|
||||
// rp 인스톨러는 리소스팩·설치기 두 약관만 확인·동의하면 된다.
|
||||
function renderAgreement() {
|
||||
setActiveStep(1)
|
||||
clearPage()
|
||||
var KINDS = [
|
||||
{ id: 'resourcepack', tab: tt('agreement.tabResourcepack') },
|
||||
{ id: 'installer-rp', tab: tt('agreement.tabInstaller') }
|
||||
]
|
||||
var section = document.createElement('section')
|
||||
section.className = 'page'
|
||||
section.innerHTML =
|
||||
'<h2>' + escapeHtml(tt('agreement.heading')) + '</h2>' +
|
||||
'<p class="formMessage">' + escapeHtml(tt('agreement.intro')) + '</p>' +
|
||||
'<div class="tabBar" id="agTabs">' +
|
||||
KINDS.map(function (k, i) {
|
||||
return '<button type="button" class="tabBtn' + (i === 0 ? ' active' : '') + '" data-ag="' + k.id + '">' + escapeHtml(k.tab) + '</button>'
|
||||
}).join('') +
|
||||
'</div>' +
|
||||
'<div class="agreementBody" id="agBody">' + escapeHtml(tt('agreement.loading')) + '</div>' +
|
||||
'<label class="toggleRow" style="margin-top:12px;"><input type="checkbox" id="agAccept" /> ' +
|
||||
escapeHtml(tt('agreement.agreeAll')) + '</label>' +
|
||||
'<div class="formMessage" id="agMsg"></div>' +
|
||||
'<div class="actionRow">' +
|
||||
' <button class="secondaryBtn" id="back">' + escapeHtml(tt('common.back')) + '</button>' +
|
||||
' <button class="primaryBtn" id="next" disabled>' + escapeHtml(tt('common.next')) + '</button>' +
|
||||
'</div>'
|
||||
pageHost.appendChild(section)
|
||||
|
||||
var body = section.querySelector('#agBody')
|
||||
var tabs = section.querySelectorAll('[data-ag]')
|
||||
var nextBtn = section.querySelector('#next')
|
||||
var accept = section.querySelector('#agAccept')
|
||||
var msg = section.querySelector('#agMsg')
|
||||
|
||||
// 본문 캐시. 탭 전환 시 재요청하지 않음.
|
||||
var cache = {}
|
||||
|
||||
function showKind(kind) {
|
||||
if (cache[kind]) { body.innerHTML = cache[kind]; return }
|
||||
body.textContent = tt('agreement.loading')
|
||||
api.getTerm(kind).then(function (res) {
|
||||
if (!res.ok) {
|
||||
body.innerHTML = '<p class="formMessage error">' + escapeHtml(tt('agreement.loadFailed', { message: res.message || '' })) + '</p>'
|
||||
return
|
||||
}
|
||||
var html = renderTermsMarkdown(res.content || '')
|
||||
cache[kind] = html
|
||||
body.innerHTML = html
|
||||
}).catch(function (err) {
|
||||
body.innerHTML = '<p class="formMessage error">' + escapeHtml(tt('agreement.loadFailed', { message: err.message })) + '</p>'
|
||||
})
|
||||
}
|
||||
|
||||
tabs.forEach(function (b) {
|
||||
b.addEventListener('click', function () {
|
||||
tabs.forEach(function (x) { x.classList.remove('active') })
|
||||
b.classList.add('active')
|
||||
showKind(b.getAttribute('data-ag'))
|
||||
})
|
||||
})
|
||||
|
||||
accept.addEventListener('change', function () {
|
||||
nextBtn.disabled = !accept.checked
|
||||
if (accept.checked) msg.textContent = ''
|
||||
})
|
||||
|
||||
nextBtn.addEventListener('click', function () {
|
||||
if (!accept.checked) {
|
||||
msg.textContent = tt('agreement.agreeRequired')
|
||||
msg.classList.add('error')
|
||||
return
|
||||
}
|
||||
renderStep2()
|
||||
})
|
||||
section.querySelector('#back').addEventListener('click', renderStep1)
|
||||
|
||||
showKind(KINDS[0].id)
|
||||
}
|
||||
|
||||
// 인스톨러용 미니 markdown 렌더러. 사이트 termsEditor 와 같은 규칙을 처리한다.
|
||||
function renderTermsMarkdown(src) {
|
||||
function escHtml(s) {
|
||||
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
}
|
||||
function inline(s) {
|
||||
s = escHtml(s)
|
||||
s = s.replace(/`([^`]+)`/g, '<code>$1</code>')
|
||||
s = s.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
||||
s = s.replace(/(^|\W)\*([^*\n]+)\*(?=\W|$)/g, '$1<em>$2</em>')
|
||||
s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>')
|
||||
s = s.replace(/(^|[\s(])(https?:\/\/[^\s)]+)/g, function (m, p, u) {
|
||||
return p + '<a href="' + u + '" target="_blank" rel="noopener">' + u + '</a>'
|
||||
})
|
||||
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 + '>'); 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('<pre><code>' + escHtml(code.join('\n')) + '</code></pre>')
|
||||
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('<details><summary>' + inline(summary) + '</summary>' + renderTermsMarkdown(body2.join('\n')) + '</details>')
|
||||
continue
|
||||
}
|
||||
var h = /^(#{1,6})\s+(.*)$/.exec(line)
|
||||
if (h) {
|
||||
closeList()
|
||||
out.push('<h' + h[1].length + '>' + inline(h[2]) + '</h' + h[1].length + '>')
|
||||
i += 1; continue
|
||||
}
|
||||
if (/^---+\s*$/.test(line)) { closeList(); out.push('<hr/>'); 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('<blockquote>' + renderTermsMarkdown(q.join('\n')) + '</blockquote>')
|
||||
continue
|
||||
}
|
||||
var ol = /^\s*\d+\.\s+(.*)$/.exec(line)
|
||||
if (ol) {
|
||||
if (stack !== 'ol') { closeList(); out.push('<ol>'); stack = 'ol' }
|
||||
out.push('<li>' + inline(ol[1]) + '</li>'); i += 1; continue
|
||||
}
|
||||
var ul = /^\s*[-*]\s+(.*)$/.exec(line)
|
||||
if (ul) {
|
||||
if (stack !== 'ul') { closeList(); out.push('<ul>'); stack = 'ul' }
|
||||
out.push('<li>' + inline(ul[1]) + '</li>'); i += 1; continue
|
||||
}
|
||||
if (/^\s*$/.test(line)) { closeList(); i += 1; continue }
|
||||
closeList()
|
||||
var para = [line]; i += 1
|
||||
while (i < lines.length && !/^\s*$/.test(lines[i])
|
||||
&& !/^(#{1,6})\s+/.test(lines[i])
|
||||
&& !/^\s*[-*]\s+/.test(lines[i])
|
||||
&& !/^\s*\d+\.\s+/.test(lines[i])
|
||||
&& !/^>/.test(lines[i])
|
||||
&& !/^---+\s*$/.test(lines[i])
|
||||
&& !/^```/.test(lines[i])
|
||||
&& !/^:::/.test(lines[i])) {
|
||||
para.push(lines[i]); i += 1
|
||||
}
|
||||
out.push('<p>' + inline(para.join('\n').replace(/\n/g, '<br/>')) + '</p>')
|
||||
}
|
||||
closeList()
|
||||
return out.join('\n')
|
||||
}
|
||||
|
||||
// ── 2단계: 설치 진행 ────────────────────────────────
|
||||
function renderStep2() {
|
||||
setActiveStep(2)
|
||||
@@ -255,9 +419,16 @@ function renderStep2() {
|
||||
}
|
||||
})
|
||||
|
||||
// 사용자가 취소를 눌렀는지 추적. 취소 흐름에서는 installFailed 알림을 띄우지 않고
|
||||
// 조용히 step1 로 돌아간다.
|
||||
var cancelInitiated = false
|
||||
cancelBtn.addEventListener('click', function () {
|
||||
if (!state.installing) return
|
||||
if (!state.installing || cancelInitiated) return
|
||||
cancelInitiated = true
|
||||
cancelBtn.disabled = true
|
||||
cancelBtn.textContent = tt('agreement.cancelling')
|
||||
// 사용자에게 어느 단계든 즉시 "취소 중" 신호가 보이도록 패키지 섹션 상태 갱신.
|
||||
pkgSub.textContent = tt('agreement.cancelling')
|
||||
api.cancelInstall()
|
||||
})
|
||||
|
||||
@@ -273,7 +444,9 @@ function renderStep2() {
|
||||
}).catch(function (err) {
|
||||
state.installing = false
|
||||
if (stopProgress) stopProgress()
|
||||
alert(tt('common.installFailed', { message: (err && err.message) || err }))
|
||||
if (!cancelInitiated) {
|
||||
alert(tt('common.installFailed', { message: (err && err.message) || err }))
|
||||
}
|
||||
renderStep1()
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user