q3a_rcon_gui/js/renderer.js

420 lines
13 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const MAX_HISTORY = 50;
let servers = [];
let activeTabs = {};
let tabCounter = 0;
let currentTheme = 'terminal';
const themes = {
terminal: 'styles/terminal.css',
dark: 'styles/dark.css',
light: 'styles/light.css'
};
document.addEventListener('DOMContentLoaded', async () => {
servers = await window.rcon.getServers();
setupEventListeners();
setupThemeButtons();
renderServerList();
servers.forEach(s => openServer(s.id));
});
let inputModalCallback = null;
function setupEventListeners() {
document.getElementById('btn-add-server').addEventListener('click', showAddServerModal);
document.getElementById('btn-servers').addEventListener('click', showServersModal);
document.getElementById('btn-cancel-server').addEventListener('click', hideAddServerModal);
document.getElementById('btn-close-servers').addEventListener('click', hideServersModal);
document.getElementById('btn-close-about').addEventListener('click', hideAboutModal);
document.getElementById('btn-about').addEventListener('click', showAboutModal);
document.getElementById('btn-close-reference').addEventListener('click', hideReferenceModal);
document.getElementById('btn-reference').addEventListener('click', showReferenceModal);
document.getElementById('form-server').addEventListener('submit', saveServer);
document.getElementById('btn-cancel-input').addEventListener('click', hideInputModal);
document.getElementById('btn-submit-input').addEventListener('click', submitInputModal);
document.getElementById('modal-input-field').addEventListener('keydown', (e) => {
if (e.key === 'Enter') submitInputModal();
if (e.key === 'Escape') hideInputModal();
});
}
function setupThemeButtons() {
document.querySelectorAll('.theme-btn').forEach(btn => {
btn.addEventListener('click', () => {
setTheme(btn.dataset.theme);
});
});
setTheme(currentTheme);
}
function setTheme(theme) {
currentTheme = theme;
document.getElementById('theme-stylesheet').href = themes[theme];
document.querySelectorAll('.theme-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.theme === theme);
});
}
function showAddServerModal(server = null) {
const modal = document.getElementById('modal-add-server');
document.getElementById('server-id').value = server?.id || '';
document.getElementById('server-name').value = server?.name || '';
document.getElementById('server-address').value = server?.address || '';
document.getElementById('server-port').value = server?.port || 27960;
document.getElementById('server-password').value = server?.password || '';
modal.classList.add('active');
}
function hideAddServerModal() {
document.getElementById('modal-add-server').classList.remove('active');
}
function showServersModal() {
renderServerList();
document.getElementById('modal-servers').classList.add('active');
}
function hideServersModal() {
document.getElementById('modal-servers').classList.remove('active');
}
function showAboutModal() {
document.getElementById('modal-about').classList.add('active');
}
function hideAboutModal() {
document.getElementById('modal-about').classList.remove('active');
}
function showReferenceModal() {
document.getElementById('modal-reference').classList.add('active');
}
function hideReferenceModal() {
document.getElementById('modal-reference').classList.remove('active');
}
function showInputModal(title, placeholder, callback) {
inputModalCallback = callback;
document.getElementById('modal-input-title').textContent = title;
document.getElementById('modal-input-field').placeholder = placeholder;
document.getElementById('modal-input-field').value = '';
document.getElementById('modal-input').classList.add('active');
document.getElementById('modal-input-field').focus();
}
function hideInputModal() {
document.getElementById('modal-input').classList.remove('active');
inputModalCallback = null;
}
function submitInputModal() {
const value = document.getElementById('modal-input-field').value;
document.getElementById('modal-input').classList.remove('active');
if (inputModalCallback) {
const cb = inputModalCallback;
inputModalCallback = null;
cb(value);
}
}
async function saveServer(e) {
e.preventDefault();
const server = {
id: document.getElementById('server-id').value || undefined,
name: document.getElementById('server-name').value,
address: document.getElementById('server-address').value,
port: parseInt(document.getElementById('server-port').value),
password: document.getElementById('server-password').value
};
servers = await window.rcon.saveServer(server);
hideAddServerModal();
renderServerList();
const saved = servers.find(s => s.name === server.name && s.address === server.address);
if (saved && !activeTabs[saved.id]) {
openServer(saved.id);
}
}
async function deleteServer(id) {
if (confirm('Delete this server?')) {
servers = await window.rcon.deleteServer(id);
closeTab(id);
renderServerList();
}
}
function renderServerList() {
const list = document.getElementById('server-list');
if (servers.length === 0) {
list.innerHTML = '<p class="no-servers">No servers configured.</p>';
return;
}
list.innerHTML = servers.map(s => `
<div class="server-item">
<div class="server-info">
<strong>${escapeHtml(s.name)}</strong>
<span>${escapeHtml(s.address)}:${s.port}</span>
</div>
<div class="server-actions">
<button onclick="openServer('${s.id}')">Open</button>
<button onclick="showAddServerModal(servers.find(x => x.id === '${s.id}'))">Edit</button>
<button onclick="deleteServer('${s.id}')" class="btn-danger">Delete</button>
</div>
</div>
`).join('');
}
function openServer(id) {
const server = servers.find(s => s.id === id);
if (!server) return;
if (!activeTabs[id]) {
activeTabs[id] = createTab(server);
tabCounter++;
}
switchToTab(id);
hideServersModal();
}
function createTab(server) {
const id = server.id;
const tab = {
id,
name: server.name,
server,
history: [],
historyIndex: -1
};
const tabEl = document.createElement('div');
tabEl.className = 'tab';
tabEl.id = `tab-${id}`;
tabEl.innerHTML = `
<span class="tab-name">${escapeHtml(server.name)}</span>
<span class="tab-close" data-id="${id}">×</span>
`;
tabEl.addEventListener('click', () => switchToTab(id));
const contentEl = document.createElement('div');
contentEl.className = 'tab-content';
contentEl.id = `content-${id}`;
contentEl.innerHTML = `
<div class="server-header">
<h2>${escapeHtml(server.name)}</h2>
<span class="server-address">${escapeHtml(server.address)}:${server.port}</span>
</div>
<div class="quick-actions">
<span class="qa-buttons">
<button class="qa-btn" data-cmd="status">Status</button>
<button class="qa-btn" data-cmd="serverinfo">Info</button>
<button class="qa-btn" data-cmd="systeminfo">Sys Info</button>
<button class="qa-btn" data-cmd="kick">Kick</button>
<button class="qa-btn" data-cmd="say">Say</button>
<button class="qa-btn" data-cmd="map">Map</button>
<button class="qa-btn" data-cmd="banaddr">Ban</button>
</span>
<button class="qa-btn qa-clear" data-id="${id}">Clear</button>
</div>
<div class="output" id="output-${id}"></div>
<div class="command-area">
<input type="text" class="cmd-input" id="cmd-${id}" placeholder="Enter command...">
<button class="cmd-send" data-id="${id}">Send</button>
</div>
`;
document.getElementById('tabs-bar').appendChild(tabEl);
document.getElementById('server-content').appendChild(contentEl);
const placeholder = document.getElementById('tabs-placeholder');
if (placeholder) placeholder.style.display = 'none';
tabEl.querySelector('.tab-close').addEventListener('click', (e) => {
e.stopPropagation();
closeTab(id);
});
contentEl.querySelector('.cmd-input').addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
sendCommand(id);
} else if (e.key === 'ArrowUp') {
navigateHistory(id, -1);
} else if (e.key === 'ArrowDown') {
navigateHistory(id, 1);
}
});
contentEl.querySelector('.cmd-send').addEventListener('click', () => sendCommand(id));
contentEl.querySelectorAll('.qa-btn[data-cmd]').forEach(btn => {
btn.addEventListener('click', () => handleQuickAction(id, btn.dataset.cmd));
});
contentEl.querySelector('.qa-clear').addEventListener('click', () => {
const output = document.getElementById(`output-${id}`);
if (output) output.innerHTML = '';
});
return tab;
}
function switchToTab(id) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
const tabEl = document.getElementById(`tab-${id}`);
const contentEl = document.getElementById(`content-${id}`);
if (tabEl && contentEl) {
tabEl.classList.add('active');
contentEl.classList.add('active');
contentEl.querySelector('.cmd-input')?.focus();
}
}
function closeTab(id) {
const tabEl = document.getElementById(`tab-${id}`);
const contentEl = document.getElementById(`content-${id}`);
if (tabEl) tabEl.remove();
if (contentEl) contentEl.remove();
delete activeTabs[id];
const remaining = Object.keys(activeTabs);
if (remaining.length > 0) {
switchToTab(remaining[remaining.length - 1]);
} else {
document.getElementById('server-content').innerHTML = `
<div class="welcome-message">
<h1>Q3A RCON Dashboard</h1>
<p>Select or add a server to begin.</p>
</div>
`;
const placeholder = document.getElementById('tabs-placeholder');
if (placeholder) placeholder.style.display = '';
}
}
async function sendCommand(tabId) {
const input = document.getElementById(`cmd-${tabId}`);
const output = document.getElementById(`output-${tabId}`);
const command = input.value.trim();
if (!command || !activeTabs[tabId]) return;
const tab = activeTabs[tabId];
tab.history.push(command);
if (tab.history.length > MAX_HISTORY) tab.history.shift();
tab.historyIndex = tab.history.length;
input.value = '';
appendOutput(tabId, `> ${command}`, 'command');
const result = await window.rcon.send({
address: tab.server.address,
port: tab.server.port,
password: tab.server.password,
command
});
if (result.success) {
appendOutput(tabId, result.response || '(no response)', 'response');
} else {
appendOutput(tabId, `Error: ${result.error}`, 'error');
}
}
async function handleQuickAction(tabId, action) {
const tab = activeTabs[tabId];
if (!tab) return;
let command = action;
let promptTitle = null;
let promptPlaceholder = null;
switch (action) {
case 'kick':
promptTitle = 'Kick Player';
promptPlaceholder = 'Player name or slot number';
command = 'kick ';
break;
case 'say':
promptTitle = 'Broadcast Message';
promptPlaceholder = 'Message to send to all players';
command = 'say ';
break;
case 'map':
promptTitle = 'Change Map';
promptPlaceholder = 'Map name (e.g., q3dm1)';
command = 'map ';
break;
case 'banaddr':
promptTitle = 'Ban IP Address';
promptPlaceholder = 'IP or IP/subnet (e.g., 192.168.1.100)';
command = 'banaddr ';
break;
case 'dumpuser':
promptTitle = 'Dump User Info';
promptPlaceholder = 'Player name or slot number';
command = 'dumpuser ';
break;
default:
break;
}
if (promptTitle) {
showInputModal(promptTitle, promptPlaceholder, (value) => {
if (!value) return;
const fullCommand = command + value;
document.getElementById(`cmd-${tabId}`).value = fullCommand;
sendCommand(tabId);
});
return;
}
document.getElementById(`cmd-${tabId}`).value = command;
await sendCommand(tabId);
}
function navigateHistory(tabId, direction) {
const tab = activeTabs[tabId];
if (!tab || tab.history.length === 0) return;
tab.historyIndex += direction;
if (tab.historyIndex < 0) tab.historyIndex = 0;
if (tab.historyIndex >= tab.history.length) {
tab.historyIndex = tab.history.length;
document.getElementById(`cmd-${tabId}`).value = '';
return;
}
document.getElementById(`cmd-${tabId}`).value = tab.history[tab.historyIndex];
}
function appendOutput(tabId, text, type = '') {
const output = document.getElementById(`output-${tabId}`);
if (!output) return;
const lines = text.split('\n');
lines.forEach(line => {
const div = document.createElement('div');
div.className = `output-line ${type}`;
div.textContent = line;
output.appendChild(div);
});
output.scrollTop = output.scrollHeight;
}
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
window.servers = servers;
window.openServer = openServer;
window.deleteServer = deleteServer;
window.showAddServerModal = showAddServerModal;