Files
rust-rcon/js/pages/playerlist.js
T
angryzeroandCopilot 1bfca0d1e0 fix: add loading indicators to all pages
- 'Loading…' shown in charts/fields/output areas while waiting for server response
- Console + chat: clears loading state on first output line
- Player list: table body shows loading row until refresh completes
- Player info / Server info: fields show loading until data arrives

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-13 18:00:31 +02:00

142 lines
5.6 KiB
JavaScript

/**
* Player list page — table with sort, filter, and kick.
*/
function PlayerListPage(params, container) {
var address = Rcon.getAddress() || '';
var html =
'<div id="PlayerListController">' +
'<div class="row mb-3">' +
'<div class="col-6">' +
'<input id="player-filter" class="form-control" placeholder="Filter..">' +
'</div>' +
'<div class="col-6">' +
'<button id="player-refresh" class="btn btn-secondary float-end" type="button"><i class="bi bi-arrow-clockwise"></i></button>' +
'</div>' +
'</div>' +
'<div class="row">' +
'<div class="col-sm-12">' +
'<div class="table-responsive">' +
'<table class="table" id="player-table">' +
'<thead>' +
'<tr>' +
'<th>Actions</th>' +
'<th class="sortable" data-field="DisplayName">Username</th>' +
'<th class="sortable text-end" data-field="SteamID">Steam ID</th>' +
'<th class="sortable text-end" data-field="VoiationLevel">Violation Level</th>' +
'<th class="sortable text-end" data-field="Address">Address</th>' +
'<th class="sortable text-end" data-field="Ping">Ping</th>' +
'<th class="sortable text-end" data-field="ConnectedSeconds">Duration</th>' +
'</tr>' +
'</thead>' +
'<tbody id="player-tbody"><tr><td colspan="7" class="text-center text-white-50">Loading…</td></tr></tbody>' +
'</table>' +
'</div>' +
'</div>' +
'</div>' +
'</div>';
container.innerHTML = html;
var players = [];
var orderBy = '-ConnectedSeconds';
var filterText = '';
function sortPlayers(a, b) {
var desc = orderBy[0] === '-';
var field = desc ? orderBy.substring(1) : orderBy;
var va = a[field], vb = b[field];
if (typeof va === 'string') va = va.toLowerCase();
if (typeof vb === 'string') vb = vb.toLowerCase();
if (va < vb) return desc ? 1 : -1;
if (va > vb) return desc ? -1 : 1;
return 0;
}
function matchesFilter(p) {
if (!filterText) return true;
var ft = filterText.toLowerCase();
return (p.DisplayName && p.DisplayName.toLowerCase().indexOf(ft) !== -1) ||
(p.SteamID && p.SteamID.indexOf(ft) !== -1) ||
(p.Address && p.Address.toLowerCase().indexOf(ft) !== -1);
}
function render() {
var filtered = players.filter(matchesFilter).sort(sortPlayers);
var tbody = document.getElementById('player-tbody');
var html = '';
for (var i = 0; i < filtered.length; i++) {
var p = filtered[i];
var fadeClass = (p.VoiationLevel === 0) ? ' class="fade"' : '';
html +=
'<tr>' +
'<td>' +
'<div class="btn-group">' +
'<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">' +
'<i class="bi bi-gear-fill"></i>' +
'</button>' +
'<ul class="dropdown-menu">' +
'<li><a class="dropdown-item" href="http://www.steamcommunity.com/profiles/' + p.SteamID + '" target="_blank"><i class="bi bi-info-circle-fill"></i> Steam Profile</a></li>' +
'<li><hr class="dropdown-divider"></li>' +
'<li><a class="dropdown-item kick-player" href="#" data-id="' + p.SteamID + '"><i class="bi bi-trash-fill"></i> Kick</a></li>' +
'</ul>' +
'</div>' +
'</td>' +
'<td><a href="#/' + address + '/player/' + p.SteamID + '/">' + (p.DisplayName || '') + '</a></td>' +
'<td class="text-end"><a href="#/' + address + '/player/' + p.SteamID + '/">' + p.SteamID + '</a></td>' +
'<td class="text-end"' + fadeClass + '>' + (p.VoiationLevel || 0) + '</td>' +
'<td class="text-end">' + (p.Address || '') + '</td>' +
'<td class="text-end">' + (p.Ping || 0) + '</td>' +
'<td class="text-end">' + secondsToDuration(p.ConnectedSeconds || 0) + '</td>' +
'</tr>';
}
tbody.innerHTML = html;
// Bind kick buttons
var kicks = tbody.querySelectorAll('.kick-player');
for (var j = 0; j < kicks.length; j++) {
kicks[j].addEventListener('click', function (e) {
e.preventDefault();
Rcon.command('kick ' + this.dataset.id);
refresh();
});
}
}
function refresh() {
Rcon.getPlayers(function (list) {
players = list;
render();
});
}
function updateSortHeaders() {
var headers = document.querySelectorAll('#player-table .sortable');
for (var i = 0; i < headers.length; i++) {
headers[i].className = headers[i].className.replace(/sorting(\s+descending)?/g, '').trim();
var field = headers[i].dataset.field;
if (orderBy === field) headers[i].className += ' sorting';
else if (orderBy === '-' + field) headers[i].className += ' sorting descending';
}
}
document.getElementById('player-refresh').addEventListener('click', refresh);
document.getElementById('player-filter').addEventListener('input', function () {
filterText = this.value;
render();
});
// Sort header clicks
var sortHeaders = document.querySelectorAll('#player-table .sortable');
for (var k = 0; k < sortHeaders.length; k++) {
sortHeaders[k].addEventListener('click', function () {
var field = this.dataset.field;
orderBy = (orderBy === field) ? '-' + field : field;
updateSortHeaders();
render();
});
}
if (Rcon.isConnected()) { refresh(); updateSortHeaders(); }
else { var _c = function () { refresh(); updateSortHeaders(); Rcon.off('connected', _c); }; Rcon.on('connected', _c); }
}