'use strict' const api = window.pfTool let I18N = {} function tt(key, params) { var parts = String(key).split('.') var cur = I18N for (var i = 0; i < parts.length; i++) { if (cur && typeof cur === 'object' && parts[i] in cur) { cur = cur[parts[i]] } else { return key } } if (typeof cur !== 'string') return key if (!params) return cur return cur.replace(/\{\{\s*(\w+)\s*\}\}/g, function (_m, name) { return name in params ? String(params[name]) : '{{' + name + '}}' }) } function escapeHtml(s) { return String(s).replace(/[&<>"']/g, function (c) { return c === '&' ? '&' : c === '<' ? '<' : c === '>' ? '>' : c === '"' ? '"' : ''' }) } const pageHost = document.getElementById('pageHost') 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 = tt('logViewer.expand') } else { logViewer.style.height = '' logToggle.textContent = tt('logViewer.collapse') } }) api.onLog(function (line) { logViewer.hidden = false logBody.textContent += line + '\n' logBody.scrollTop = logBody.scrollHeight }) function applyStaticI18n() { document.title = tt('app.title') var h1 = document.querySelector('.appHeader h1') if (h1) h1.textContent = tt('app.title') var logH2 = logViewer.querySelector('header h2') if (logH2) logH2.textContent = tt('logViewer.heading') logToggle.textContent = tt('logViewer.collapse') } function renderMain() { pageHost.innerHTML = '
' + '

' + escapeHtml(tt('main.heading')) + '

' + '

' + escapeHtml(tt('main.intro')) + '

' + '
' + ' ' + ' ' + '
' + '
' + ' ' + ' ' + ' ' + '
' + '
' + '
' var portEl = document.getElementById('port') var openBtn = document.getElementById('openBtn') var closeBtn = document.getElementById('closeBtn') var quitBtn = document.getElementById('quitBtn') var resultEl = document.getElementById('result') function parsePort() { var n = parseInt(String(portEl.value).replace(/[^0-9]/g, ''), 10) if (!isFinite(n) || n <= 0 || n >= 65536) return 25565 return n } function setBusy(busy) { openBtn.disabled = busy closeBtn.disabled = busy portEl.disabled = busy } function showResult(outcome) { var addr = (outcome.externalIp || tt('main.ipUnknown')) + (outcome.port === 25565 ? '' : (':' + outcome.port)) var cls, badge, msg if (outcome.reachable === true) { cls = 'ok'; badge = tt('verdict.success') msg = tt('main.successMsg', { addr: addr }) + (outcome.preForwarded ? ' ' + tt('main.alreadyOpen') : '') } else if (outcome.reachable === false) { cls = 'fail'; badge = tt('verdict.fail') msg = tt('main.failMsg') } else { cls = 'warn'; badge = tt('verdict.unknown') msg = tt('main.unknownMsg') } // 실패/확인불가일 때 다음 조치 안내: CGNAT 경고 + 수동 포워딩 방법. var extra = '' if (outcome.reachable !== true) { if (outcome.cgnat) { extra += '

' + escapeHtml(tt('main.cgnatWarn', { ip: outcome.externalIp || '?' })) + '

' } var manual = outcome.localIp ? tt('main.manualForward', { port: outcome.port, localIp: outcome.localIp }) : tt('main.manualForwardNoIp', { port: outcome.port }) extra += '

' + escapeHtml(manual) + '

' } resultEl.innerHTML = '
' + '
' + escapeHtml(badge) + ' ' + ' ' + escapeHtml(addr) + '
' + '

' + escapeHtml(msg) + '

' + extra + '

' + escapeHtml(tt('main.detailLabel', { detail: outcome.detail || '' })) + '

' + '
' } openBtn.addEventListener('click', function () { var port = parsePort() portEl.value = String(port) setBusy(true) resultEl.innerHTML = '

' + escapeHtml(tt('main.working')) + '

' api.openPort(port).then(function (outcome) { showResult(outcome) }).catch(function (err) { resultEl.innerHTML = '

' + escapeHtml(tt('main.error', { message: (err && err.message) || String(err) })) + '

' }).then(function () { setBusy(false) }) }) closeBtn.addEventListener('click', function () { var port = parsePort() portEl.value = String(port) setBusy(true) api.closePort(port).then(function () { resultEl.innerHTML = '

' + escapeHtml(tt('main.closed', { port: port })) + '

' }).then(function () { setBusy(false) }) }) quitBtn.addEventListener('click', function () { api.quit() }) } ;(async function () { try { I18N = (await api.loadLocale()) || {} } catch (_) { I18N = {} } applyStaticI18n() renderMain() })()