420 lines
13 KiB
JavaScript
420 lines
13 KiB
JavaScript
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; |