- '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>
142 lines
5.6 KiB
JavaScript
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); }
|
|
}
|