(async function () { const pingEl = document.querySelector('[data-player-ping]'); const statusEl = document.querySelector('[data-player-status]'); const worldEl = document.querySelector('[data-player-world]'); const gamemodeEl = document.querySelector('[data-player-gamemode]'); if (!pingEl) return; const uuid = pingEl.getAttribute('data-uuid'); if (!uuid) return; const { renderItem } = await import('/assets/blockrenderer.js'); // ---- Helpers ---- function escapeHtml(s) { if (s == null) return ''; return String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function toTitle(snake) { if (!snake) return ''; return snake.toLowerCase().replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); } const ROMAN = ['', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X']; function toRoman(n) { return ROMAN[n] || String(n); } function pingColor(ping) { if (ping < 80) return 'text-success'; if (ping < 200) return 'text-warning'; return 'text-destructive'; } // ---- Live header (ping/world/gamemode/status) ---- function setOffline() { pingEl.textContent = '—'; pingEl.classList.remove('text-success', 'text-warning', 'text-destructive'); if (statusEl) { statusEl.textContent = 'offline'; statusEl.classList.remove('text-success'); statusEl.classList.add('text-muted-foreground'); } if (worldEl) worldEl.textContent = '—'; if (gamemodeEl) gamemodeEl.textContent = '—'; } function setOnline(p) { pingEl.textContent = (p.ping | 0) + 'ms'; pingEl.classList.remove('text-success', 'text-warning', 'text-destructive'); pingEl.classList.add(pingColor(p.ping)); if (statusEl) { statusEl.textContent = 'online'; statusEl.classList.remove('text-muted-foreground'); statusEl.classList.add('text-success'); } if (worldEl) worldEl.textContent = p.world || '—'; if (gamemodeEl) gamemodeEl.textContent = p.gamemode ? p.gamemode.toLowerCase() : '—'; } function handle(state) { const players = Array.isArray(state.players) ? state.players : []; const match = players.find(p => p.uuid === uuid); if (match) setOnline(match); else setOffline(); } const staffSrc = new EventSource('/api/players/stream/staff'); staffSrc.addEventListener('message', (evt) => { try { handle(JSON.parse(evt.data)); } catch (e) {} }); // ---- Action dialog wiring ---- const dialog = document.getElementById('action-dialog'); const form = document.getElementById('action-form'); if (dialog && form) { const actionInput = form.querySelector('[data-action-input]'); const actionLabel = form.querySelector('[data-action-label]'); const durationField = form.querySelector('[data-duration-field]'); const reasonField = form.querySelector('[data-reason-field]'); const reasonInput = form.querySelector('input[name="reason"]'); const slotInput = form.querySelector('[data-slot-input]'); const actionDescription = form.querySelector('[data-action-description]'); document.querySelectorAll('[data-admin-action]').forEach(btn => { btn.addEventListener('click', () => { const action = btn.getAttribute('data-admin-action'); const isTemp = btn.getAttribute('data-admin-temp') === 'true'; const noReason = btn.getAttribute('data-admin-no-reason') === 'true'; const selectedRequired = btn.getAttribute('data-selected-required') === 'true'; if (selectedRequired && !selectedKey) return; actionInput.value = action; actionLabel.textContent = action.replace(/-/g, ' '); if (slotInput) slotInput.value = selectedRequired ? selectedKey : ''; durationField.hidden = !isTemp; durationField.querySelector('select').disabled = !isTemp; if (reasonField) reasonField.hidden = noReason; if (reasonInput) { reasonInput.disabled = noReason; reasonInput.required = !noReason; reasonInput.value = ''; } if (actionDescription) { const target = 'Target: ' + escapeHtml(document.querySelector('h1')?.textContent || 'player') + ''; actionDescription.innerHTML = selectedRequired ? target + '
Slot: ' + escapeHtml(selectedKey) + '' : target; } if (typeof dialog.showModal === 'function') dialog.showModal(); else dialog.setAttribute('open', ''); if (!noReason && reasonInput) setTimeout(() => reasonInput.focus(), 0); }); }); form.querySelectorAll('[data-dialog-cancel]').forEach(btn => { btn.addEventListener('click', () => dialog.close()); }); } // ---- Live inventory ---- const invRoot = document.getElementById('inv-root'); if (!invRoot) return; // Latest inventory snapshot, used by the click handler. let lastInv = null; // Slot currently rendered in the detail panel (key like "storage-5"); kept across re-renders so the highlight survives data refreshes. let selectedKey = null; function updateInventoryActionButtons() { const selectedItem = getItemBySlotKey(lastInv, selectedKey); const enabled = !!(lastInv && lastInv.online && selectedKey && selectedItem); document.querySelectorAll('[data-selected-required]').forEach(btn => { btn.disabled = !enabled; if (enabled) btn.removeAttribute('disabled'); else btn.setAttribute('disabled', ''); btn.title = enabled ? 'Clear ' + selectedKey : 'Select an occupied inventory slot first'; }); } function renderDurabilityBar(item) { if (!item.maxDamage) return ''; const damage = item.damage || 0; const remaining = (item.maxDamage - damage) / item.maxDamage; if (remaining >= 0.999) return ''; const cls = remaining > 0.5 ? 'bg-success' : remaining > 0.25 ? 'bg-warning' : 'bg-destructive'; const pct = Math.max(0, Math.min(100, remaining * 100)); return `
`; } function tooltipFor(item) { const parts = []; parts.push(item.name || toTitle(item.type)); if (item.amount > 1) parts[0] += ' ×' + item.amount; if (item.enchants) { for (const [k, v] of Object.entries(item.enchants)) { parts.push(toTitle(k) + ' ' + toRoman(v)); } } if (item.maxDamage) { const remaining = item.maxDamage - (item.damage || 0); parts.push('Durability: ' + remaining + ' / ' + item.maxDamage); } return escapeHtml(parts.join(' • ')); } function renderItemIcon(item) { const name = item.type.toLowerCase(); const label = escapeHtml(name.replace(/_/g, ' ')); return ` ${label} `; } async function hydrateIcons(root) { const targets = Array.from(root.querySelectorAll('[data-item-icon]:not([data-item-hydrated])')); for (const el of targets) el.setAttribute('data-item-hydrated', ''); await Promise.all(targets.map(async el => { const name = el.getAttribute('data-item-icon'); const url = await renderItem(name); if (!url) return; const img = document.createElement('img'); img.className = 'size-full object-contain pointer-events-none [image-rendering:pixelated]'; img.alt = name; img.src = url; el.innerHTML = ''; el.appendChild(img); })); } function renderSlot(item, key) { if (!item) { return `
`; } const tooltip = tooltipFor(item); const amount = item.amount > 1 ? `${item.amount}` : ''; const enchanted = item.enchants ? '' : ''; const selected = key === selectedKey ? 'ring-2 ring-primary' : 'ring-card'; return ` `; } function renderInventoryGrid(inv) { const armor = inv.armor || {}; const storage = inv.storage || []; const hotbar = inv.hotbar || []; return `

Main

${storage.map((s, i) => renderSlot(s, 'storage-' + i)).join('')}
${hotbar.map((s, i) => renderSlot(s, 'hotbar-' + i)).join('')}

Armor

${renderSlot(armor.helmet, 'armor-helmet')} ${renderSlot(armor.chest, 'armor-chest')} ${renderSlot(armor.legs, 'armor-legs')} ${renderSlot(armor.boots, 'armor-boots')}

Offhand

${renderSlot(inv.offhand, 'offhand')}
`; } function renderDetailPanel(item) { if (!item) { return `
Click a slot to inspect the item.
`; } const safeType = escapeHtml(item.type); const safeName = item.name ? escapeHtml(item.name) : null; const lines = []; lines.push(`
${renderItemIcon(item)}
${safeName ? `

${safeName}

` : ''}

${safeType}

Count: ${item.amount}

`); if (item.lore && item.lore.length) { lines.push(`

Lore

`); } if (item.enchants && Object.keys(item.enchants).length) { const rows = Object.entries(item.enchants) .map(([k, v]) => `
  • ${escapeHtml(toTitle(k))}${toRoman(v)}
  • `) .join(''); lines.push(`

    Enchantments

    `); } if (item.maxDamage) { const remaining = item.maxDamage - (item.damage || 0); const pct = Math.max(0, Math.min(100, (remaining / item.maxDamage) * 100)); const cls = pct > 50 ? 'bg-success' : pct > 25 ? 'bg-warning' : 'bg-destructive'; lines.push(`

    Durability

    ${remaining} / ${item.maxDamage}
    `); } const tags = []; if (item.unbreakable) tags.push('Unbreakable'); if (item.flags) item.flags.forEach(f => tags.push(toTitle(f.replace(/^HIDE_/, 'Hide ')))); if (tags.length) { lines.push(`

    Tags

    ${tags.map(t => `${escapeHtml(t)}`).join('')}
    `); } if (item.pdcKeys && item.pdcKeys.length) { lines.push(`

    Plugin NBT keys

    `); } if (item.nbt) { lines.push(`

    NBT

    ${escapeHtml(item.nbt)}
    `); } return `
    ${lines.join('')}
    `; } function getItemBySlotKey(inv, key) { if (!inv || !inv.online || !key) return null; if (key === 'offhand') return inv.offhand || null; if (key.startsWith('storage-')) return (inv.storage || [])[parseInt(key.substring(8), 10)] || null; if (key.startsWith('hotbar-')) return (inv.hotbar || [])[parseInt(key.substring(7), 10)] || null; if (key.startsWith('armor-')) return (inv.armor || {})[key.substring(6)] || null; return null; } function render(inv) { const previousGrid = invRoot.querySelector('[data-inv-grid]'); const previousScrollLeft = previousGrid ? previousGrid.scrollLeft : 0; lastInv = inv; if (!inv.online) { selectedKey = null; invRoot.innerHTML = `

    Player is offline.

    `; updateInventoryActionButtons(); return; } invRoot.innerHTML = `
    ${renderInventoryGrid(inv)}
    ${renderDetailPanel(getItemBySlotKey(inv, selectedKey))}
    `; const grid = invRoot.querySelector('[data-inv-grid]'); if (grid) grid.scrollLeft = previousScrollLeft; hydrateIcons(invRoot); updateInventoryActionButtons(); } invRoot.addEventListener('click', (evt) => { const copyBtn = evt.target.closest('[data-copy-nbt]'); if (copyBtn) { const pre = copyBtn.closest('div').parentElement.querySelector('[data-nbt-text]'); if (pre && navigator.clipboard) { navigator.clipboard.writeText(pre.textContent).then(() => { const original = copyBtn.textContent; copyBtn.textContent = 'Copied'; setTimeout(() => { copyBtn.textContent = original; }, 1500); }).catch(() => {}); } evt.stopPropagation(); return; } const btn = evt.target.closest('[data-slot-key]'); if (!btn) return; selectedKey = btn.getAttribute('data-slot-key'); const item = getItemBySlotKey(lastInv, selectedKey); const detail = invRoot.querySelector('[data-inv-detail]'); if (detail) { detail.innerHTML = renderDetailPanel(item); hydrateIcons(detail); } invRoot.querySelectorAll('[data-slot-key]').forEach(el => { const isSelected = el.getAttribute('data-slot-key') === selectedKey; el.classList.toggle('ring-2', isSelected); el.classList.toggle('ring-primary', isSelected); el.classList.toggle('ring-card', !isSelected); }); updateInventoryActionButtons(); }); const invSrc = new EventSource('/api/player/inventory/stream?uuid=' + encodeURIComponent(uuid)); invSrc.addEventListener('message', (evt) => { try { render(JSON.parse(evt.data)); } catch (e) {} }); })();