π Source: players.js
/**
* Player panel + mod / pack / inventory modals.
*
* Depends on globals from dashboard.js:
* - currentServer
* - api(), post(), escapeHtml()
*/
let currentPlayers = [];
async function refreshPlayers() {
if (!currentServer) return;
const container = document.getElementById('playersList');
if (!container) return; // panel not in DOM yet
const r = await api('players.php?server=' + encodeURIComponent(currentServer));
if (!r.ok) {
container.innerHTML = `<div class="muted" style="padding:10px">${escapeHtml(r.error || 'Unavailable')}</div>`;
currentPlayers = [];
return;
}
currentPlayers = r.players || [];
if (currentPlayers.length === 0) {
container.innerHTML = '<div class="muted" style="padding:10px">No players online</div>';
return;
}
container.innerHTML = '';
currentPlayers.forEach(p => {
const row = document.createElement('div');
row.className = 'player-row';
row.innerHTML = `
<div class="player-name">${escapeHtml(p)}</div>
<div class="player-actions">
<button class="small" onclick="openInventory('${escapeHtml(p)}')">Inventory</button>
<button class="small" onclick="openMods('${escapeHtml(p)}')">Mods</button>
<button class="small" onclick="openPacks('${escapeHtml(p)}')">Packs</button>
</div>
`;
container.appendChild(row);
});
}
/* ---------------- Inventory modal ---------------- */
async function openInventory(player) {
openModal('Inventory β ' + player, '<div class="muted">Loadingβ¦</div>');
const r = await api('inventory.php?server=' + encodeURIComponent(currentServer) + '&player=' + encodeURIComponent(player));
if (!r.ok) {
setModalBody(`<div class="error">${escapeHtml(r.error || 'Failed')}</div>`);
return;
}
let html = '';
if (r.health !== null || r.food !== null || r.xp_level !== null) {
html += '<div class="stats-row">';
if (r.health !== null) html += `<span class="stat-pill">β€ ${r.health}</span>`;
if (r.food !== null) html += `<span class="stat-pill">π ${r.food}</span>`;
if (r.xp_level !== null) html += `<span class="stat-pill">β¦ ${r.xp_level} XP</span>`;
html += '</div>';
}
html += '<h4 class="modal-sub">Inventory</h4>';
html += renderItemGrid(r.inventory);
if (r.enderchest && r.enderchest.length > 0) {
html += '<h4 class="modal-sub">Ender Chest</h4>';
html += renderItemGrid(r.enderchest);
}
setModalBody(html);
}
/**
* Renders a grid of item slots. Shulker boxes get a clickable "contains" badge.
*/
function renderItemGrid(items) {
if (!items || items.length === 0) {
return '<div class="muted">Empty</div>';
}
// Main inventory slots:
// 0-8 hotbar
// 9-35 main inventory
// 36-39 armor
// 40 offhand
// 41+ (some mods add slots; we size to fit)
const maxSlot = items.reduce((max, i) => Math.max(max, (i.slot ?? 0) + 1), 41);
const slotCount = Math.max(41, maxSlot);
// Map by slot
const bySlot = {};
items.forEach(i => {
if (i.slot !== null && i.slot !== undefined) bySlot[i.slot] = i;
});
let html = '<div class="inv-grid">';
for (let s = 0; s < slotCount; s++) {
html += renderSlot(bySlot[s], s);
}
html += '</div>';
return html;
}
function renderSlot(item, slotIndex) {
if (!item) {
return `<div class="inv-slot empty" title="Slot ${slotIndex}"></div>`;
}
const label = item.custom_name || item.name;
const count = item.count > 1 ? `<span class="inv-count">${item.count}</span>` : '';
const hasContains = item.contains && item.contains.length > 0;
const tooltip = buildTooltip(item);
const classes = ['inv-slot'];
if (hasContains) classes.push('has-contains');
if (item.enchantments && item.enchantments.length > 0) classes.push('enchanted');
// Encode the item for the click handler. Use encodeURIComponent on a
// JSON string, then decode it inside openShulker to avoid quoting hell.
const onclick = hasContains
? `onclick="openShulker('${encodeURIComponent(JSON.stringify(item))}')"`
: '';
return `<div class="${classes.join(' ')}" title="${escapeHtml(tooltip)}" ${onclick}>
<span class="inv-item-name">${escapeHtml(label)}</span>
${count}
</div>`;
}
function buildTooltip(item) {
const lines = [];
lines.push(item.custom_name || item.name);
lines.push('ID: ' + item.id);
if (item.count > 1) lines.push('Count: ' + item.count);
if (item.damage !== null && item.damage !== undefined) {
// Newer Minecraft omits max_damage for standard tools β hardcode the common ones
const maxes = {
'wooden_axe': 59, 'wooden_pickaxe': 59, 'wooden_shovel': 59, 'wooden_hoe': 59, 'wooden_sword': 59,
'stone_axe': 131, 'stone_pickaxe': 131, 'stone_shovel': 131, 'stone_hoe': 131, 'stone_sword': 131,
'iron_axe': 250, 'iron_pickaxe': 250, 'iron_shovel': 250, 'iron_hoe': 250, 'iron_sword': 250,
'golden_axe': 32, 'golden_pickaxe': 32, 'golden_shovel': 32, 'golden_hoe': 32, 'golden_sword': 32,
'diamond_axe': 1561, 'diamond_pickaxe': 1561, 'diamond_shovel': 1561, 'diamond_hoe': 1561, 'diamond_sword': 1561,
'netherite_axe': 2031, 'netherite_pickaxe': 2031, 'netherite_shovel': 2031, 'netherite_hoe': 2031, 'netherite_sword': 2031,
'copper_axe': 190, 'copper_pickaxe': 190, 'copper_shovel': 190, 'copper_hoe': 190, 'copper_sword': 190,
'bow': 384, 'crossbow': 465, 'trident': 250, 'elytra': 432, 'fishing_rod': 64,
'flint_and_steel': 64, 'shears': 238, 'shield': 336, 'brush': 64, 'carrot_on_a_stick': 25, 'warped_fungus_on_a_stick': 100,
};
const short = item.id.includes(':') ? item.id.split(':')[1] : item.id;
const max = item.max_damage || maxes[short];
if (max) {
lines.push(`Durability: ${max - item.damage}/${max}`);
} else {
lines.push(`Damage: ${item.damage}`);
}
}
if (item.enchantments && item.enchantments.length) {
lines.push('');
item.enchantments.forEach(e => lines.push(`${e.id} ${romanLevel(e.level)}`));
}
if (item.contains && item.contains.length) {
lines.push('');
lines.push(`Contains ${item.contains.length} item stack(s) β click to view`);
}
return lines.join('\n');
}
function romanLevel(n) {
const map = {1:'I',2:'II',3:'III',4:'IV',5:'V',6:'VI',7:'VII',8:'VIII',9:'IX',10:'X'};
return map[n] || n;
}
function openShulker(encodedJson) {
const item = JSON.parse(decodeURIComponent(encodedJson));
openModal('Shulker β ' + (item.custom_name || item.name), renderItemGrid(item.contains));
}
/* ---------------- Mods / Packs modals ---------------- */
async function openMods(player) {
openModal('Mods β ' + player, '<div class="muted">Loadingβ¦</div>');
const r = await api('clientid.php?server=' + encodeURIComponent(currentServer) + '&player=' + encodeURIComponent(player));
if (!r.ok) {
setModalBody(`<div class="error">${escapeHtml(r.error || 'Failed')}</div>`);
return;
}
const extras = r.mods_extra || [];
let html = '';
if (extras.length === 0) {
html += '<div class="ok">No extra mods beyond baseline β</div>';
} else {
html += `<div class="error">${extras.length} extra mod(s) beyond baseline:</div>`;
html += '<div class="chip-list">';
extras.forEach(m => html += `<span class="chip chip-warn">${escapeHtml(m)}</span>`);
html += '</div>';
}
html += `<details style="margin-top:16px"><summary class="muted">Show all ${r.mods_all.length} mods</summary>`;
html += '<div class="chip-list" style="margin-top:8px">';
r.mods_all.forEach(m => {
const isExtra = extras.includes(m);
html += `<span class="chip ${isExtra ? 'chip-warn' : ''}">${escapeHtml(m)}</span>`;
});
html += '</div></details>';
setModalBody(html);
}
async function openPacks(player) {
openModal('Packs β ' + player, '<div class="muted">Loadingβ¦</div>');
const r = await api('clientid.php?server=' + encodeURIComponent(currentServer) + '&player=' + encodeURIComponent(player));
if (!r.ok) {
setModalBody(`<div class="error">${escapeHtml(r.error || 'Failed')}</div>`);
return;
}
const extras = r.packs_extra || [];
let html = '';
if (extras.length === 0) {
html += '<div class="ok">No extra resource packs beyond baseline β</div>';
} else {
html += `<div class="error">${extras.length} extra pack(s) beyond baseline:</div>`;
html += '<div class="chip-list">';
extras.forEach(p => html += `<span class="chip chip-warn">${escapeHtml(p)}</span>`);
html += '</div>';
}
html += `<details style="margin-top:16px"><summary class="muted">Show all ${r.packs_all.length} packs</summary>`;
html += '<div class="chip-list" style="margin-top:8px">';
r.packs_all.forEach(p => {
const isExtra = extras.includes(p);
html += `<span class="chip ${isExtra ? 'chip-warn' : ''}">${escapeHtml(p)}</span>`;
});
html += '</div></details>';
setModalBody(html);
}
/* ---------------- Modal plumbing ---------------- */
function openModal(title, bodyHtml) {
const bd = document.getElementById('infoModal');
if (!bd) return;
document.getElementById('infoModalTitle').textContent = title;
document.getElementById('infoModalBody').innerHTML = bodyHtml;
bd.classList.add('open');
}
function setModalBody(html) {
const el = document.getElementById('infoModalBody');
if (el) el.innerHTML = html;
}
function closeInfoModal() {
const bd = document.getElementById('infoModal');
if (bd) bd.classList.remove('open');
}
β Back