9 Commits
3.6.2 ... 3.6.5

7 changed files with 961 additions and 731 deletions

Binary file not shown.

View File

@@ -1,187 +1,187 @@
// background.js // background.js
const POLL_INTERVAL_SEC = 2; const POLL_INTERVAL_SEC = 2;
// Alarm erstellen // Alarm erstellen
chrome.runtime.onInstalled.addListener(() => { chrome.runtime.onInstalled.addListener(() => {
chrome.alarms.create('periodicRefresh', { periodInMinutes: POLL_INTERVAL_SEC / 60 }); chrome.alarms.create('periodicRefresh', { periodInMinutes: POLL_INTERVAL_SEC / 60 });
}); });
chrome.alarms.onAlarm.addListener((alarm) => { chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'periodicRefresh') { if (alarm.name === 'periodicRefresh') {
refreshAllServers().catch(console.error); refreshAllServers().catch(console.error);
} }
}); });
// Message Listener // Message Listener
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg && msg.cmd === 'refreshNow') { if (msg && msg.cmd === 'refreshNow') {
refreshAllServers() refreshAllServers()
.then(statuses => sendResponse({ ok: true, statuses })) .then(statuses => sendResponse({ ok: true, statuses }))
.catch(err => sendResponse({ ok: false, error: String(err) })); .catch(err => sendResponse({ ok: false, error: String(err) }));
return true; return true;
} }
}); });
async function refreshAllServers() { async function refreshAllServers() {
const obj = await chrome.storage.local.get('servers'); const obj = await chrome.storage.local.get('servers');
const servers = obj.servers || []; const servers = obj.servers || [];
if (servers.length === 0) { if (servers.length === 0) {
await chrome.storage.local.set({ serverStatuses: {} }); await chrome.storage.local.set({ serverStatuses: {} });
chrome.action.setBadgeText({ text: '' }); chrome.action.setBadgeText({ text: '' });
return {}; return {};
} }
const results = {}; const results = {};
let totalOnlinePlayers = 0; let totalOnlinePlayers = 0;
const tasks = servers.map(async (srv) => { const tasks = servers.map(async (srv) => {
try { try {
const data = await fetchServerStatus(srv, 2000); const data = await fetchServerStatus(srv, 2000);
results[srv.id] = { ok: true, server: srv, fetched: Date.now(), data }; results[srv.id] = { ok: true, server: srv, fetched: Date.now(), data };
if (data) { if (data) {
if (Array.isArray(data.players)) totalOnlinePlayers += data.players.length; if (Array.isArray(data.players)) totalOnlinePlayers += data.players.length;
else if (typeof data.players === 'number') totalOnlinePlayers += data.players; else if (typeof data.players === 'number') totalOnlinePlayers += data.players;
} }
} catch (e) { } catch (e) {
results[srv.id] = { ok: false, server: srv, fetched: Date.now(), error: String(e) }; results[srv.id] = { ok: false, server: srv, fetched: Date.now(), error: String(e) };
} }
}); });
await Promise.all(tasks); await Promise.all(tasks);
await chrome.storage.local.set({ serverStatuses: results }); await chrome.storage.local.set({ serverStatuses: results });
const badgeText = totalOnlinePlayers > 0 ? String(totalOnlinePlayers) : ''; const badgeText = totalOnlinePlayers > 0 ? String(totalOnlinePlayers) : '';
chrome.action.setBadgeText({ text: badgeText }); chrome.action.setBadgeText({ text: badgeText });
chrome.action.setBadgeBackgroundColor({ color: '#3b82f6' }); chrome.action.setBadgeBackgroundColor({ color: '#3b82f6' });
return results; return results;
} }
async function fetchServerStatus(server, timeoutMs = 2000) { async function fetchServerStatus(server, timeoutMs = 2000) {
let serverObj = typeof server === 'string' ? { id: 'legacy', url: server } : server; let serverObj = typeof server === 'string' ? { id: 'legacy', url: server } : server;
let fetchUrl = serverObj.url || ''; let fetchUrl = serverObj.url || '';
// 1. Direkte URL-Abfrage // 1. Direkte URL-Abfrage
if (fetchUrl) { if (fetchUrl) {
if (!/^https?:\/\//i.test(fetchUrl)) fetchUrl = 'http://' + fetchUrl; if (!/^https?:\/\//i.test(fetchUrl)) fetchUrl = 'http://' + fetchUrl;
try { try {
const startTime = performance.now(); const startTime = performance.now();
const controller = new AbortController(); const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeoutMs); const id = setTimeout(() => controller.abort(), timeoutMs);
const resp = await fetch(fetchUrl, { method: 'GET', signal: controller.signal }); const resp = await fetch(fetchUrl, { method: 'GET', signal: controller.signal });
clearTimeout(id); clearTimeout(id);
const endTime = performance.now(); const endTime = performance.now();
const latency = Math.round(endTime - startTime); const latency = Math.round(endTime - startTime);
if (resp.ok) { if (resp.ok) {
let text = await resp.text(); let text = await resp.text();
const lastBrace = text.lastIndexOf('}'); const lastBrace = text.lastIndexOf('}');
if (lastBrace !== -1) text = text.substring(0, lastBrace + 1); if (lastBrace !== -1) text = text.substring(0, lastBrace + 1);
text = text.trim(); text = text.trim();
try { try {
const parsed = JSON.parse(text); const parsed = JSON.parse(text);
if (parsed && (typeof parsed.online !== 'undefined' || Array.isArray(parsed.players) || parsed.version)) { if (parsed && (typeof parsed.online !== 'undefined' || Array.isArray(parsed.players) || parsed.version)) {
parsed.ping = latency; // Ping hinzufügen parsed.ping = latency; // Ping hinzufügen
return parsed; return parsed;
} }
} catch (e) { } catch (e) {
// Parsing fehlgeschlagen → Fallback-Objekt mit Ping // Parsing fehlgeschlagen → Fallback-Objekt mit Ping
return { return {
online: false, online: false,
players: [], players: [],
max_players: 0, max_players: 0,
version: 'unknown', version: 'unknown',
motd: '', motd: '',
ping: latency > timeoutMs ? null : latency // bei Timeout kein sinnvoller Ping ping: latency > timeoutMs ? null : latency // bei Timeout kein sinnvoller Ping
}; };
} }
} }
// HTTP nicht ok → Offline mit gemessener Latenz (falls unter Timeout) // HTTP nicht ok → Offline mit gemessener Latenz (falls unter Timeout)
return { return {
online: false, online: false,
players: [], players: [],
max_players: 0, max_players: 0,
version: 'unknown', version: 'unknown',
motd: '', motd: '',
ping: latency > timeoutMs ? null : latency ping: latency > timeoutMs ? null : latency
}; };
} catch (e) { } catch (e) {
// Timeout oder Netzwerkfehler // Timeout oder Netzwerkfehler
return { return {
online: false, online: false,
players: [], players: [],
max_players: 0, max_players: 0,
version: 'unknown', version: 'unknown',
motd: '', motd: '',
ping: null ping: null
}; };
} }
} }
// 2. WordPress AJAX-Abfrage // 2. WordPress AJAX-Abfrage
const wpSite = serverObj.wpSite ? String(serverObj.wpSite).replace(/\/$/, '') : null; const wpSite = serverObj.wpSite ? String(serverObj.wpSite).replace(/\/$/, '') : null;
const wpServerId = serverObj.wpServerId ? String(serverObj.wpServerId) : null; const wpServerId = serverObj.wpServerId ? String(serverObj.wpServerId) : null;
if (wpSite && wpServerId) { if (wpSite && wpServerId) {
try { try {
const ajaxUrl = wpSite + '/wp-admin/admin-ajax.php'; const ajaxUrl = wpSite + '/wp-admin/admin-ajax.php';
const body = 'action=mcss_fetch&server_id=' + encodeURIComponent(wpServerId); const body = 'action=mcss_fetch&server_id=' + encodeURIComponent(wpServerId);
const startTime = performance.now(); const startTime = performance.now();
const controller2 = new AbortController(); const controller2 = new AbortController();
const id2 = setTimeout(() => controller2.abort(), timeoutMs); const id2 = setTimeout(() => controller2.abort(), timeoutMs);
const resp2 = await fetch(ajaxUrl, { const resp2 = await fetch(ajaxUrl, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body, body,
signal: controller2.signal signal: controller2.signal
}); });
clearTimeout(id2); clearTimeout(id2);
const endTime = performance.now(); const endTime = performance.now();
const latency = Math.round(endTime - startTime); const latency = Math.round(endTime - startTime);
if (!resp2.ok) throw new Error('WP AJAX HTTP ' + resp2.status); if (!resp2.ok) throw new Error('WP AJAX HTTP ' + resp2.status);
const json = await resp2.json(); const json = await resp2.json();
if (json && (typeof json.online !== 'undefined' || Array.isArray(json.players) || json.version)) { if (json && (typeof json.online !== 'undefined' || Array.isArray(json.players) || json.version)) {
json.ping = latency; // Ping hinzufügen json.ping = latency; // Ping hinzufügen
return json; return json;
} }
// JSON nicht im erwarteten Format // JSON nicht im erwarteten Format
return { return {
online: false, online: false,
players: [], players: [],
max_players: 0, max_players: 0,
version: 'unknown', version: 'unknown',
motd: '', motd: '',
ping: latency > timeoutMs ? null : latency ping: latency > timeoutMs ? null : latency
}; };
} catch (e) { } catch (e) {
return { return {
online: false, online: false,
players: [], players: [],
max_players: 0, max_players: 0,
version: 'unknown', version: 'unknown',
motd: '', motd: '',
ping: null ping: null
}; };
} }
} }
// Keine gültige Konfiguration // Keine gültige Konfiguration
return { return {
online: false, online: false,
players: [], players: [],
max_players: 0, max_players: 0,
version: 'unknown', version: 'unknown',
motd: '', motd: '',
ping: null ping: null
}; };
} }

View File

@@ -10,34 +10,63 @@
--offline: #ef4444; --offline: #ef4444;
} }
* { box-sizing:border-box; } * { box-sizing: border-box; }
html,body { margin:0; padding:0; font-family:Inter, Arial, sans-serif; background:var(--bg); color:var(--text);}
.root { width:520px; max-width:520px; padding:12px; }
header { display:flex; align-items:center; justify-content:space-between; margin-bottom:8px; } html, body {
header h1 { font-size:16px; margin:0; } margin: 0;
header .actions button { background:transparent; border:1px solid var(--muted); color:var(--text); padding:4px 8px; border-radius:6px; cursor:pointer; } padding: 0;
font-family: Inter, Arial, sans-serif;
background: var(--bg);
color: var(--text);
overflow-x: hidden;
}
.root {
width: 520px;
padding: 12px;
display: flex;
flex-direction: column;
}
header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
header h1 { font-size: 16px; margin: 0; }
header .actions button {
background: transparent;
border: 1px solid var(--muted);
color: var(--text);
padding: 4px 8px;
border-radius: 6px;
cursor: pointer;
}
.main { .main {
display:flex; display: flex;
gap:12px; gap: 12px;
position: relative; position: relative;
align-items: flex-start;
} }
.server-list { .server-list {
width:45%; width: 45%;
background:var(--panel); background: var(--panel);
padding:8px; padding: 8px;
border-radius:var(--radius); border-radius: var(--radius);
flex-shrink: 0; flex-shrink: 0;
} }
.detail { .detail {
flex:1; flex: 1;
background:var(--panel); background: var(--panel);
padding:8px; padding: 8px;
border-radius:var(--radius); border-radius: var(--radius);
min-height:220px; min-height: 220px;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
@@ -46,175 +75,143 @@ header .actions button { background:transparent; border:1px solid var(--muted);
flex: 1 1 100%; flex: 1 1 100%;
} }
.add-form { display:flex; flex-direction:column; gap:6px; margin-bottom:8px; } .add-form { display: flex; flex-direction: column; gap: 6px; margin-bottom: 8px; }
.add-form input { padding:6px 8px; border-radius:6px; border:1px solid #21303b; background:#071019; color:var(--text); } .add-form input { padding: 6px 8px; border-radius: 6px; border: 1px solid #21303b; background: #071019; color: var(--text); }
.add-form button { padding:6px 8px; border-radius:6px; border:none; background:var(--accent); color:white; cursor:pointer; } .add-form button { padding: 6px 8px; border-radius: 6px; border: none; background: var(--accent); color: white; cursor: pointer; }
#serversContainer {
list-style: none;
margin: 0;
padding: 0;
}
#serversContainer { list-style:none; margin:0; padding:0; max-height:260px; overflow:auto; }
.server-item { .server-item {
display:flex; display: flex;
align-items:center; align-items: center;
justify-content:space-between; justify-content: space-between;
padding:6px; padding: 8px;
margin-bottom:6px; margin-bottom: 6px;
border-radius:6px; border-radius: 6px;
cursor:pointer; cursor: pointer;
background: rgba(255,255,255,0.02); background: rgba(255,255,255,0.02);
transition: background 0.2s ease; transition: background 0.2s ease;
} }
.server-item:hover { background: rgba(255,255,255,0.05); } .server-item:hover { background: rgba(255,255,255,0.05); }
.server-item .meta { display: flex; flex-direction: column; }
.server-item .meta .name { font-weight: 600; }
.server-item .meta .url { font-size: 11px; color: var(--muted); }
.server-item .meta { display:flex; flex-direction:column; }
.server-item .meta .name { font-weight:600; }
.server-item .meta .url { font-size:12px; color:var(--muted); }
.server-item .status-bubble { .server-item .status-bubble {
font-size:12px; font-size: 11px;
padding:3px 6px; padding: 2px 8px;
border-radius:999px; border-radius: 999px;
color:#fff; color: #fff;
transition: background-color 0.3s ease;
} }
.placeholder { color:var(--muted); padding:12px; } .placeholder { color: var(--muted); padding: 12px; font-size: 13px; }
.hidden { display:none; } .hidden { display: none; }
/* --- Detail Header (Zeile 1) --- */
.detail-header { .detail-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 8px; margin-bottom: 12px;
} }
.server-identity h2 { .server-identity h2 { margin: 0; font-size: 18px; line-height: 1.2; }
margin: 0; .server-url { display: block; font-size: 12px; color: var(--muted); margin-top: 2px; }
font-size: 18px;
line-height: 1.2;
}
.server-url {
display: block;
font-size: 12px;
color: var(--muted);
margin-top: 2px;
}
.status-wrapper { .status-wrapper {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
font-weight: 600; font-weight: 600;
font-size: 13px;
} }
/* --- Pulsierender Punkt Animation --- */
.pulsing-dot { .pulsing-dot {
width: 10px; width: 10px; height: 10px; border-radius: 50%;
height: 10px; background-color: var(--offline);
border-radius: 50%; box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7);
background-color: var(--offline); /* Standard Offline Rot */ animation: pulse-red 2s infinite;
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7);
animation: pulse-red 2s infinite;
} }
.pulsing-dot.online { .pulsing-dot.online {
background-color: var(--online); /* Online Grün */ background-color: var(--online);
animation: pulse-green 2s infinite; animation: pulse-green 2s infinite;
} }
@keyframes pulse-green { @keyframes pulse-green { 0% {box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.7);} 70% {box-shadow: 0 0 0 8px rgba(34, 197, 94, 0);} 100% {box-shadow: 0 0 0 0 rgba(34, 197, 94, 0);} }
0% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.7); } @keyframes pulse-red { 0% {box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7);} 70% {box-shadow: 0 0 0 8px rgba(239, 68, 68, 0);} 100% {box-shadow: 0 0 0 0 rgba(239, 68, 68, 0);} }
70% { box-shadow: 0 0 0 10px rgba(34, 197, 94, 0); }
100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); }
}
@keyframes pulse-red {
0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); }
70% { box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); }
100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); }
}
/* --- Detail Stats (Zeile 2) --- */
.detail-stats { .detail-stats {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; background: rgba(255,255,255,0.05);
background: rgba(255,255,255,0.05); padding: 10px;
padding: 8px; border-radius: var(--radius);
border-radius: var(--radius); margin-bottom: 12px;
margin-bottom: 16px; font-size: 13px;
font-size: 13px;
} }
/* ---------------------------------- */
.playerList { .playerList {
list-style:none; list-style: none;
padding-left:16px; padding: 10px 0;
max-height:140px; margin: 0;
overflow:auto; display: flex;
margin:6px 0; flex-wrap: wrap;
display:flex; gap: 12px;
flex-wrap:wrap; justify-content: center; /* Das hier setzt die Köpfe wieder in die Mitte */
gap:8px;
} }
.playerList li { .player-item {
display:flex; position: relative;
align-items:center; display: flex;
gap:4px; flex-direction: column;
font-size:13px; align-items: center;
color:var(--text);
} }
.player-avatar { .player-avatar {
width:24px; width: 42px; height: 42px;
height:24px; border-radius: 6px;
border-radius:4px;
transition: transform 0.2s ease; transition: transform 0.2s ease;
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
border: 2px solid rgba(255,255,255,0.1);
} }
.player-avatar:hover { transform: scale(1.1); } .player-avatar:hover {
transform: scale(1.1);
.detailButtons { border-color: var(--accent);
display:flex;
gap:8px;
margin-top:8px;
transition: opacity 0.3s ease;
} }
.detailButtons button { .player-hover-info {
padding:6px 8px; position: absolute;
border-radius:6px; bottom: -28px;
border:none; background: rgba(0, 0, 0, 0.95);
cursor:pointer; color: white;
transition: opacity 0.2s ease, transform 0.1s ease; padding: 3px 10px;
border-radius: 4px;
font-size: 11px;
white-space: nowrap;
pointer-events: none;
opacity: 0;
transform: translateY(5px);
transition: all 0.2s ease;
z-index: 100;
border: 1px solid rgba(255,255,255,0.2);
} }
.detailButtons button:hover { opacity: 0.9; transform: translateY(-1px); } .player-item:hover .player-hover-info {
.detailButtons button:active { transform: translateY(0); } opacity: 1;
transform: translateY(0);
#btnEdit { background: #f59e0b; color:#000; }
#btnDelete { background: var(--danger); color:#fff; }
#serversContainer::-webkit-scrollbar,
.playerList::-webkit-scrollbar {
width: 6px;
} }
#serversContainer::-webkit-scrollbar-track, .detailButtons { display: flex; gap: 8px; margin-top: 15px; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 10px; }
.playerList::-webkit-scrollbar-track { .detailButtons button { padding: 6px 12px; border-radius: 6px; border: none; cursor: pointer; font-size: 12px; font-weight: 600; }
background: rgba(255,255,255,0.05); #btnEdit { background: #f59e0b; color: #000; }
border-radius: 3px; #btnDelete { background: var(--danger); color: #fff; }
}
#serversContainer::-webkit-scrollbar-thumb, ::-webkit-scrollbar { width: 6px; }
.playerList::-webkit-scrollbar-thumb { ::-webkit-scrollbar-track { background: transparent; }
background: var(--muted); ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 10px; }
border-radius: 3px; ::-webkit-scrollbar-thumb:hover { background: var(--accent); }
}
#serversContainer::-webkit-scrollbar-thumb:hover,
.playerList::-webkit-scrollbar-thumb:hover {
background: var(--accent);
}

View File

@@ -33,7 +33,6 @@
<div id="noSelection" class="placeholder">Wähle einen Server aus der linken Liste.</div> <div id="noSelection" class="placeholder">Wähle einen Server aus der linken Liste.</div>
<div id="detailContent" class="detailContent hidden"> <div id="detailContent" class="detailContent hidden">
<!-- NEUES LAYOUT ZEILE 1: Name, URL, Status (mit Pulsierendem Punkt) -->
<div class="detail-header"> <div class="detail-header">
<div class="server-identity"> <div class="server-identity">
<h2 id="detailName"></h2> <h2 id="detailName"></h2>
@@ -45,7 +44,6 @@
</div> </div>
</div> </div>
<!-- NEUES LAYOUT ZEILE 2: Spieler, Version, Ping -->
<div class="detail-stats"> <div class="detail-stats">
<div><strong>Spieler:</strong> <span id="detailPlayers"></span></div> <div><strong>Spieler:</strong> <span id="detailPlayers"></span></div>
<div><strong>Version:</strong> <span id="detailVersion"></span></div> <div><strong>Version:</strong> <span id="detailVersion"></span></div>

View File

@@ -1,384 +1,335 @@
const $ = id => document.getElementById(id); //popup.js
const uid = () => 'srv_' + Math.random().toString(36).slice(2,9); const $ = id => document.getElementById(id);
const uid = () => 'srv_' + Math.random().toString(36).slice(2,9);
const inputName = $('inputName');
const inputUrl = $('inputUrl'); const inputName = $('inputName');
const inputWpSite = $('inputWpSite'); const inputUrl = $('inputUrl');
const inputWpServerId = $('inputWpServerId'); const inputWpSite = $('inputWpSite');
const btnAdd = $('btnAddServer'); const inputWpServerId = $('inputWpServerId');
const serversContainer = $('serversContainer'); const btnAdd = $('btnAddServer');
const serverListPanel = document.querySelector('.server-list'); const serversContainer = $('serversContainer');
const btnRefresh = $('btnRefresh'); const serverListPanel = document.querySelector('.server-list');
const btnToggleSettings = $('btnToggleSettings'); const btnRefresh = $('btnRefresh');
const settingsForm = $('settingsForm'); const btnToggleSettings = $('btnToggleSettings');
const settingsForm = $('settingsForm');
const noSelection = $('noSelection');
const detailContent = $('detailContent'); const noSelection = $('noSelection');
const detailName = $('detailName'); const detailContent = $('detailContent');
const detailUrlText = $('detailUrlText'); const detailName = $('detailName');
const detailStatus = $('detailStatus'); const detailUrlText = $('detailUrlText');
const detailPulse = $('detailPulse'); const detailStatus = $('detailStatus');
const detailVersion = $('detailVersion'); const detailPulse = $('detailPulse');
const detailPlayers = $('detailPlayers'); const detailVersion = $('detailVersion');
const detailPing = $('detailPing'); const detailPlayers = $('detailPlayers');
const detailPlayerList = $('detailPlayerList'); const detailPing = $('detailPing');
const btnEdit = $('btnEdit'); const detailPlayerList = $('detailPlayerList');
const btnDelete = $('btnDelete'); const btnEdit = $('btnEdit');
const btnDelete = $('btnDelete');
let servers = [];
let selectedId = null; let servers = [];
let statuses = {}; let selectedId = null;
let previousStatuses = {}; let statuses = {};
let settingsVisible = false; let previousStatuses = {};
let settingsVisible = false;
document.addEventListener('DOMContentLoaded', init);
btnAdd.addEventListener('click', handleAdd); document.addEventListener('DOMContentLoaded', init);
btnRefresh.addEventListener('click', manualRefresh); btnAdd.addEventListener('click', handleAdd);
btnToggleSettings.addEventListener('click', toggleSettings); btnRefresh.addEventListener('click', manualRefresh);
btnEdit.addEventListener('click', handleEdit); btnToggleSettings.addEventListener('click', toggleSettings);
btnDelete.addEventListener('click', handleDelete); btnEdit.addEventListener('click', handleEdit);
btnDelete.addEventListener('click', handleDelete);
async function init() {
await loadServersFromStorage(); async function init() {
await loadStatusesFromStorage(); await loadServersFromStorage();
await loadSettingsVisibility(); await loadStatusesFromStorage();
renderServerList(); await loadSettingsVisibility();
renderServerList();
if (servers.length === 1) selectedId = servers[0].id;
renderDetail(selectedId); if (servers.length === 1) selectedId = servers[0].id;
renderDetail(selectedId);
adjustDetailLayout();
} adjustDetailLayout();
}
// --- Settings Visibility ---
async function loadSettingsVisibility() { // --- NEU: Minecraft Color Parser (Eingebaut ohne Löschung) ---
const obj = await chrome.storage.local.get(['settingsVisible']); function parseMinecraftColors(text) {
settingsVisible = obj.settingsVisible !== undefined ? obj.settingsVisible : false; if (!text) return '';
if (!settingsVisible) settingsForm.classList.add('hidden'); const map = {
else settingsForm.classList.remove('hidden'); '&0': '#000000', '&1': '#0000AA', '&2': '#00AA00', '&3': '#00AAAA',
} '&4': '#AA0000', '&5': '#AA00AA', '&6': '#FFAA00', '&7': '#AAAAAA',
'&8': '#555555', '&9': '#5555FF', '&a': '#55FF55', '&b': '#55FFFF',
async function saveSettingsVisibility() { '&c': '#FF5555', '&d': '#FF55FF', '&e': '#FFFF55', '&f': '#FFFFFF'
await chrome.storage.local.set({ settingsVisible }); };
} let html = text.replace(/[<>]/g, '');
Object.keys(map).forEach(code => {
async function toggleSettings() { const color = map[code];
settingsVisible = !settingsVisible; const regex = new RegExp(code, 'g');
settingsForm.classList.toggle('hidden'); html = html.replace(regex, `</span><span style="color: ${color}">`);
await saveSettingsVisibility(); });
adjustDetailLayout(); return `<span>${html}</span>`.replace(/&l/g, '<b>').replace(/&r/g, '</b>');
} }
// --- Storage --- // --- 3D Avatar URL Generator ---
async function loadServersFromStorage() { function get3DAvatarUrl(playerName, uuid = null) {
const obj = await chrome.storage.local.get(['servers']); const isBedrock = playerName.includes('.') || (uuid && uuid.startsWith('xuid'));
servers = obj.servers || []; if (isBedrock) {
for (const s of servers) if (!s.id) s.id = uid(); if (uuid && uuid.length > 0) {
await chrome.storage.local.set({ servers }); return `https://mc-heads.net/head/${encodeURIComponent(uuid)}/64`;
} }
return `https://mc-heads.net/head/${encodeURIComponent(playerName)}/64`;
async function saveServersToStorage() { } else {
await chrome.storage.local.set({ servers }); return `https://mc-heads.net/head/${encodeURIComponent(playerName)}/64`;
} }
}
async function loadStatusesFromStorage() {
const obj = await chrome.storage.local.get(['serverStatuses']); async function loadSettingsVisibility() {
statuses = obj.serverStatuses || {}; const obj = await chrome.storage.local.get(['settingsVisible']);
} settingsVisible = obj.settingsVisible !== undefined ? obj.settingsVisible : false;
if (!settingsVisible) settingsForm.classList.add('hidden');
// --- Layout --- else settingsForm.classList.remove('hidden');
function adjustDetailLayout() { }
const settingsHidden = settingsForm.classList.contains('hidden');
detailContent.classList.toggle('full-width', settingsHidden); async function saveSettingsVisibility() {
serverListPanel.classList.toggle('hidden', settingsHidden); await chrome.storage.local.set({ settingsVisible });
btnEdit.style.display = settingsHidden ? 'none' : 'inline-block'; }
btnDelete.style.display = settingsHidden ? 'none' : 'inline-block';
} async function toggleSettings() {
settingsVisible = !settingsVisible;
// --- Render Server List --- settingsForm.classList.toggle('hidden');
function renderServerList() { await saveSettingsVisibility();
serversContainer.innerHTML = ''; adjustDetailLayout();
if (servers.length === 0) { }
serversContainer.innerHTML = '<div class="placeholder">Noch keine Server hinzugefügt.</div>';
return; async function loadServersFromStorage() {
} const obj = await chrome.storage.local.get(['servers']);
servers = obj.servers || [];
for (const s of servers) { for (const s of servers) if (!s.id) s.id = uid();
const item = document.createElement('li'); await chrome.storage.local.set({ servers });
item.className = 'server-item'; }
item.dataset.id = s.id;
async function saveServersToStorage() {
const meta = document.createElement('div'); await chrome.storage.local.set({ servers });
meta.className = 'meta'; }
const name = document.createElement('div');
name.className = 'name'; async function loadStatusesFromStorage() {
name.textContent = s.name || '(Kein Name)'; const obj = await chrome.storage.local.get(['serverStatuses']);
statuses = obj.serverStatuses || {};
let listUrl = s.url || (s.wpSite ? s.wpSite + ' (WP)' : ''); }
listUrl = listUrl.replace(':9191', '');
function adjustDetailLayout() {
const url = document.createElement('div'); const settingsHidden = settingsForm.classList.contains('hidden');
url.className = 'url'; detailContent.classList.toggle('full-width', settingsHidden);
url.textContent = listUrl; serverListPanel.classList.toggle('hidden', settingsHidden);
meta.append(name, url); btnEdit.style.display = settingsHidden ? 'none' : 'inline-block';
btnDelete.style.display = settingsHidden ? 'none' : 'inline-block';
const statusBubble = document.createElement('div'); }
statusBubble.className = 'status-bubble';
statusBubble.textContent = '—'; function renderServerList() {
statusBubble.style.backgroundColor = 'transparent'; serversContainer.innerHTML = '';
item.statusBubble = statusBubble; if (servers.length === 0) {
serversContainer.innerHTML = '<div class="placeholder">Noch keine Server hinzugefügt.</div>';
item.append(meta, statusBubble); return;
item.addEventListener('click', () => { }
selectedId = s.id; for (const s of servers) {
renderDetail(selectedId); const item = document.createElement('li');
}); item.className = 'server-item';
item.dataset.id = s.id;
serversContainer.appendChild(item); const meta = document.createElement('div');
} meta.className = 'meta';
updateServerListStatuses(false); const name = document.createElement('div');
} name.className = 'name';
name.textContent = s.name || '(Kein Name)';
// --- Render Detail --- let listUrl = s.url || (s.wpSite ? s.wpSite + ' (WP)' : '');
function renderDetail(id) { listUrl = listUrl.replace(':9191', '');
if (!id) { const url = document.createElement('div');
noSelection.classList.remove('hidden'); url.className = 'url';
detailContent.classList.add('hidden'); url.textContent = listUrl;
return; meta.append(name, url);
} const statusBubble = document.createElement('div');
const srv = servers.find(x => x.id === id); statusBubble.className = 'status-bubble';
if (!srv) { statusBubble.textContent = '—';
noSelection.classList.remove('hidden'); statusBubble.style.backgroundColor = 'transparent';
detailContent.classList.add('hidden'); item.statusBubble = statusBubble;
return; item.append(meta, statusBubble);
} item.addEventListener('click', () => {
noSelection.classList.add('hidden'); selectedId = s.id;
detailContent.classList.remove('hidden'); renderDetail(selectedId);
});
detailName.textContent = srv.name || 'Unbenannter Server'; serversContainer.appendChild(item);
}
let urlToShow = srv.url || (srv.wpSite ? srv.wpSite : 'Lokal'); updateServerListStatuses();
urlToShow = urlToShow.replace(':9191', ''); }
detailUrlText.textContent = urlToShow; // --- Fortsetzung popup.js ---
updateDetailForServer(srv, true); function renderDetail(id) {
} if (!id) {
noSelection.classList.remove('hidden');
// --- Update Detail --- detailContent.classList.add('hidden');
function updateDetailForServer(srv, force = false) { return;
const st = statuses[srv.id]; }
const prevSt = previousStatuses[srv.id]; const srv = servers.find(x => x.id === id);
if (!srv) {
const statusChanged = force || noSelection.classList.remove('hidden');
!prevSt || detailContent.classList.add('hidden');
(prevSt.ok !== st?.ok) || return;
(prevSt.data?.online !== st?.data?.online); }
noSelection.classList.add('hidden');
if (!st || !st.ok || !st.data) { detailContent.classList.remove('hidden');
if (statusChanged) { detailName.textContent = srv.name || 'Unbenannter Server';
detailStatus.textContent = 'Offline'; let urlToShow = srv.url || (srv.wpSite ? srv.wpSite : 'Lokal');
detailPulse.classList.remove('online'); urlToShow = urlToShow.replace(':9191', '');
detailVersion.textContent = '-'; detailUrlText.textContent = urlToShow;
detailPlayers.textContent = '-'; updateDetailForServer(srv, true);
detailPing.textContent = '-'; }
updatePlayerList([]);
} function updateDetailForServer(srv, force = false) {
} else { const st = statuses[srv.id];
const d = st.data; const prevSt = previousStatuses[srv.id];
const statusChanged = force || !prevSt || (prevSt.ok !== st?.ok) || (prevSt.data?.online !== st?.data?.online);
if (statusChanged) {
detailStatus.textContent = d.online ? 'Online' : 'Offline'; if (!st || !st.ok || !st.data) {
if (d.online) { if (statusChanged) {
detailPulse.classList.add('online'); detailStatus.textContent = 'Offline';
} else { detailPulse.classList.remove('online');
detailPulse.classList.remove('online'); detailVersion.textContent = '-';
} detailPlayers.textContent = '-';
} detailPing.textContent = '-';
updatePlayerList([]);
const newVersion = d.version || 'unknown'; }
if (force || detailVersion.textContent !== newVersion) { } else {
detailVersion.textContent = newVersion; const d = st.data;
} if (statusChanged) {
detailStatus.textContent = d.online ? 'Online' : 'Offline';
const playersCount = Array.isArray(d.players) ? d.players.length : (typeof d.players === 'number' ? d.players : 0); if (d.online) detailPulse.classList.add('online');
const maxPlayers = d.max_players; else detailPulse.classList.remove('online');
let newPlayersText = String(playersCount); }
if (maxPlayers && maxPlayers !== '-1') { const newVersion = d.version || 'unknown';
newPlayersText += ` / ${maxPlayers}`; if (force || detailVersion.textContent !== newVersion) detailVersion.textContent = newVersion;
} const playersCount = Array.isArray(d.players) ? d.players.length : (typeof d.players === 'number' ? d.players : 0);
const maxPlayers = d.max_players;
if (force || detailPlayers.textContent !== newPlayersText) { let newPlayersText = String(playersCount);
detailPlayers.textContent = newPlayersText; if (maxPlayers && maxPlayers !== '-1') newPlayersText += ` / ${maxPlayers}`;
} if (force || detailPlayers.textContent !== newPlayersText) detailPlayers.textContent = newPlayersText;
let pingVal = d.ping || d.latency || '-';
let pingVal = d.ping || d.latency || '-'; if (pingVal !== '-' && typeof pingVal === 'number') pingVal = pingVal + ' ms';
if (pingVal !== '-' && typeof pingVal === 'number') { if (force || (pingVal !== '-' && detailPing.textContent !== pingVal)) detailPing.textContent = pingVal;
pingVal = pingVal + ' ms';
} const currentPlayers = Array.isArray(d.players) ? d.players : [];
const prevPlayers = (prevSt && Array.isArray(prevSt.data?.players)) ? prevSt.data.players : [];
if (force || (pingVal !== '-' && detailPing.textContent !== pingVal)) { let playersChanged = force || JSON.stringify(currentPlayers) !== JSON.stringify(prevPlayers);
detailPing.textContent = pingVal; if (playersChanged) updatePlayerList(currentPlayers);
} }
previousStatuses[srv.id] = st ? JSON.parse(JSON.stringify(st)) : null;
const currentPlayers = Array.isArray(d.players) ? d.players : []; }
const prevPlayers = (prevSt && Array.isArray(prevSt.data?.players)) ? prevSt.data.players : [];
// --- Spielerliste mit Prefix-Hover und Farbsupport ---
let playersChanged = false; function updatePlayerList(players) {
if (force) { detailPlayerList.innerHTML = '';
playersChanged = true; if (!players || players.length === 0) {
} else { detailPlayerList.innerHTML = '<li class="placeholder">Keine Spieler online.</li>';
playersChanged = JSON.stringify(currentPlayers) !== JSON.stringify(prevPlayers); return;
} }
if (playersChanged) { for (const p of players) {
updatePlayerList(currentPlayers); const li = document.createElement('li');
} li.className = 'player-item'; // CSS Klasse für den Hover-Container
}
let name = '';
previousStatuses[srv.id] = st ? JSON.parse(JSON.stringify(st)) : null; let uuid = null;
} let prefix = '';
// --- Spielerliste --- if (typeof p === 'object') {
function updatePlayerList(players) { name = p.name || p.username || p.player || '';
detailPlayerList.innerHTML = ''; uuid = p.uuid || null;
if (!players || players.length === 0) { prefix = p.prefix || '';
detailPlayerList.innerHTML = '<li class="placeholder">Keine Spieler online.</li>'; } else {
return; name = String(p);
} }
for (const p of players) {
const li = document.createElement('li'); const img = document.createElement('img');
const name = typeof p === 'object' ? p.name || p.username || p.player || '' : String(p); img.src = (typeof p === 'object' && p.avatar) ? p.avatar : get3DAvatarUrl(name, uuid);
img.className = 'player-avatar';
if (typeof p === 'object' && p.avatar) { img.loading = 'lazy';
const img = document.createElement('img'); img.onerror = function() {
img.src = p.avatar; this.onerror = null;
img.className = 'player-avatar'; this.src = `https://mc-heads.net/avatar/${encodeURIComponent(name)}/64`;
img.title = name; };
li.appendChild(img);
} else { // Das Hover-Element mit farbigem Prefix + Name
// Falls kein Avatar vorhanden, generiere einen von mc-heads.net const info = document.createElement('div');
const img = document.createElement('img'); info.className = 'player-hover-info';
img.src = `https://mc-heads.net/avatar/${encodeURIComponent(name)}/32`; info.innerHTML = `${parseMinecraftColors(prefix)} ${name}`.trim();
img.className = 'player-avatar';
img.title = name; li.appendChild(img);
li.appendChild(img); li.appendChild(info);
} detailPlayerList.appendChild(li);
}
detailPlayerList.appendChild(li); }
}
} function updateServerListStatuses() {
const items = serversContainer.querySelectorAll('.server-item');
// --- Update Server List Statuses --- items.forEach(item => {
function updateServerListStatuses() { const s = servers.find(x => x.id === item.dataset.id);
const items = serversContainer.querySelectorAll('.server-item'); if (!s) return;
items.forEach(item => { const st = statuses[s.id];
const s = servers.find(x => x.id === item.dataset.id); if (!st || !st.ok || !st.data) {
if (!s) return; item.statusBubble.textContent = 'Offline';
item.statusBubble.style.backgroundColor = 'var(--offline)';
const st = statuses[s.id]; } else if (st.data.online) {
const prevSt = previousStatuses[s.id]; item.statusBubble.textContent = 'Online';
item.statusBubble.style.backgroundColor = 'var(--online)';
const statusChanged = !prevSt || } else {
(prevSt.ok !== st?.ok) || item.statusBubble.textContent = 'Offline';
(prevSt.data?.online !== st?.data?.online); item.statusBubble.style.backgroundColor = 'var(--offline)';
}
if (!statusChanged) return; });
}
if (!st || !st.ok || !st.data) {
item.statusBubble.textContent = 'Offline'; async function handleAdd() {
item.statusBubble.style.backgroundColor = 'var(--offline)'; const name = inputName.value.trim();
} else if (st.data.online) { const url = inputUrl.value.trim();
item.statusBubble.textContent = 'Online'; const wpSite = inputWpSite.value.trim();
item.statusBubble.style.backgroundColor = 'var(--online)'; const wpServerId = inputWpServerId.value.trim();
} else { if (!url && !wpSite) return;
item.statusBubble.textContent = 'Offline'; const s = { id: uid(), name: name || url || wpSite, url: url || null, wpSite: wpSite || null, wpServerId: wpServerId || null };
item.statusBubble.style.backgroundColor = 'var(--offline)'; servers.push(s);
} await saveServersToStorage();
inputName.value = inputUrl.value = inputWpSite.value = inputWpServerId.value = '';
previousStatuses[s.id] = st ? JSON.parse(JSON.stringify(st)) : null; renderServerList();
}); }
}
async function handleEdit() {
// --- Add / Edit / Delete --- if (!selectedId) return;
async function handleAdd() { const srv = servers.find(s => s.id === selectedId);
const name = inputName.value.trim(); if (!srv) return;
const url = inputUrl.value.trim(); const newName = prompt('Name:', srv.name) || srv.name;
const wpSite = inputWpSite.value.trim(); srv.name = newName.trim();
const wpServerId = inputWpServerId.value.trim(); await saveServersToStorage();
renderServerList();
if (!url && !wpSite) { renderDetail(selectedId);
alert('Bitte URL oder WP Site angeben'); }
return;
} async function handleDelete() {
if (!selectedId || !confirm('Server wirklich löschen?')) return;
const s = { servers = servers.filter(s => s.id !== selectedId);
id: uid(), selectedId = null;
name: name || url || wpSite, await saveServersToStorage();
url: url || null, renderServerList();
wpSite: wpSite || null, renderDetail(null);
wpServerId: wpServerId || null }
};
async function manualRefresh() {
servers.push(s); try { chrome.runtime.sendMessage({ cmd: 'refreshNow' }); } catch(e) {}
await saveServersToStorage(); }
inputName.value = ''; chrome.storage.onChanged.addListener((changes, area) => {
inputUrl.value = ''; if (area === 'local' && changes.serverStatuses) {
inputWpSite.value = ''; statuses = changes.serverStatuses.newValue || {};
inputWpServerId.value = ''; updateServerListStatuses();
if (selectedId) {
renderServerList(); const srv = servers.find(s => s.id === selectedId);
} if (srv) updateDetailForServer(srv);
}
async function handleEdit() { }
if (!selectedId) return;
const srv = servers.find(s => s.id === selectedId);
if (!srv) return;
const newName = prompt('Name:', srv.name) || srv.name;
const newUrl = prompt('URL:', srv.url || '') || srv.url;
const newWpSite = prompt('WP Site:', srv.wpSite || '') || srv.wpSite;
const newWpServerId = prompt('WP Server ID:', srv.wpServerId || '') || srv.wpServerId;
srv.name = newName.trim();
srv.url = newUrl.trim();
srv.wpSite = newWpSite.trim();
srv.wpServerId = newWpServerId.trim();
await saveServersToStorage();
renderServerList();
renderDetail(selectedId);
}
async function handleDelete() {
if (!selectedId) return;
if (!confirm('Server wirklich löschen?')) return;
servers = servers.filter(s => s.id !== selectedId);
selectedId = null;
await saveServersToStorage();
renderServerList();
renderDetail(null);
}
async function manualRefresh() {
try {
chrome.runtime.sendMessage({ cmd: 'refreshNow' });
} catch(e) {
console.error('Refresh fehlgeschlagen:', e);
}
}
// --- Storage Listener ---
chrome.storage.onChanged.addListener((changes, area) => {
if (area === 'local' && changes.serverStatuses) {
statuses = changes.serverStatuses.newValue || {};
updateServerListStatuses();
if (selectedId) {
const srv = servers.find(s => s.id === selectedId);
if (srv) updateDetailForServer(srv);
}
}
}); });

View File

@@ -3,7 +3,7 @@
* Plugin Name: Minecraft BungeeCord Status Network Edition * Plugin Name: Minecraft BungeeCord Status Network Edition
* Description: Der ultimative Live-Status für dein BungeeCord Netzwerk (Border None Fix). * Description: Der ultimative Live-Status für dein BungeeCord Netzwerk (Border None Fix).
* Tags: minecraft, bungeecord, server status, player list * Tags: minecraft, bungeecord, server status, player list
* Version: 3.6.1 * Version: 3.6.4
* Author: M_Viper * Author: M_Viper
* Requires at least: 6.0 * Requires at least: 6.0
* Requires PHP: 7.4 * Requires PHP: 7.4
@@ -59,6 +59,23 @@ function mcss_format_minecraft_colors($text) {
return $formatted; return $formatted;
} }
/* ---------------- HELPER: 3D AVATAR URL ---------------- */
function mcss_get_3d_avatar($name, $uuid = null) {
// Bedrock-Spieler erkennen (Name enthält Punkt oder UUID beginnt mit xuid)
$is_bedrock = strpos($name, '.') !== false || ($uuid && strpos($uuid, 'xuid') === 0);
if ($is_bedrock) {
// Bedrock: mc-heads.net mit 3D Head (nutze UUID falls vorhanden)
if ($uuid && !empty($uuid)) {
return 'https://mc-heads.net/head/' . rawurlencode($uuid) . '/100';
}
return 'https://mc-heads.net/head/' . rawurlencode($name) . '/100';
} else {
// Java: mc-heads.net mit 3D Head
return 'https://mc-heads.net/head/' . rawurlencode($name) . '/100';
}
}
/* ---------------- AUTO UPDATE (MCSS) ---------------- */ /* ---------------- AUTO UPDATE (MCSS) ---------------- */
if ( ! class_exists( 'MCSS_Auto_Update' ) ) { if ( ! class_exists( 'MCSS_Auto_Update' ) ) {
class MCSS_Auto_Update { class MCSS_Auto_Update {
@@ -547,10 +564,12 @@ function mcss_fetch_server_with_ranks($srv) {
if (is_array($player_data)) { if (is_array($player_data)) {
$name = $player_data['name']; $name = $player_data['name'];
$prefix = $player_data['prefix'] ?? ''; $prefix = $player_data['prefix'] ?? '';
$uuid = $player_data['uuid'] ?? null;
} else { } else {
// Fallback für alte API // Fallback für alte API
$name = $player_data; $name = $player_data;
$prefix = ''; $prefix = '';
$uuid = null;
} }
// 1. Prefix mit Farben konvertieren // 1. Prefix mit Farben konvertieren
@@ -566,9 +585,13 @@ function mcss_fetch_server_with_ranks($srv) {
$display_html = $name_html; $display_html = $name_html;
} }
// 4. 3D Avatar URL generieren (mit Bedrock-Support)
$avatar_url = mcss_get_3d_avatar($name, $uuid);
$players_info[] = [ $players_info[] = [
'name' => $name, 'name' => $name,
'avatar' => "https://mc-heads.net/avatar/" . rawurlencode($name) . "/64", 'uuid' => $uuid,
'avatar' => $avatar_url,
'prefix' => $prefix, 'prefix' => $prefix,
'display_html' => $display_html, 'display_html' => $display_html,
'rank' => $prefix ?: 'Spieler', 'rank' => $prefix ?: 'Spieler',
@@ -670,7 +693,7 @@ function mcss_shortcode($atts) {
$widget_width = "650px"; $widget_width = "650px";
$widget_padding = "15px"; $widget_padding = "15px";
$logo_size = "70px"; $logo_size = "70px";
$player_head_size = "32px"; $player_head_size = "40px"; // Größer für 3D-Köpfe
$copy_addr = !empty($srv['copy_address']) ? $srv['copy_address'] : $srv['host']; $copy_addr = !empty($srv['copy_address']) ? $srv['copy_address'] : $srv['host'];
if (empty($srv['hide_port']) && !empty($srv['player_port_copy'])) { if (empty($srv['hide_port']) && !empty($srv['player_port_copy'])) {
@@ -756,13 +779,13 @@ function mcss_shortcode($atts) {
<!-- PLAYER GRID --> <!-- PLAYER GRID -->
<span style="color:#666;margin-bottom:15px;">Spieler:</span> <span style="color:#666;margin-bottom:15px;">Spieler:</span>
<div id="mcss-player-grid-<?php echo esc_attr($uid); ?>" style="display:flex;flex-wrap:wrap;gap:8px;margin-top:8px;color:#666;justify-content:center;"> <div id="mcss-player-grid-<?php echo esc_attr($uid); ?>" style="display:flex;flex-wrap:wrap;gap:12px;margin-top:8px;color:#666;justify-content:center;">
<?php if (!empty($data['players']) && is_array($data['players'])): ?> <?php if (!empty($data['players']) && is_array($data['players'])): ?>
<?php foreach ($data['players'] as $p): ?> <?php foreach ($data['players'] as $p): ?>
<div style="text-align:center;"> <div style="text-align:center;">
<img src="<?php echo esc_url($p['avatar']); ?>" style="width:<?php echo $player_head_size; ?>;height:<?php echo $player_head_size; ?>;border-radius:4px;"> <img src="<?php echo esc_url($p['avatar']); ?>" style="width:<?php echo $player_head_size; ?>;height:<?php echo $player_head_size; ?>;border-radius:6px;box-shadow:0 2px 6px rgba(0,0,0,0.15);" loading="lazy">
<div style="font-size:0.75em;"><?php echo $p['display_html']; ?></div> <div style="font-size:0.75em;margin-top:4px;"><?php echo $p['display_html']; ?></div>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
<?php else: ?> <?php else: ?>
@@ -847,8 +870,8 @@ function mcss_shortcode($atts) {
d.players.forEach(function(p){ d.players.forEach(function(p){
var content = p.display_html ? p.display_html : p.name; var content = p.display_html ? p.display_html : p.name;
html += '<div style="text-align:center;">' + html += '<div style="text-align:center;">' +
'<img src="' + (p.avatar ? p.avatar : '') + '" style="width:<?php echo $player_head_size; ?>;height:<?php echo $player_head_size; ?>;border-radius:4px;">' + '<img src="' + (p.avatar ? p.avatar : '') + '" style="width:<?php echo $player_head_size; ?>;height:<?php echo $player_head_size; ?>;border-radius:6px;box-shadow:0 2px 6px rgba(0,0,0,0.15);" loading="lazy">' +
'<div style="font-size:0.75em;">' + content + '</div>' + '<div style="font-size:0.75em;margin-top:4px;">' + content + '</div>' +
'</div>'; '</div>';
}); });
} else { } else {
@@ -874,4 +897,185 @@ function mcss_shortcode($atts) {
<?php return ob_get_clean(); <?php return ob_get_clean();
} }
/* ---------------- SIDEBAR WIDGET: MODERN PILL BADGE (ROBUST INLINE STYLES) ---------------- */
class MCSS_Sidebar_Status_Widget extends WP_Widget {
public function __construct() {
parent::__construct(
'mcss_sidebar_status', // Basis ID
'MC Server Status (Simple)', // Name
array( 'description' => 'Zeigt einen modernen Online/Offline Status in der Sidebar an.' ) // Args
);
}
// Backend: Formular im Widget-Bereich
public function form( $instance ) {
$title = ! empty( $instance['title'] ) ? $instance['title'] : 'Server Status';
$server_id = ! empty( $instance['server_id'] ) ? $instance['server_id'] : '';
// Server-Liste holen
$servers = get_option('mcss_servers', []);
?>
<p>
<label for="<?php echo $this->get_field_id( 'title' ); ?>">Titel:</label>
<input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>">
</p>
<p>
<label for="<?php echo $this->get_field_id( 'server_id' ); ?>">Server wählen:</label>
<select class="widefat" id="<?php echo $this->get_field_id( 'server_id' ); ?>" name="<?php echo $this->get_field_name( 'server_id' ); ?>">
<option value="">-- Bitte wählen --</option>
<?php foreach ($servers as $srv): ?>
<option value="<?php echo esc_attr($srv['id'] ?? ''); ?>" <?php selected($server_id, $srv['id'] ?? ''); ?>>
<?php echo esc_html($srv['name'] ?? 'Unbenannt'); ?>
</option>
<?php endforeach; ?>
</select>
</p>
<?php
}
// Backend: Speichern
public function update( $new_instance, $old_instance ) {
$instance = array();
$instance['title'] = ( ! empty( $new_instance['title'] ) ) ? sanitize_text_field( $new_instance['title'] ) : '';
$instance['server_id'] = ( ! empty( $new_instance['server_id'] ) ) ? sanitize_text_field( $new_instance['server_id'] ) : '';
return $instance;
}
// Frontend: Ausgabe
public function widget( $args, $instance ) {
echo $args['before_widget'];
if ( ! empty( $instance['title'] ) ) {
echo $args['before_title'] . apply_filters( 'widget_title', $instance['title'] ) . $args['after_title'];
}
$server_id = $instance['server_id'];
$servers = get_option('mcss_servers', []);
$target_srv = null;
// Server suchen
foreach ($servers as $srv) {
if (($srv['id'] ?? '') === $server_id) { $target_srv = $srv; break; }
}
if ($target_srv) {
// Initiale Daten holen
$data = mcss_fetch_server_with_ranks($target_srv);
$is_online = $data['online'] ?? false;
$uid = md5($target_srv['host'] . 'widget'); // Einzigartige ID für JS
// Farben definieren
$bg_online = '#ecfdf5';
$bg_offline = '#fef2f2';
$text_online = '#059669';
$text_offline = '#dc2626';
$dot_online = '#10b981';
$dot_offline = '#ef4444';
$border_online = 'rgba(16, 185, 129, 0.2)';
$border_offline = 'rgba(239, 68, 68, 0.2)';
$anim_name_online = 'mcss-pulse-modern';
$anim_name_offline = 'mcss-pulse-modern-red';
// Werte für den Initialzustand setzen
$current_bg = $is_online ? $bg_online : $bg_offline;
$current_text = $is_online ? $text_online : $text_offline;
$current_dot = $is_online ? $dot_online : $dot_offline;
$current_border = $is_online ? $border_online : $border_offline;
$current_anim = $is_online ? $anim_name_online : $anim_name_offline;
$status_label = $is_online ? 'Online' : 'Offline';
// Inline Animation Definitionen für Robustheit
?>
<style>
@keyframes mcss-pulse-modern {
0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4); }
70% { box-shadow: 0 0 0 6px rgba(16, 185, 129, 0); }
100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
}
@keyframes mcss-pulse-modern-red {
0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); }
70% { box-shadow: 0 0 0 6px rgba(239, 68, 68, 0); }
100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); }
}
</style>
<!-- Wrapper mit PUREN Inline Styles -->
<div id="mcss-sw-wrapper-<?php echo esc_attr($uid); ?>"
style="display: flex; align-items: center; justify-content: center; width: 93%; padding: 8px; border-radius: 50px; background-color: <?php echo $current_bg; ?>; border: 1px solid <?php echo $current_border; ?>; transition: all 0.3s ease; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; cursor: default; margin: 10px 0;">
<span id="mcss-sw-dot-<?php echo esc_attr($uid); ?>"
style="width: 8px; height: 8px; border-radius: 50%; margin-right: 10px; background-color: <?php echo $current_dot; ?>; animation: <?php echo $current_anim; ?> 2s infinite;"></span>
<span id="mcss-sw-text-<?php echo esc_attr($uid); ?>"
style="font-size: 12px; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; color: <?php echo $current_text; ?>;">
<?php echo $status_label; ?>
</span>
</div>
<!-- Kleines Skript für Live-Update (alle 3 Sekunden) -->
<script>
(function(){
var uid = "<?php echo esc_js($uid); ?>";
var serverId = "<?php echo esc_js($target_srv['id']); ?>";
// Konstanten Farben
var cOnlineBg = '<?php echo esc_js($bg_online); ?>';
var cOfflineBg = '<?php echo esc_js($bg_offline); ?>';
var cOnlineText = '<?php echo esc_js($text_online); ?>';
var cOfflineText = '<?php echo esc_js($text_offline); ?>';
var cOnlineDot = '<?php echo esc_js($dot_online); ?>';
var cOfflineDot = '<?php echo esc_js($dot_offline); ?>';
var bOnline = '<?php echo esc_js($border_online); ?>';
var bOffline = '<?php echo esc_js($border_offline); ?>';
var aOnline = '<?php echo esc_js($anim_name_online); ?>';
var aOffline = '<?php echo esc_js($anim_name_offline); ?>';
setInterval(function(){
fetch('<?php echo admin_url('admin-ajax.php'); ?>', {
method: 'POST',
headers: {'Content-Type':'application/x-www-form-urlencoded'},
body: 'action=mcss_fetch&server_id=' + serverId
})
.then(r => r.json())
.then(d => {
var wrapper = document.getElementById('mcss-sw-wrapper-' + uid);
var dot = document.getElementById('mcss-sw-dot-' + uid);
var txt = document.getElementById('mcss-sw-text-' + uid);
if(wrapper && dot && txt && d) {
if(d.online) {
wrapper.style.backgroundColor = cOnlineBg;
wrapper.style.borderColor = bOnline;
dot.style.backgroundColor = cOnlineDot;
dot.style.animationName = aOnline;
txt.style.color = cOnlineText;
txt.textContent = 'Online';
} else {
wrapper.style.backgroundColor = cOfflineBg;
wrapper.style.borderColor = bOffline;
dot.style.backgroundColor = cOfflineDot;
dot.style.animationName = aOffline;
txt.style.color = cOfflineText;
txt.textContent = 'Offline';
}
}
});
}, 3000);
})();
</script>
<?php
} else {
echo '<p>Bitte Server in den Einstellungen wählen.</p>';
}
echo $args['after_widget'];
}
}
// Widget registrieren
add_action( 'widgets_init', function(){
register_widget( 'MCSS_Sidebar_Status_Widget' );
});

View File

@@ -2,6 +2,10 @@
StatusAPI zeigt den aktuellen Status deines Minecraft-Servers direkt auf deiner Webseite an inklusive Online/Offline, Version, Ping, Spieleranzahl und Spieler-Avatare. StatusAPI zeigt den aktuellen Status deines Minecraft-Servers direkt auf deiner Webseite an inklusive Online/Offline, Version, Ping, Spieleranzahl und Spieler-Avatare.
**StatusAPI Repository:** [https://git.viper.ipv64.net/M_Viper/StatusAPI](https://git.viper.ipv64.net/M_Viper/StatusAPI)
---
## Features ## Features
- Zeigt Serverstatus: **Online / Offline** - Zeigt Serverstatus: **Online / Offline**
@@ -12,26 +16,67 @@ StatusAPI zeigt den aktuellen Status deines Minecraft-Servers direkt auf deiner
- Hinweise/Banner können vom Nutzer geschlossen werden - Hinweise/Banner können vom Nutzer geschlossen werden
- Anpassbare Darstellung für verschiedene Layouts - Anpassbare Darstellung für verschiedene Layouts
---
## Wichtige Hinweise ## Wichtige Hinweise
### BungeeCord Plugin ### BungeeCord Plugin (Voraussetzung)
⚠️ **Die [StatusAPI](https://git.viper.ipv64.net/M_Viper/StatusAPI) MUSS auf deinem BungeeCord-Server installiert sein!**
Das Plugin **StatusAPI.jar** muss in den BungeeCord Plugin-Ordner kopiert werden, damit die API korrekt funktioniert. Das Plugin **StatusAPI.jar** muss in den BungeeCord Plugin-Ordner kopiert werden, damit die API korrekt funktioniert.
```text
BungeeCord/
├─ plugins/
│ └─ StatusAPI.jar ← PFLICHT
└─ config.yml
```
**Download:** [https://git.viper.ipv64.net/M_Viper/StatusAPI](https://git.viper.ipv64.net/M_Viper/StatusAPI)
### WordPress Integration ### WordPress Integration
In WordPress muss der API-Port auf **9191** eingestellt sein, damit die Serverdaten korrekt abgerufen werden. In WordPress muss der API-Port auf **9191** eingestellt sein, damit die Serverdaten korrekt abgerufen werden.
Stelle sicher, dass dein Server und die Webseite auf diesen Port zugreifen können. Stelle sicher, dass dein Server und die Webseite auf diesen Port zugreifen können.
Die StatusAPI stellt einen HTTP-Endpunkt bereit (Standard: `http://localhost:9191`), den das WordPress-Plugin abfragt.
### Shortcode für WordPress ### Shortcode für WordPress
Um den Serverstatus auf deiner WordPress-Seite anzuzeigen, füge einfach folgenden Shortcode ein: Um den Serverstatus auf deiner WordPress-Seite anzuzeigen, füge einfach folgenden Shortcode ein:
```html ```html
[bungeecord_status id="Bungeecord"] [bungeecord_status id="Bungeecord"]
``` ```
Ersetze `"Bungeecord"` durch die ID deines Servers, falls mehrere Server eingebunden werden. Ersetze `"Bungeecord"` durch die ID deines Servers, falls mehrere Server eingebunden werden.
---
## Installation
### Schritt 1: StatusAPI auf BungeeCord installieren
1. Lade die **[StatusAPI](https://git.viper.ipv64.net/M_Viper/StatusAPI)** herunter
2. Kopiere die **StatusAPI.jar** in den `plugins`-Ordner deines BungeeCord-Servers
3. Starte den BungeeCord-Server neu
4. Die API läuft nun standardmäßig auf Port **9191**
### Schritt 2: WordPress-Plugin einrichten
1. Installiere das WordPress-Plugin für den BungeeCord-Status
2. Gehe zu den Plugin-Einstellungen
3. Trage die **StatusAPI URL** ein (z.B. `http://localhost:9191`)
4. Speichere die Einstellungen
### Schritt 3: Shortcode einbinden
Füge den Shortcode auf einer beliebigen WordPress-Seite ein:
```html
[bungeecord_status id="Bungeecord"]
```
---
## Beispielanzeige ## Beispielanzeige
**Status:** Online **Status:** Online
@@ -42,17 +87,52 @@ Ersetze `"Bungeecord"` durch die ID deines Servers, falls mehrere Server eingebu
**Avatare:** **Avatare:**
![M_Viper Avatar](https://example.com/avatar/M_Viper.png) <!-- Platzhalter wird im Plugin dynamisch geladen --> ![M_Viper Avatar](https://example.com/avatar/M_Viper.png) <!-- Platzhalter wird im Plugin dynamisch geladen -->
> Der Abstand zwischen dem Text „Spieler: und den Avataren ist bewusst etwas größer, um die Übersichtlichkeit zu erhöhen. > Der Abstand zwischen dem Text „Spieler:" und den Avataren ist bewusst etwas größer, um die Übersichtlichkeit zu erhöhen.
---
## Nutzung ## Nutzung
1. Shortcode an der gewünschten Stelle einfügen 1. Shortcode an der gewünschten Stelle einfügen
2. Der Serverstatus aktualisiert sich automatisch, sodass die angezeigten Spieler und der Ping immer aktuell sind 2. Der Serverstatus aktualisiert sich automatisch, sodass die angezeigten Spieler und der Ping immer aktuell sind
3. Alle Daten werden live von der StatusAPI bezogen
---
## Technische Details
- **API-Port:** 9191 (Standard)
- **Update-Intervall:** 2 Sekunden (automatisch)
- **Protokoll:** HTTP/JSON
- **Datenquelle:** StatusAPI auf BungeeCord
---
## Support ## Support
Bei Problemen überprüfe bitte: Bei Problemen überprüfe bitte:
- Ob das Plugin **StatusAPI.jar** korrekt im BungeeCord Plugin-Ordner liegt - Ob das Plugin **[StatusAPI.jar](https://git.viper.ipv64.net/M_Viper/StatusAPI)** korrekt im BungeeCord Plugin-Ordner liegt
- Ob der API-Port in WordPress korrekt auf **9191** eingestellt ist - Ob der API-Port in WordPress korrekt auf **9191** eingestellt ist
- Ob der Server erreichbar ist und Spieler online sind - Ob der BungeeCord-Server läuft und die StatusAPI erreichbar ist
- Ob keine Firewall den Port 9191 blockiert
- Ob die StatusAPI gültige JSON-Daten zurückliefert
### Häufige Fehler
**"Server offline" obwohl Server läuft**
- StatusAPI ist nicht installiert oder läuft nicht
- Falscher Port in WordPress eingestellt
- Firewall blockiert Port 9191
**Keine Spieler werden angezeigt**
- StatusAPI liefert keine Spielerdaten
- API-Verbindung fehlgeschlagen
- WordPress kann die API-URL nicht erreichen
---
## Weitere Informationen
Für detaillierte Informationen zur Installation, Konfiguration und Fehlerbehebung der StatusAPI:
**[https://git.viper.ipv64.net/M_Viper/StatusAPI](https://git.viper.ipv64.net/M_Viper/StatusAPI)**