const PageSettings = { _el: null, init() { this._el = document.getElementById('page-settings'); }, async render() { const cfg = await API.getConfig(); const games = await API.getGames(); const forwards = await this._getForwards(); this._el.innerHTML = this._template(cfg, games, forwards); this._bindEvents(); this._toggleGamePorts(cfg.use_unified_port !== false); }, _toggleGamePorts(hide) { const el = document.getElementById('game-ports-section'); if (el) el.style.display = hide ? 'none' : 'block'; }, async _getForwards() { try { const res = await fetch('/api/forward'); return await res.json(); } catch (e) { return []; } }, async _saveForwards(targets) { await fetch('/api/forward', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ targets }) }); }, _bindEvents() { document.getElementById('settings-telemetry-port')?.addEventListener('change', async (e) => { await API.updateConfig({ telemetry_port: parseInt(e.target.value) || 20777 }); Toast.show(Lang.t('portUpdated'), 'info'); }); document.getElementById('settings-unified-port')?.addEventListener('change', async (e) => { await API.updateConfig({ use_unified_port: e.target.checked }); this._toggleGamePorts(e.target.checked); Toast.show(Lang.t('configSaved'), 'info'); }); document.getElementById('settings-restart-telemetry')?.addEventListener('click', async () => { await API.stopTelemetry(); await API.startTelemetry(); Toast.show(Lang.t('telemetryRestarted'), 'success'); }); document.getElementById('btn-add-forward')?.addEventListener('click', () => this._addForward()); document.getElementById('forward-list')?.addEventListener('click', async (e) => { if (e.target.closest('.btn-del-forward')) { e.target.closest('.forward-row').remove(); this._collectAndSave(); } }); document.getElementById('btn-save-forwards')?.addEventListener('click', () => this._collectAndSave()); document.getElementById('btn-import-plugin')?.addEventListener('click', () => { const input = document.createElement('input'); input.type = 'file'; input.accept = '.tsp'; input.onchange = async (e) => { const file = e.target.files[0]; if (!file) return; const fd = new FormData(); fd.append('file', file); const res = await fetch('/api/games/install', { method: 'POST', body: fd }); const data = await res.json(); if (data.ok) { Toast.show(Lang.t('pluginInstalled'), 'success'); this.render(); } else Toast.show(Lang.t('importFailed'), 'error'); }; input.click(); }); document.getElementById('btn-reload-plugins')?.addEventListener('click', async () => { await API.reloadGamePlugins(); Toast.show(Lang.t('pluginReloaded'), 'success'); this.render(); }); document.getElementById('settings-game-list')?.addEventListener('click', async (e) => { const exportBtn = e.target.closest('.btn-export-plugin'); const removeBtn = e.target.closest('.btn-remove-plugin'); if (exportBtn) { const pid = exportBtn.dataset.pluginId; const a = document.createElement('a'); a.href = '/api/games/' + pid + '/export'; a.download = pid + '.tsp'; a.click(); } if (removeBtn) { if (confirm(Lang.t('confirmRemove'))) { await API.removeGamePlugin(removeBtn.dataset.pluginId); Toast.show(Lang.t('removeSuccess'), 'info'); this.render(); } } }); document.getElementById('btn-import-dashboard')?.addEventListener('click', () => { const input = document.createElement('input'); input.type = 'file'; input.accept = '.tsd'; input.onchange = async (e) => { const file = e.target.files[0]; if (!file) return; const fd = new FormData(); fd.append('file', file); const res = await fetch('/api/dashboards/import', { method: 'POST', body: fd }); const data = await res.json(); Toast.show(data.ok ? Lang.t('importSuccess') : Lang.t('importFailed'), data.ok ? 'success' : 'error'); }; input.click(); }); document.getElementById('btn-export-backup')?.addEventListener('click', () => { const a = document.createElement('a'); a.href = '/api/backup/export'; a.download = 'turbosu_backup.tsb'; a.click(); }); document.getElementById('btn-import-backup')?.addEventListener('click', () => { const input = document.createElement('input'); input.type = 'file'; input.accept = '.tsb'; input.onchange = async (e) => { const file = e.target.files[0]; if (!file) return; const fd = new FormData(); fd.append('file', file); const res = await fetch('/api/backup/import', { method: 'POST', body: fd }); const data = await res.json(); Toast.show(data.ok ? data.message || Lang.t('restoreSuccess') : Lang.t('restoreFailed') + data.message, data.ok ? 'success' : 'error'); }; input.click(); }); document.getElementById('btn-restart-server')?.addEventListener('click', async () => { if (!confirm(Lang.t('confirmRestart'))) return; try { await fetch('/api/restart', { method: 'POST' }); Toast.show(Lang.t('restarting'), 'info'); setTimeout(() => location.reload(), 3000); } catch (e) { Toast.show(Lang.t('restartRequestSent'), 'info'); setTimeout(() => location.reload(), 3000); } }); document.querySelectorAll('.settings-nav-item').forEach(el => { el.addEventListener('click', (e) => { e.preventDefault(); const id = el.getAttribute('data-target'); const section = document.getElementById(id); if (section) section.scrollIntoView({ behavior: 'smooth', block: 'start' }); document.querySelectorAll('.settings-nav-item').forEach(s => s.classList.remove('active')); el.classList.add('active'); }); }); document.getElementById('btn-rec-start')?.addEventListener('click', async () => { await fetch('/api/record/start', { method: 'POST' }); document.getElementById('btn-rec-start').style.display = 'none'; document.getElementById('btn-rec-stop').style.display = ''; document.getElementById('rec-status').textContent = Lang.t('recording'); }); document.getElementById('btn-rec-stop')?.addEventListener('click', async () => { await fetch('/api/record/stop', { method: 'POST' }); document.getElementById('btn-rec-start').style.display = ''; document.getElementById('btn-rec-stop').style.display = 'none'; document.getElementById('rec-status').textContent = Lang.t('stopped'); this._loadRecordings(); }); document.getElementById('rec-auto-mode')?.addEventListener('change', async (e) => { await fetch('/api/record/auto', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: e.target.checked }) }); }); this._loadRecordings(); document.getElementById('settings-test-mode')?.addEventListener('change', async (e) => { const on = e.target.checked; await API.updateConfig({ ui_test_mode: on }); if (on) { await fetch('/api/test-mode/start', { method: 'POST' }); Toast.show(Lang.t('testModeOn'), 'success'); } else { await fetch('/api/test-mode/stop', { method: 'POST' }); Toast.show(Lang.t('testModeOff'), 'info'); } }); document.getElementById('btn-save-game-ports')?.addEventListener('click', async () => { const rows = document.querySelectorAll('#game-ports-section .game-port-row'); const gamePorts = {}; rows.forEach(r => { const id = r.querySelector('.gp-id')?.textContent; const port = parseInt(r.querySelector('.gp-port')?.value) || 0; if (id && port > 0) gamePorts[id] = port; }); await API.updateConfig({ game_ports: gamePorts }); Toast.show(Lang.t('portsSaved'), 'success'); }); }, _addForward() { const list = document.getElementById('forward-list'); const row = document.createElement('div'); row.className = 'forward-row glass-card'; row.style.cssText = 'padding:12px;margin-bottom:8px;display:flex;gap:8px;align-items:center;flex-wrap:wrap'; row.innerHTML = ''; list.appendChild(row); }, _collectAndSave() { const rows = document.querySelectorAll('#forward-list .forward-row'); const targets = []; rows.forEach(r => { const host = r.querySelector('.fw-host')?.value?.trim(); const port = parseInt(r.querySelector('.fw-port')?.value) || 0; const name = r.querySelector('.fw-name')?.value?.trim() || ''; const enabled = r.querySelector('.forward-toggle')?.checked !== false; if (host && port > 0) targets.push({ host, port, name, enabled }); }); this._saveForwards(targets).then(() => Toast.show(Lang.t('forwardsSaved').replace('%d', targets.length), 'success')); }, async _loadRecordings() { try { const res = await fetch('/api/recordings'); const list = await res.json(); const el = document.getElementById('recording-list'); if (!el) return; if (!list.length) { el.innerHTML = '

' + Lang.t('noRecordings') + '

'; return; } el.innerHTML = list.map(f => { const size = f.size > 1048576 ? (f.size/1048576).toFixed(1)+'MB' : (f.size/1024).toFixed(1)+'KB'; return '
' + f.name + ' ' + size + '
'; }).join(''); el.querySelectorAll('.btn-play-rec').forEach(btn => { btn.addEventListener('click', async () => { await fetch('/api/playback/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ filename: btn.dataset.file }) }); Toast.show(Lang.t('playbackStarted'), 'success'); }); }); } catch (e) { console.error(e); } }, _gamePortListHtml(games, cfg) { const gamePorts = cfg.game_ports || {}; return games.map(g => { const proto = g.protocol || 'udp'; const mode = g.mode || (proto === 'tcp' ? 'server' : ''); const protoLabel = proto.toUpperCase() + (mode ? '/' + mode : ''); return '
' + g.name + '' + protoLabel + '' + Lang.t('defaultPortLabel') + (g.default_port || (proto==='tcp'?cfg.tcp_port||20778:20777)) + '
'; }).join(''); }, _forwardListHtml(forwards) { if (!forwards || forwards.length === 0) return '

' + Lang.t('noForwardTargets') + '

'; return forwards.map(f => '
').join(''); }, _gamePluginListHtml(games) { if (!games || games.length === 0) return '

' + Lang.t('noPlugins') + '

'; return games.map(g => '
' + g.name + '
' + (g.description||'') + ' | v' + g.version + ' | ' + (g.author||'') + '' + (g.is_builtin ? Lang.t('builtin') : Lang.t('community')) + '
' + (g.is_builtin?'':'') + '
').join(''); }, _template(cfg, games, forwards) { const sections = [ { id: 'sec-forward', label: Lang.t('dataForwarding') }, { id: 'sec-connection', label: Lang.t('connectionSettings') }, { id: 'sec-dashboards', label: Lang.t('dashManagement') }, { id: 'sec-plugins', label: Lang.t('pluginManagement') }, { id: 'sec-backup', label: Lang.t('dataBackup') }, { id: 'sec-dev', label: Lang.t('devTools') }, { id: 'sec-system', label: Lang.t('systemSection') }, ]; return '
' + sections.map(s => '' + s.label + '').join('') + '

' + Lang.t('settings') + '

' + '

' + Lang.t('dataForwarding') + '

' + Lang.t('dataForwardingDesc') + '

' + this._forwardListHtml(forwards) + '
' + '

' + Lang.t('connectionSettings') + '

' + Lang.t('telemetryPort') + '
' + Lang.t('telemetryPortDesc') + '
' + Lang.t('unifiedPortToggle') + '
' + Lang.t('unifiedPortDesc') + '

' + Lang.t('perGamePort') + '

' + this._gamePortListHtml(games, cfg) + '
' + Lang.t('restartListener') + '
' + Lang.t('restartListenerDesc') + '
' + '

' + Lang.t('dashManagement') + '

' + Lang.t('rightClickExport') + '
' + '

' + Lang.t('pluginManagement') + '

' + this._gamePluginListHtml(games) + '
' + '

' + Lang.t('dataBackup') + '

' + Lang.t('exportBackup') + '
' + Lang.t('backupDesc') + '
' + Lang.t('restoreBackup') + '
' + Lang.t('restoreBackupDesc') + '
' + '

' + Lang.t('devTools') + '

' + Lang.t('testMode') + '
' + Lang.t('testModeDesc') + '
' + '

' + Lang.t('systemSection') + '

' + Lang.t('restartServer') + '
' + Lang.t('restartServerDesc') + '
' + '

' + Lang.t('about') + '

TurboSu
' + Lang.t('aboutDesc') + '
https://git.yeij.top/AskaEth/TurboSu
' + '
'; } }; window.PageSettings = PageSettings;