279 lines
20 KiB
JavaScript
279 lines
20 KiB
JavaScript
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 = '<input class="fw-host" placeholder="IP/域名" style="flex:2;min-width:120px"><input class="fw-port" type="number" placeholder="端口" style="flex:1;min-width:70px" min="1" max="65535"><input class="fw-name" placeholder="名称" style="flex:1;min-width:80px"><label class="switch-label"><input type="checkbox" class="forward-toggle" checked><span class="switch-track"></span> ' + Lang.t('forwardEnabled') + '</label><button class="btn btn-sm btn-danger btn-del-forward">x</button>';
|
|
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 = '<p style="color:var(--text-tertiary);font-size:12px">' + Lang.t('noRecordings') + '</p>'; return; }
|
|
el.innerHTML = list.map(f => {
|
|
const size = f.size > 1048576 ? (f.size/1048576).toFixed(1)+'MB' : (f.size/1024).toFixed(1)+'KB';
|
|
return '<div class="glass-card" style="padding:8px 12px;margin-bottom:4px;display:flex;align-items:center;justify-content:space-between"><span style="font-size:12px">' + f.name + ' <span style="color:var(--text-tertiary)">' + size + '</span></span><button class="btn btn-sm btn-primary btn-play-rec" data-file="' + f.name + '">' + Lang.t('play') + '</button></div>';
|
|
}).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 '<div class="game-port-row" style="display:flex;gap:8px;align-items:center;margin-bottom:6px"><span class="gp-id" style="width:120px;font-size:13px;color:var(--text-secondary)">' + g.name + '</span><span style="width:70px;font-size:11px;color:var(--text-tertiary)">' + protoLabel + '</span><input class="gp-port" type="number" value="' + (gamePorts[g.id] || g.default_port || (proto==='tcp'?cfg.tcp_port||20778:20777)) + '" min="1024" max="65535" style="width:100px"><span style="font-size:11px;color:var(--text-tertiary)">' + Lang.t('defaultPortLabel') + (g.default_port || (proto==='tcp'?cfg.tcp_port||20778:20777)) + '</span></div>';
|
|
}).join('');
|
|
},
|
|
|
|
_forwardListHtml(forwards) {
|
|
if (!forwards || forwards.length === 0) return '<p style="color:var(--text-tertiary);font-size:13px;padding:8px 0">' + Lang.t('noForwardTargets') + '</p>';
|
|
return forwards.map(f => '<div class="forward-row glass-card" style="padding:12px;margin-bottom:8px;display:flex;gap:8px;align-items:center;flex-wrap:wrap"><input class="fw-host" placeholder="IP/域名" style="flex:2;min-width:120px" value="' + (f.host||'') + '"><input class="fw-port" type="number" placeholder="端口" style="flex:1;min-width:70px" value="' + (f.port||'') + '" min="1" max="65535"><input class="fw-name" placeholder="名称" style="flex:1;min-width:80px" value="' + (f.name||'') + '"><label class="switch-label"><input type="checkbox" class="forward-toggle" ' + (f.enabled!==false?'checked':'') + '><span class="switch-track"></span> ' + Lang.t('forwardEnabled') + '</label><button class="btn btn-sm btn-danger btn-del-forward">x</button></div>').join('');
|
|
},
|
|
|
|
_gamePluginListHtml(games) {
|
|
if (!games || games.length === 0) return '<p style="color:var(--text-tertiary);font-size:13px;">' + Lang.t('noPlugins') + '</p>';
|
|
return games.map(g => '<div class="glass-card" style="padding:16px;margin-bottom:10px;display:flex;align-items:center;justify-content:space-between"><div><div style="font-weight:600">' + g.name + '</div><div style="font-size:12px;color:var(--text-tertiary)">' + (g.description||'') + ' | v' + g.version + ' | ' + (g.author||'') + '<span class="badge ' + (g.is_builtin?'badge-info':'badge-success') + '" style="margin-left:6px">' + (g.is_builtin ? Lang.t('builtin') : Lang.t('community')) + '</span></div></div><div style="display:flex;gap:6px"><button class="btn btn-sm btn-secondary btn-export-plugin" data-plugin-id="' + g.id + '">' + Lang.t('export') + ' .tsp</button>' + (g.is_builtin?'':'<button class="btn btn-sm btn-danger btn-remove-plugin" data-plugin-id="' + g.id + '">' + Lang.t('remove') + '</button>') + '</div></div>').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 '<div class="settings-layout"><div class="settings-nav">' +
|
|
sections.map(s => '<a class="settings-nav-item" data-target="' + s.id + '">' + s.label + '</a>').join('') +
|
|
'</div><div class="settings-main"><div class="animated"><h2 style="font-size:24px;font-weight:700;margin-bottom:24px">' + Lang.t('settings') + '</h2>' +
|
|
|
|
'<div class="settings-section" id="sec-forward"><h3>' + Lang.t('dataForwarding') + '</h3><p style="font-size:12px;color:var(--text-tertiary);margin-bottom:12px">' + Lang.t('dataForwardingDesc') + '</p><div id="forward-list">' + this._forwardListHtml(forwards) + '</div><div style="display:flex;gap:8px;margin-top:8px"><button id="btn-add-forward" class="btn btn-sm btn-secondary">' + Lang.t('addTarget') + '</button><button id="btn-save-forwards" class="btn btn-sm btn-primary">' + Lang.t('saveEffective') + '</button></div></div>' +
|
|
|
|
'<div class="settings-section" id="sec-connection"><h3>' + Lang.t('connectionSettings') + '</h3><div class="settings-row"><div><div class="settings-label">' + Lang.t('telemetryPort') + '</div><div class="settings-desc">' + Lang.t('telemetryPortDesc') + '</div></div><div class="settings-control"><input type="number" id="settings-telemetry-port" value="' + (cfg.telemetry_port||20777) + '" min="1024" max="65535"></div></div><div class="settings-row"><div><div class="settings-label">' + Lang.t('unifiedPortToggle') + '</div><div class="settings-desc">' + Lang.t('unifiedPortDesc') + '</div></div><div class="settings-control"><label class="switch-label"><input type="checkbox" id="settings-unified-port" ' + (cfg.use_unified_port!==false?'checked':'') + '><span class="switch-track"></span></label></div></div><div id="game-ports-section" style="margin-top:8px;padding:12px;background:var(--bg-tertiary);border-radius:8px"><h4 style="font-size:13px;color:var(--text-secondary);margin-bottom:10px">' + Lang.t('perGamePort') + '</h4>' + this._gamePortListHtml(games, cfg) + '<button id="btn-save-game-ports" class="btn btn-sm btn-primary" style="margin-top:8px">' + Lang.t('savePorts') + '</button></div><div class="settings-row"><div><div class="settings-label">' + Lang.t('restartListener') + '</div><div class="settings-desc">' + Lang.t('restartListenerDesc') + '</div></div><div class="settings-control"><button id="settings-restart-telemetry" class="btn btn-secondary btn-sm">' + Lang.t('restartBtn') + '</button></div></div></div>' +
|
|
|
|
'<div class="settings-section" id="sec-dashboards"><h3>' + Lang.t('dashManagement') + '</h3><div style="display:flex;gap:8px;margin-bottom:16px"><button id="btn-import-dashboard" class="btn btn-secondary btn-sm">' + Lang.t('importTsd') + '</button><span style="font-size:12px;color:var(--text-tertiary);display:flex;align-items:center">' + Lang.t('rightClickExport') + '</span></div></div>' +
|
|
|
|
'<div class="settings-section" id="sec-plugins"><h3>' + Lang.t('pluginManagement') + '</h3><div style="display:flex;gap:8px;margin-bottom:16px"><button id="btn-import-plugin" class="btn btn-secondary btn-sm">' + Lang.t('importTsp') + '</button><button id="btn-reload-plugins" class="btn btn-secondary btn-sm">' + Lang.t('reloadPlugins') + '</button></div><div id="settings-game-list">' + this._gamePluginListHtml(games) + '</div></div>' +
|
|
|
|
'<div class="settings-section" id="sec-backup"><h3>' + Lang.t('dataBackup') + '</h3><div class="settings-row"><div><div class="settings-label">' + Lang.t('exportBackup') + '</div><div class="settings-desc">' + Lang.t('backupDesc') + '</div></div><div class="settings-control"><button id="btn-export-backup" class="btn btn-secondary btn-sm">' + Lang.t('downloadTsb') + '</button></div></div><div class="settings-row"><div><div class="settings-label">' + Lang.t('restoreBackup') + '</div><div class="settings-desc">' + Lang.t('restoreBackupDesc') + '</div></div><div class="settings-control"><button id="btn-import-backup" class="btn btn-secondary btn-sm">' + Lang.t('importTsb') + '</button></div></div></div>' +
|
|
|
|
|
|
'<div class="settings-section" id="sec-dev"><h3>' + Lang.t('devTools') + '</h3><div class="settings-row"><div><div class="settings-label">' + Lang.t('testMode') + '</div><div class="settings-desc">' + Lang.t('testModeDesc') + '</div></div><div class="settings-control"><label class="switch-label"><input type="checkbox" id="settings-test-mode" ' + (cfg.ui_test_mode?'checked':'') + '><span class="switch-track"></span></label></div></div></div>' +
|
|
|
|
'<div class="settings-section" id="sec-system"><h3>' + Lang.t('systemSection') + '</h3><div class="settings-row"><div><div class="settings-label">' + Lang.t('restartServer') + '</div><div class="settings-desc">' + Lang.t('restartServerDesc') + '</div></div><div class="settings-control"><button id="btn-restart-server" class="btn btn-danger btn-sm">' + Lang.t('restartBtnSys') + '</button></div></div></div>' +
|
|
|
|
'<div class="settings-section"><h3>' + Lang.t('about') + '</h3><div class="settings-row"><div><div class="settings-label">TurboSu</div><div class="settings-desc">' + Lang.t('aboutDesc') + '<br><a href="https://git.yeij.top/AskaEth/TurboSu" target="_blank" style="font-size:11px;color:var(--accent)">https://git.yeij.top/AskaEth/TurboSu</a></div></div></div></div>' +
|
|
|
|
'</div></div></div>';
|
|
}
|
|
};
|
|
|
|
window.PageSettings = PageSettings;
|