add standalone port-forwarding tool (마인크래프트 간편 포트포워딩)
포트포워딩만 단독으로 수행하는 세 번째 Electron 앱 추가.
- 포트 입력(기본 25565) → UPnP 개방 시도 → 외부에서 닿는지 점검(ifconfig.co
+ 임시 리스너) → 외부 접속 주소/성공·실패·확인불가 안내. '포트 닫기'로 매핑 제거.
- 점검 로직은 메인 설치기의 v0.3.14 오탐 수정(ifconfig 실패→확인불가)을 반영해
자체 포함. sharp 미사용이라 빌드에서 제외(exe 약 73MB).
- 신규: src/installer-pf/{main,preload}.ts, installer-pf/{index.html,renderer.js},
locales/installer-pf/ko-kr.json, tsconfig.installer-pf.json, electron-builder-pf.yml.
- package.json 에 installer:pf / dist:win:pf 스크립트, i18n 컴포넌트 유니온에
'installer-pf' 추가. v0.3.15.
Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
This commit is contained in:
154
installer-pf/renderer.js
Normal file
154
installer-pf/renderer.js
Normal file
@@ -0,0 +1,154 @@
|
||||
'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 =
|
||||
'<section class="page">' +
|
||||
' <h2>' + escapeHtml(tt('main.heading')) + '</h2>' +
|
||||
' <p class="formMessage">' + escapeHtml(tt('main.intro')) + '</p>' +
|
||||
' <div class="fieldset">' +
|
||||
' <label for="port">' + escapeHtml(tt('main.portLabel')) + '</label>' +
|
||||
' <input type="text" id="port" inputmode="numeric" value="25565" />' +
|
||||
' </div>' +
|
||||
' <div class="actionRow">' +
|
||||
' <button class="primaryBtn" id="openBtn">' + escapeHtml(tt('main.openBtn')) + '</button>' +
|
||||
' <button class="secondaryBtn" id="closeBtn">' + escapeHtml(tt('main.closeBtn')) + '</button>' +
|
||||
' <button class="secondaryBtn" id="quitBtn">' + escapeHtml(tt('main.quitBtn')) + '</button>' +
|
||||
' </div>' +
|
||||
' <div id="result"></div>' +
|
||||
'</section>'
|
||||
|
||||
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')
|
||||
}
|
||||
resultEl.innerHTML =
|
||||
'<div class="progressCard ' + (cls === 'ok' ? 'done' : cls === 'fail' ? 'error' : 'running') + '" style="margin-top:14px;">' +
|
||||
' <div class="cardTop"><span class="statusBadge ' + cls + '">' + escapeHtml(badge) + '</span> ' +
|
||||
' <span class="label">' + escapeHtml(addr) + '</span></div>' +
|
||||
' <p class="formMessage" style="margin-top:8px;">' + escapeHtml(msg) + '</p>' +
|
||||
' <p class="formMessage"><small>' + escapeHtml(tt('main.detailLabel', { detail: outcome.detail || '' })) + '</small></p>' +
|
||||
'</div>'
|
||||
}
|
||||
|
||||
openBtn.addEventListener('click', function () {
|
||||
var port = parsePort()
|
||||
portEl.value = String(port)
|
||||
setBusy(true)
|
||||
resultEl.innerHTML = '<p class="formMessage" style="margin-top:14px;">' + escapeHtml(tt('main.working')) + '</p>'
|
||||
api.openPort(port).then(function (outcome) {
|
||||
showResult(outcome)
|
||||
}).catch(function (err) {
|
||||
resultEl.innerHTML = '<p class="formMessage error" style="margin-top:14px;">' +
|
||||
escapeHtml(tt('main.error', { message: (err && err.message) || String(err) })) + '</p>'
|
||||
}).then(function () {
|
||||
setBusy(false)
|
||||
})
|
||||
})
|
||||
|
||||
closeBtn.addEventListener('click', function () {
|
||||
var port = parsePort()
|
||||
portEl.value = String(port)
|
||||
setBusy(true)
|
||||
api.closePort(port).then(function () {
|
||||
resultEl.innerHTML = '<p class="formMessage" style="margin-top:14px;">' +
|
||||
escapeHtml(tt('main.closed', { port: port })) + '</p>'
|
||||
}).then(function () {
|
||||
setBusy(false)
|
||||
})
|
||||
})
|
||||
|
||||
quitBtn.addEventListener('click', function () { api.quit() })
|
||||
}
|
||||
|
||||
;(async function () {
|
||||
try { I18N = (await api.loadLocale()) || {} } catch (_) { I18N = {} }
|
||||
applyStaticI18n()
|
||||
renderMain()
|
||||
})()
|
||||
Reference in New Issue
Block a user