πŸ“‚ File Browser

serversdashboard/serversdashboard/assets
πŸŒ™ Dark Mode
🎯 Quick Launch:

πŸ“ Directories

πŸ“„ Files

πŸ“œ dashboard.js
β–Ά Open πŸ“„ View Source
🎨 players.css
β–Ά Open πŸ“„ View Source
πŸ“œ players.js
β–Ά Open πŸ“„ View Source
🎨 style.css
β–Ά Open πŸ“„ View Source

πŸ“„ 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