28 Commits
Author SHA1 Message Date
Git Manager GUI 5eb462e5a7 Upload via Git Manager GUI 2026-07-06 07:40:39 +02:00
Git Manager GUI e5dda9531f Upload via Git Manager GUI 2026-07-06 07:40:37 +02:00
Git Manager GUI b851cbc20b Upload via Git Manager GUI 2026-05-22 11:16:17 +02:00
Git Manager GUI de04e58242 Upload via Git Manager GUI 2026-05-22 11:16:15 +02:00
Git Manager GUI 166bfad1d4 Upload via Git Manager GUI 2026-05-22 11:16:13 +02:00
Git Manager GUI 1de9e4b155 Upload via Git Manager GUI 2026-05-22 11:16:12 +02:00
M_Viper 501be8649a Delete BungeeCord-Chrome/BungeeCord-Chrome.zip via Git Manager GUI 2026-05-22 09:16:02 +00:00
M_Viper 3c80758b12 Minecraft-BungeeCord-Status/minecraft-bungeecord-status.php aktualisiert 2026-04-02 19:38:17 +00:00
Git Manager GUI f7e0dcb23d Upload via Git Manager GUI 2026-04-02 21:13:38 +02:00
Git Manager GUI 65ea93e22d Upload via Git Manager GUI 2026-04-02 20:57:27 +02:00
M_Viper fde1947ae7 README.md aktualisiert 2026-03-01 11:21:36 +00:00
M_Viper 7556521a0e Minecraft-BungeeCord-Status/minecraft-bungeecord-status.php aktualisiert 2026-02-11 19:13:59 +00:00
M_Viper 40c0ed7b0c Minecraft-BungeeCord-Status/minecraft-bungeecord-status.php aktualisiert 2026-02-10 22:31:09 +00:00
M_Viper f407604ff6 Upload manifest.json via GUI 2026-02-09 21:06:02 +00:00
M_Viper 2fce65544b Upload file BungeeCord-Chrome.zip via GUI 2026-02-09 22:06:01 +01:00
M_Viper 1c89b4fdfe Upload background.js via GUI 2026-02-09 21:05:53 +00:00
M_Viper 700e6026b6 Upload popup.js via GUI 2026-02-09 21:05:51 +00:00
M_Viper b4ee8b0564 Upload popup.html via GUI 2026-02-09 21:05:50 +00:00
M_Viper 111158967f Upload popup.css via GUI 2026-02-09 21:05:50 +00:00
M_Viper f7e99f8625 Minecraft-BungeeCord-Status/minecraft-bungeecord-status.php aktualisiert 2026-02-09 20:08:51 +00:00
M_Viper e03ebffb64 README.md aktualisiert 2026-01-08 15:25:50 +00:00
M_Viper 056a98b6a9 StatusAPI/pom.xml aktualisiert 2026-01-03 11:32:09 +00:00
M_Viper 045eb29b18 StatusAPI/src/main/resources/plugin.yml aktualisiert 2026-01-03 11:31:57 +00:00
M_Viper 059a0d23f6 Dateien nach "StatusAPI/src/main/java/net/viper/status" hochladen 2026-01-03 11:31:32 +00:00
M_Viper b2e1338597 StatusAPI/src/main/java/net/viper/status/UpdateListener.java gelöscht 2026-01-03 11:31:03 +00:00
M_Viper 0ec7710840 StatusAPI/src/main/java/net/viper/status/UpdateChecker.java aktualisiert 2026-01-03 11:30:53 +00:00
M_Viper 3a7534c4eb StatusAPI/src/main/java/net/viper/status/StatusAPI.java aktualisiert 2026-01-03 11:30:38 +00:00
M_Viper 3dc4ad4bb4 Minecraft-BungeeCord-Status/minecraft-bungeecord-status.php aktualisiert 2026-01-03 08:49:02 +00:00
13 changed files with 1927 additions and 1306 deletions
+186 -186
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
}; };
} }
+1 -1
View File
@@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "Minecraft Bungee Status", "name": "Minecraft Bungee Status",
"description": "Zeigt den Live-Status mehrerer Bungee/Proxy-Server an. Fallback auf WordPress AJAX möglich.", "description": "Zeigt den Live-Status mehrerer Bungee/Proxy-Server an. Fallback auf WordPress AJAX möglich.",
"version": "1.1.0", "version": "1.1.1",
"action": { "action": {
"default_popup": "popup.html", "default_popup": "popup.html",
"default_icon": { "default_icon": {
+435 -220
View File
@@ -1,220 +1,435 @@
:root { :root {
--bg: #111827; --bg: #0d1117;
--panel: #0b1220; --panel: #161b27;
--text: #e6eef8; --panel2: #111620;
--muted: #9aa7b3; --text: #e6eef8;
--accent: #3b82f6; --muted: #5a6a7a;
--danger: #ef4444; --muted2: #8899aa;
--radius: 8px; --accent: #3b82f6;
--online: #22c55e; --radius: 9px;
--offline: #ef4444; --online: #22c55e;
} --offline: #ef4444;
--border: rgba(255,255,255,0.07);
* { 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; } * { box-sizing: border-box; }
header { display:flex; align-items:center; justify-content:space-between; margin-bottom:8px; } html, body {
header h1 { font-size:16px; margin:0; } margin: 0; padding: 0;
header .actions button { background:transparent; border:1px solid var(--muted); color:var(--text); padding:4px 8px; border-radius:6px; cursor:pointer; } font-family: Inter, Arial, sans-serif;
background: var(--bg);
.main { color: var(--text);
display:flex; overflow-x: hidden;
gap:12px; }
position: relative;
} .root {
width: 560px;
.server-list { display: flex;
width:45%; flex-direction: column;
background:var(--panel); }
padding:8px;
border-radius:var(--radius); /* ── Header ── */
flex-shrink: 0; header {
} background: var(--panel);
display: flex;
.detail { align-items: center;
flex:1; justify-content: space-between;
background:var(--panel); padding: 13px 20px;
padding:8px; border-bottom: 1px solid var(--border);
border-radius:var(--radius); }
min-height:220px;
transition: all 0.3s ease; header h1 {
} font-size: 14px;
font-weight: 700;
.detail.full-width { margin: 0;
width: 100%; letter-spacing: 0.03em;
flex: 1 1 100%; color: #fff;
} }
.add-form { display:flex; flex-direction:column; gap:6px; margin-bottom:8px; } header .actions { display: flex; gap: 7px; }
.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; } header .actions button {
background: rgba(255,255,255,0.07);
#serversContainer { list-style:none; margin:0; padding:0; max-height:260px; overflow:auto; } border: 1px solid rgba(255,255,255,0.1);
.server-item { color: var(--muted2);
display:flex; border-radius: 7px;
align-items:center; width: 32px; height: 32px;
justify-content:space-between; cursor: pointer;
padding:6px; font-size: 16px;
margin-bottom:6px; display: flex; align-items: center; justify-content: center;
border-radius:6px; transition: background 0.15s, color 0.15s;
cursor:pointer; }
background: rgba(255,255,255,0.02);
transition: background 0.2s ease; header .actions button:hover {
} background: rgba(255,255,255,0.12);
color: #fff;
.server-item:hover { background: rgba(255,255,255,0.05); } }
.server-item .meta { display:flex; flex-direction:column; } /* ── Main layout ── */
.server-item .meta .name { font-weight:600; } .main {
.server-item .meta .url { font-size:12px; color:var(--muted); } display: flex;
.server-item .status-bubble { flex-direction: column;
font-size:12px; }
padding:3px 6px;
border-radius:999px; /* ── Settings / add-form panel ── */
color:#fff; .settings-panel {
transition: background-color 0.3s ease; background: var(--panel2);
} border-bottom: 1px solid var(--border);
padding: 14px 20px;
.placeholder { color:var(--muted); padding:12px; } display: flex;
.hidden { display:none; } flex-direction: column;
gap: 8px;
/* --- Detail Header (Zeile 1) --- */ }
.detail-header {
display: flex; .settings-panel input {
justify-content: space-between; padding: 8px 12px;
align-items: center; border-radius: 7px;
margin-bottom: 8px; border: 1px solid rgba(255,255,255,0.09);
} background: rgba(0,0,0,0.3);
color: var(--text);
.server-identity h2 { font-size: 13px;
margin: 0; outline: none;
font-size: 18px; }
line-height: 1.2;
} .settings-panel input:focus {
border-color: var(--accent);
.server-url { }
display: block;
font-size: 12px; .settings-panel button {
color: var(--muted); padding: 8px 12px;
margin-top: 2px; border-radius: 7px;
} border: none;
background: var(--accent);
.status-wrapper { color: white;
display: flex; font-size: 13px;
align-items: center; font-weight: 600;
gap: 8px; cursor: pointer;
font-weight: 600; }
}
/* ── Server list ── */
/* --- Pulsierender Punkt Animation --- */ .server-list-panel {
.pulsing-dot { background: var(--panel2);
width: 10px; border-bottom: 1px solid var(--border);
height: 10px; padding: 10px 12px;
border-radius: 50%; }
background-color: var(--offline); /* Standard Offline Rot */
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); #serversContainer {
animation: pulse-red 2s infinite; list-style: none;
} margin: 0; padding: 0;
display: flex;
.pulsing-dot.online { flex-direction: column;
background-color: var(--online); /* Online Grün */ gap: 4px;
animation: pulse-green 2s infinite; }
}
.server-item {
@keyframes pulse-green { display: flex;
0% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.7); } align-items: center;
70% { box-shadow: 0 0 0 10px rgba(34, 197, 94, 0); } justify-content: space-between;
100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); } padding: 9px 12px;
} border-radius: 8px;
cursor: pointer;
@keyframes pulse-red { background: transparent;
0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); } border: 1px solid transparent;
70% { box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); } transition: background 0.15s, border-color 0.15s;
100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); } }
}
.server-item:hover { background: rgba(255,255,255,0.04); }
/* --- Detail Stats (Zeile 2) --- */ .server-item.selected { background: rgba(59,130,246,0.08); border-color: rgba(59,130,246,0.25); }
.detail-stats {
display: flex; .server-item .meta { display: flex; flex-direction: column; gap: 2px; }
justify-content: space-between; .server-item .meta .name { font-size: 13px; font-weight: 600; color: #fff; }
align-items: center; .server-item .meta .url { font-size: 11px; color: var(--muted); }
background: rgba(255,255,255,0.05);
padding: 8px; .status-bubble {
border-radius: var(--radius); font-size: 11px;
margin-bottom: 16px; font-weight: 600;
font-size: 13px; padding: 3px 10px;
} border-radius: 999px;
color: #fff;
/* ---------------------------------- */ background: transparent;
}
.playerList {
list-style:none; /* ── Detail panel ── */
padding-left:16px; .detail-panel {
max-height:140px; padding: 18px 20px;
overflow:auto; display: flex;
margin:6px 0; flex-direction: column;
display:flex; gap: 16px;
flex-wrap:wrap; min-height: 200px;
gap:8px; }
}
.placeholder {
.playerList li { color: var(--muted);
display:flex; font-size: 13px;
align-items:center; padding: 24px 0;
gap:4px; text-align: center;
font-size:13px; }
color:var(--text);
} /* Server identity + online badge */
.srv-identity {
.player-avatar { display: flex;
width:24px; justify-content: space-between;
height:24px; align-items: flex-start;
border-radius:4px; }
transition: transform 0.2s ease;
} .srv-name { font-size: 20px; font-weight: 700; color: #fff; margin: 0 0 4px; }
.srv-url { font-size: 12px; color: var(--muted); }
.player-avatar:hover { transform: scale(1.1); }
.online-badge {
.detailButtons { display: flex;
display:flex; align-items: center;
gap:8px; gap: 7px;
margin-top:8px; border-radius: 999px;
transition: opacity 0.3s ease; padding: 5px 14px;
} font-size: 13px;
font-weight: 600;
.detailButtons button { }
padding:6px 8px;
border-radius:6px; .online-badge.online {
border:none; background: rgba(34,197,94,0.12);
cursor:pointer; border: 1px solid rgba(34,197,94,0.25);
transition: opacity 0.2s ease, transform 0.1s ease; color: #4ade80;
} }
.detailButtons button:hover { opacity: 0.9; transform: translateY(-1px); } .online-badge.offline {
.detailButtons button:active { transform: translateY(0); } background: rgba(239,68,68,0.1);
border: 1px solid rgba(239,68,68,0.2);
#btnEdit { background: #f59e0b; color:#000; } color: #f87171;
#btnDelete { background: var(--danger); color:#fff; } }
#serversContainer::-webkit-scrollbar, .pulsing-dot {
.playerList::-webkit-scrollbar { width: 7px; height: 7px;
width: 6px; border-radius: 50%;
} flex-shrink: 0;
}
#serversContainer::-webkit-scrollbar-track,
.playerList::-webkit-scrollbar-track { .pulsing-dot.online {
background: rgba(255,255,255,0.05); background: var(--online);
border-radius: 3px; animation: pulse-green 2s infinite;
} }
#serversContainer::-webkit-scrollbar-thumb, .pulsing-dot.offline {
.playerList::-webkit-scrollbar-thumb { background: var(--offline);
background: var(--muted); animation: pulse-red 2s infinite;
border-radius: 3px; }
}
@keyframes pulse-green {
#serversContainer::-webkit-scrollbar-thumb:hover, 0%,100% { box-shadow: 0 0 0 0 rgba(34,197,94,0.5); }
.playerList::-webkit-scrollbar-thumb:hover { 50% { box-shadow: 0 0 0 5px rgba(34,197,94,0); }
background: var(--accent); }
} @keyframes pulse-red {
0%,100% { box-shadow: 0 0 0 0 rgba(239,68,68,0.5); }
50% { box-shadow: 0 0 0 5px rgba(239,68,68,0); }
}
/* MOTD */
.motd-bar {
font-size: 12px;
color: var(--muted2);
background: rgba(255,255,255,0.03);
border-radius: 8px;
padding: 8px 14px;
border-left: 2px solid rgba(59,130,246,0.35);
font-style: italic;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-top: 4px;
}
/* Stats row – 3 gleich breite Karten */
.stats-row {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 10px;
margin-top: 4px;
}
.stat-card {
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 12px 16px;
}
.stat-label {
font-size: 10px;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 5px;
}
.stat-val {
font-size: 18px;
font-weight: 700;
color: #fff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.stat-val.small {
font-size: 12px;
padding-top: 4px;
font-weight: 500;
color: var(--muted2);
}
/* Section label */
.section-label {
font-size: 11px;
font-weight: 700;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: 9px;
padding-top: 6px;
border-top: 1px solid var(--border);
}
/* Sub-server chips – immer 3 pro Zeile */
.sub-servers {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.sub-chip {
display: flex;
align-items: center;
gap: 7px;
border-radius: 8px;
padding: 8px 12px;
font-size: 13px;
font-weight: 600;
border: 1px solid var(--border);
background: var(--panel);
color: var(--muted2);
cursor: default;
transition: background 0.15s;
min-width: 0;
}
.sub-chip.active {
background: rgba(59,130,246,0.1);
border-color: rgba(59,130,246,0.3);
color: #93c5fd;
}
.sub-chip-dot {
width: 7px; height: 7px;
border-radius: 50%;
background: #2a3040;
flex-shrink: 0;
}
.sub-chip.active .sub-chip-dot {
background: var(--accent);
}
.sub-chip-name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
min-width: 0;
}
.sub-chip-count {
font-size: 11px;
font-weight: 500;
opacity: 0.65;
flex-shrink: 0;
margin-left: auto;
padding-left: 4px;
}
/* Players grid – 2 Spalten */
.players-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.player-card {
display: flex;
align-items: center;
gap: 12px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 10px 14px;
transition: background 0.15s;
min-width: 0;
}
.player-card:hover {
background: rgba(255,255,255,0.05);
}
.player-avatar {
width: 40px; height: 40px;
border-radius: 7px;
border: 1px solid rgba(255,255,255,0.1);
flex-shrink: 0;
image-rendering: pixelated;
}
.player-info { flex: 1; min-width: 0; }
.player-name {
font-size: 14px;
font-weight: 700;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.player-prefix {
font-size: 11px;
color: var(--muted2);
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.player-server-tag {
display: inline-block;
margin-top: 3px;
font-size: 11px;
font-weight: 600;
background: rgba(59,130,246,0.18);
color: #93c5fd;
border-radius: 5px;
padding: 2px 7px;
}
/* Edit/Delete buttons */
.detail-buttons {
display: flex;
gap: 10px;
padding-top: 10px;
border-top: 1px solid var(--border);
}
.detail-buttons button {
padding: 7px 16px;
border-radius: 7px;
border: none;
cursor: pointer;
font-size: 13px;
font-weight: 600;
}
#btnEdit { background: rgba(245,158,11,0.15); color: #fbbf24; border: 1px solid rgba(245,158,11,0.25); }
#btnDelete { background: rgba(239,68,68,0.12); color: #f87171; border: 1px solid rgba(239,68,68,0.2); }
.hidden { display: none !important; }
::-webkit-scrollbar { width: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: var(--accent); }
+96 -67
View File
@@ -1,68 +1,97 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" /> <meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Bungee Status</title> <title>Bungee Status</title>
<link rel="stylesheet" href="popup.css"> <link rel="stylesheet" href="popup.css">
</head> </head>
<body> <body>
<div class="root"> <div class="root">
<header>
<h1>Bungee Status</h1> <header>
<div class="actions"> <h1>Bungee Status</h1>
<button id="btnRefresh" title="Jetzt aktualisieren">↻</button> <div class="actions">
<button id="btnToggleSettings">⚙</button> <button id="btnRefresh" title="Jetzt aktualisieren">↻</button>
</div> <button id="btnToggleSettings" title="Einstellungen">⚙</button>
</header> </div>
</header>
<section class="main">
<aside class="server-list"> <div class="main">
<div class="add-form" id="settingsForm">
<input id="inputName" placeholder="Name (z. B. Lobby)" /> <div id="settingsForm" class="settings-panel hidden">
<input id="inputUrl" placeholder="URL (127.0.0.1:9191)" /> <h3 id="settingsTitle">Neuen Server hinzufügen</h3>
<input id="inputWpSite" placeholder="WP Site" /> <input id="inputName" placeholder="Name (z. B. Lobby)" />
<input id="inputWpServerId" placeholder="WP Server ID" /> <input id="inputUrl" placeholder="URL (z. B. 192.168.1.1:9191)" />
<button id="btnAddServer">Hinzufügen</button> <input id="inputWpSite" placeholder="WP Site (z. B. https://meine-seite.de)" />
</div> <input id="inputWpServerId" placeholder="WP Server ID" />
<button id="btnAddServer">Hinzufügen</button>
<ul id="serversContainer"></ul> </div>
</aside>
<div id="serverListPanel" class="server-list-panel hidden">
<div class="detail"> <ul id="serversContainer"></ul>
<div id="noSelection" class="placeholder">Wähle einen Server aus der linken Liste.</div> </div>
<div id="detailContent" class="detailContent hidden">
<div class="detail-panel">
<!-- NEUES LAYOUT ZEILE 1: Name, URL, Status (mit Pulsierendem Punkt) -->
<div class="detail-header"> <div id="noSelection" class="placeholder">Noch kein Server ausgewählt.</div>
<div class="server-identity">
<h2 id="detailName"></h2> <div id="detailContent" class="hidden">
<span id="detailUrlText" class="server-url"></span>
</div> <!-- Server Name + Status Badge -->
<div class="status-wrapper"> <div class="srv-identity">
<div id="detailPulse" class="pulsing-dot"></div> <div>
<span id="detailStatus"></span> <div class="srv-name" id="detailName"></div>
</div> <div class="srv-url" id="detailUrlText"></div>
</div> </div>
<div class="online-badge offline" id="statusBadge">
<!-- NEUES LAYOUT ZEILE 2: Spieler, Version, Ping --> <div class="pulsing-dot offline" id="detailPulse"></div>
<div class="detail-stats"> <span id="detailStatus">Offline</span>
<div><strong>Spieler:</strong> <span id="detailPlayers"></span></div> </div>
<div><strong>Version:</strong> <span id="detailVersion"></span></div> </div>
<div><strong>Ping:</strong> <span id="detailPing">-</span></div>
</div> <!-- MOTD -->
<div id="motdBar" class="motd-bar hidden"></div>
<h3>Online Spieler</h3>
<ul id="detailPlayerList" class="playerList"></ul> <!-- Stats: Spieler / Version / Ping -->
<div class="stats-row">
<div class="detailButtons"> <div class="stat-card">
<button id="btnEdit">Bearbeiten</button> <div class="stat-label">Spieler</div>
<button id="btnDelete">Löschen</button> <div class="stat-val" id="detailPlayers">-</div>
</div> </div>
</div> <div class="stat-card">
</div> <div class="stat-label">Version</div>
</section> <div class="stat-val small" id="detailVersion">-</div>
</div> </div>
<script src="popup.js"></script> <div class="stat-card">
</body> <div class="stat-label">Ping</div>
<div class="stat-val" id="detailPing">-</div>
</div>
</div>
<!-- Sub-Server -->
<div id="backendSection" class="hidden">
<div class="section-label">Sub-Server</div>
<div class="sub-servers" id="backendServersList"></div>
</div>
<!-- Online Spieler -->
<div>
<div class="section-label">Online Spieler</div>
<div class="players-grid" id="detailPlayerList"></div>
</div>
<!-- Edit / Delete -->
<div class="detail-buttons hidden" id="detailButtons">
<button id="btnEdit">Bearbeiten</button>
<button id="btnDelete">Löschen</button>
</div>
</div>
</div>
</div>
</div>
<script src="popup.js"></script>
</body>
</html> </html>
+312 -266
View File
@@ -1,377 +1,423 @@
// popup.js
const $ = id => document.getElementById(id); const $ = id => document.getElementById(id);
const uid = () => 'srv_' + Math.random().toString(36).slice(2,9); const uid = () => 'srv_' + Math.random().toString(36).slice(2,9);
const inputName = $('inputName');
const inputUrl = $('inputUrl');
const inputWpSite = $('inputWpSite');
const inputWpServerId = $('inputWpServerId');
const btnAdd = $('btnAddServer');
const serversContainer = $('serversContainer');
const serverListPanel = document.querySelector('.server-list');
const btnRefresh = $('btnRefresh');
const btnToggleSettings = $('btnToggleSettings');
const settingsForm = $('settingsForm');
const noSelection = $('noSelection');
const detailContent = $('detailContent');
const detailName = $('detailName');
const detailUrlText = $('detailUrlText');
const detailStatus = $('detailStatus');
const detailPulse = $('detailPulse');
const detailVersion = $('detailVersion');
const detailPlayers = $('detailPlayers');
const detailPing = $('detailPing');
const detailPlayerList = $('detailPlayerList');
const btnEdit = $('btnEdit');
const btnDelete = $('btnDelete');
let servers = []; let servers = [];
let selectedId = null; let selectedId = null;
let statuses = {}; let statuses = {};
let previousStatuses = {}; let previousStatuses = {};
let settingsVisible = false; let settingsVisible = false;
let editingId = null; // Welcher Server gerade bearbeitet wird
document.addEventListener('DOMContentLoaded', init); document.addEventListener('DOMContentLoaded', init);
btnAdd.addEventListener('click', handleAdd); $('btnAdd')?.addEventListener('click', handleAdd);
btnRefresh.addEventListener('click', manualRefresh); $('btnAddServer')?.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() { async function init() {
await loadServersFromStorage(); await loadServersFromStorage();
await loadStatusesFromStorage(); await loadStatusesFromStorage();
await loadSettingsVisibility(); await loadSettingsVisibility();
renderServerList(); renderServerList();
if (servers.length === 1) selectedId = servers[0].id; if (servers.length === 1) selectedId = servers[0].id;
renderDetail(selectedId); renderDetail(selectedId);
adjustDetailLayout();
} }
// --- Settings Visibility --- // ── Minecraft color code parser ──
function parseMinecraftColors(text) {
if (!text) return '';
const map = {
'§0':'#000000','§1':'#0000AA','§2':'#00AA00','§3':'#00AAAA',
'§4':'#AA0000','§5':'#AA00AA','§6':'#FFAA00','§7':'#AAAAAA',
'§8':'#555555','§9':'#5555FF','§a':'#55FF55','§b':'#55FFFF',
'§c':'#FF5555','§d':'#FF55FF','§e':'#FFFF55','§f':'#FFFFFF',
'&0':'#000000','&1':'#0000AA','&2':'#00AA00','&3':'#00AAAA',
'&4':'#AA0000','&5':'#AA00AA','&6':'#FFAA00','&7':'#AAAAAA',
'&8':'#555555','&9':'#5555FF','&a':'#55FF55','&b':'#55FFFF',
'&c':'#FF5555','&d':'#FF55FF','&e':'#FFFF55','&f':'#FFFFFF'
};
let html = text.replace(/[<>]/g, '');
let result = '';
let i = 0;
while (i < html.length) {
const ch = html[i];
if ((ch === '§' || ch === '&') && i + 1 < html.length) {
const code = ch + html[i+1].toLowerCase();
if (map[code]) {
result += `<span style="color:${map[code]}">`;
i += 2;
continue;
}
}
result += html[i];
i++;
}
return result;
}
function get3DAvatarUrl(name, uuid = null) {
return `https://mc-heads.net/head/${encodeURIComponent(name)}/64`;
}
// ── Storage ──
async function loadSettingsVisibility() { async function loadSettingsVisibility() {
const obj = await chrome.storage.local.get(['settingsVisible']); const obj = await chrome.storage.local.get(['settingsVisible']);
settingsVisible = obj.settingsVisible !== undefined ? obj.settingsVisible : false; settingsVisible = !!obj.settingsVisible;
if (!settingsVisible) settingsForm.classList.add('hidden'); applySettingsVisibility();
else settingsForm.classList.remove('hidden');
} }
async function saveSettingsVisibility() { async function saveSettingsVisibility() {
await chrome.storage.local.set({ settingsVisible }); await chrome.storage.local.set({ settingsVisible });
} }
function applySettingsVisibility() {
$('settingsForm').classList.toggle('hidden', !settingsVisible);
$('serverListPanel').classList.toggle('hidden', !settingsVisible);
$('detailButtons').classList.toggle('hidden', !settingsVisible);
}
async function toggleSettings() { async function toggleSettings() {
settingsVisible = !settingsVisible; settingsVisible = !settingsVisible;
settingsForm.classList.toggle('hidden'); applySettingsVisibility();
await saveSettingsVisibility(); await saveSettingsVisibility();
adjustDetailLayout();
} }
// --- Storage ---
async function loadServersFromStorage() { async function loadServersFromStorage() {
const obj = await chrome.storage.local.get(['servers']); const obj = await chrome.storage.local.get(['servers']);
servers = obj.servers || []; servers = obj.servers || [];
for (const s of servers) if (!s.id) s.id = uid(); for (const s of servers) if (!s.id) s.id = uid();
await chrome.storage.local.set({ servers }); await chrome.storage.local.set({ servers });
} }
async function saveServersToStorage() {
async function saveServersToStorage() { await chrome.storage.local.set({ servers });
await chrome.storage.local.set({ servers });
} }
async function loadStatusesFromStorage() { async function loadStatusesFromStorage() {
const obj = await chrome.storage.local.get(['serverStatuses']); const obj = await chrome.storage.local.get(['serverStatuses']);
statuses = obj.serverStatuses || {}; statuses = obj.serverStatuses || {};
} }
// --- Layout --- // ── Server list ──
function adjustDetailLayout() {
const settingsHidden = settingsForm.classList.contains('hidden');
detailContent.classList.toggle('full-width', settingsHidden);
serverListPanel.classList.toggle('hidden', settingsHidden);
btnEdit.style.display = settingsHidden ? 'none' : 'inline-block';
btnDelete.style.display = settingsHidden ? 'none' : 'inline-block';
}
// --- Render Server List ---
function renderServerList() { function renderServerList() {
serversContainer.innerHTML = ''; const container = $('serversContainer');
container.innerHTML = '';
if (servers.length === 0) { if (servers.length === 0) {
serversContainer.innerHTML = '<div class="placeholder">Noch keine Server hinzugefügt.</div>'; container.innerHTML = '<div class="placeholder" style="padding:8px 0;">Noch kein Server hinzugefügt.</div>';
return; return;
} }
for (const s of servers) { for (const s of servers) {
const item = document.createElement('li'); const li = document.createElement('li');
item.className = 'server-item'; li.className = 'server-item' + (s.id === selectedId ? ' selected' : '');
item.dataset.id = s.id; li.dataset.id = s.id;
const meta = document.createElement('div'); const meta = document.createElement('div');
meta.className = 'meta'; meta.className = 'meta';
const name = document.createElement('div');
name.className = 'name';
name.textContent = s.name || '(Kein Name)';
let listUrl = s.url || (s.wpSite ? s.wpSite + ' (WP)' : '');
listUrl = listUrl.replace(':9191', '');
const url = document.createElement('div');
url.className = 'url';
url.textContent = listUrl;
meta.append(name, url);
const statusBubble = document.createElement('div'); const nameEl = document.createElement('div');
statusBubble.className = 'status-bubble'; nameEl.className = 'name';
statusBubble.textContent = '—'; nameEl.textContent = s.name || '(Kein Name)';
statusBubble.style.backgroundColor = 'transparent';
item.statusBubble = statusBubble;
item.append(meta, statusBubble); let urlText = s.url || (s.wpSite || '');
item.addEventListener('click', () => { urlText = urlText.replace(':9191', '');
selectedId = s.id; const urlEl = document.createElement('div');
renderDetail(selectedId); urlEl.className = 'url';
urlEl.textContent = urlText;
meta.append(nameEl, urlEl);
const bubble = document.createElement('div');
bubble.className = 'status-bubble';
bubble.textContent = '—';
li._bubble = bubble;
li.append(meta, bubble);
li.addEventListener('click', () => {
selectedId = s.id;
document.querySelectorAll('.server-item').forEach(el => el.classList.remove('selected'));
li.classList.add('selected');
renderDetail(selectedId);
}); });
container.appendChild(li);
serversContainer.appendChild(item);
} }
updateServerListStatuses(false); updateServerListStatuses();
} }
// --- Render Detail --- // ── Detail ──
function renderDetail(id) { function renderDetail(id) {
if (!id) { if (!id) {
noSelection.classList.remove('hidden'); $('noSelection').classList.remove('hidden');
detailContent.classList.add('hidden'); $('detailContent').classList.add('hidden');
return; return;
} }
const srv = servers.find(x => x.id === id); const srv = servers.find(x => x.id === id);
if (!srv) { if (!srv) {
noSelection.classList.remove('hidden'); $('noSelection').classList.remove('hidden');
detailContent.classList.add('hidden'); $('detailContent').classList.add('hidden');
return; return;
} }
noSelection.classList.add('hidden'); $('noSelection').classList.add('hidden');
detailContent.classList.remove('hidden'); $('detailContent').classList.remove('hidden');
detailName.textContent = srv.name || 'Unbenannter Server';
let urlToShow = srv.url || (srv.wpSite ? srv.wpSite : 'Lokal');
urlToShow = urlToShow.replace(':9191', '');
detailUrlText.textContent = urlToShow;
updateDetailForServer(srv, true); $('detailName').textContent = srv.name || 'Unbenannter Server';
let urlToShow = srv.url || (srv.wpSite || 'Lokal');
urlToShow = urlToShow.replace(':9191', '');
$('detailUrlText').textContent = urlToShow;
updateDetailForServer(srv, true);
} }
// --- Update Detail ---
function updateDetailForServer(srv, force = false) { function updateDetailForServer(srv, force = false) {
const st = statuses[srv.id]; const st = statuses[srv.id];
const prevSt = previousStatuses[srv.id]; const prevSt = previousStatuses[srv.id];
const changed = force || JSON.stringify(st) !== JSON.stringify(prevSt);
const statusChanged = force || if (!changed) return;
!prevSt ||
(prevSt.ok !== st?.ok) || const badge = $('statusBadge');
(prevSt.data?.online !== st?.data?.online); const pulse = $('detailPulse');
if (!st || !st.ok || !st.data) { if (!st || !st.ok || !st.data) {
if (statusChanged) { badge.className = 'online-badge offline';
detailStatus.textContent = 'Offline'; pulse.className = 'pulsing-dot offline';
detailPulse.classList.remove('online'); $('detailStatus').textContent = 'Offline';
detailVersion.textContent = '-'; $('detailVersion').textContent = '-';
detailPlayers.textContent = '-'; $('detailPlayers').textContent = '-';
detailPing.textContent = '-'; $('detailPing').textContent = '-';
updatePlayerList([]); $('motdBar').classList.add('hidden');
} updatePlayerList([]);
updateBackendServers(null);
} else { } else {
const d = st.data; const d = st.data;
const isOnline = !!d.online;
if (statusChanged) {
detailStatus.textContent = d.online ? 'Online' : 'Offline';
if (d.online) {
detailPulse.classList.add('online');
} else {
detailPulse.classList.remove('online');
}
}
const newVersion = d.version || 'unknown';
if (force || detailVersion.textContent !== newVersion) {
detailVersion.textContent = newVersion;
}
const playersCount = Array.isArray(d.players) ? d.players.length : (typeof d.players === 'number' ? d.players : 0); badge.className = 'online-badge ' + (isOnline ? 'online' : 'offline');
const maxPlayers = d.max_players; pulse.className = 'pulsing-dot ' + (isOnline ? 'online' : 'offline');
let newPlayersText = String(playersCount); $('detailStatus').textContent = isOnline ? 'Online' : 'Offline';
if (maxPlayers && maxPlayers !== '-1') { $('detailVersion').textContent = d.version || 'unknown';
newPlayersText += ` / ${maxPlayers}`;
}
if (force || detailPlayers.textContent !== newPlayersText) { const count = Array.isArray(d.players) ? d.players.length
detailPlayers.textContent = newPlayersText; : (typeof d.players === 'number' ? d.players : 0);
} const max = d.max_players;
$('detailPlayers').textContent = max && max !== '-1' ? `${count} / ${max}` : String(count);
let pingVal = d.ping || d.latency || '-'; const ping = d.ping || d.latency;
if (pingVal !== '-' && typeof pingVal === 'number') { $('detailPing').textContent = typeof ping === 'number' ? ping + ' ms' : '-';
pingVal = pingVal + ' ms';
} const motdRaw = d.motd || (d.network && d.network.motd) || '';
if (motdRaw) {
if (force || (pingVal !== '-' && detailPing.textContent !== pingVal)) { const motdEl = $('motdBar');
detailPing.textContent = pingVal; motdEl.innerHTML = parseMinecraftColors(motdRaw);
motdEl.classList.remove('hidden');
} else {
$('motdBar').classList.add('hidden');
} }
const currentPlayers = Array.isArray(d.players) ? d.players : []; const currentPlayers = Array.isArray(d.players) ? d.players : [];
const prevPlayers = (prevSt && Array.isArray(prevSt.data?.players)) ? prevSt.data.players : []; updatePlayerList(currentPlayers);
updateBackendServers(d.network?.backend_servers || null);
let playersChanged = false;
if (force) {
playersChanged = true;
} else {
playersChanged = JSON.stringify(currentPlayers) !== JSON.stringify(prevPlayers);
}
if (playersChanged) {
updatePlayerList(currentPlayers);
}
} }
previousStatuses[srv.id] = st ? JSON.parse(JSON.stringify(st)) : null; previousStatuses[srv.id] = st ? JSON.parse(JSON.stringify(st)) : null;
} }
// --- Spielerliste --- // ── Sub-Server chips ──
function updatePlayerList(players) { function updateBackendServers(list) {
detailPlayerList.innerHTML = ''; const section = $('backendSection');
if (!players || players.length === 0) { const container = $('backendServersList');
detailPlayerList.innerHTML = '<li class="placeholder">Keine Spieler online.</li>'; if (!list || list.length === 0) {
section.classList.add('hidden');
container.innerHTML = '';
return; return;
} }
for (const p of players) { section.classList.remove('hidden');
const li = document.createElement('li'); container.innerHTML = '';
const name = typeof p === 'object' ? p.name || p.username || p.player || '' : String(p); for (const bs of list) {
const count = bs.online_players || 0;
if (typeof p === 'object' && p.avatar) { const chip = document.createElement('div');
const img = document.createElement('img'); chip.className = 'sub-chip' + (count > 0 ? ' active' : '');
img.src = p.avatar;
img.className = 'player-avatar'; const dot = document.createElement('span');
img.title = name; dot.className = 'sub-chip-dot';
li.appendChild(img);
} else { const nameEl = document.createElement('span');
// Falls kein Avatar vorhanden, generiere einen von mc-heads.net nameEl.className = 'sub-chip-name';
const img = document.createElement('img'); nameEl.textContent = bs.name;
img.src = `https://mc-heads.net/avatar/${encodeURIComponent(name)}/32`;
img.className = 'player-avatar'; const countEl = document.createElement('span');
img.title = name; countEl.className = 'sub-chip-count';
li.appendChild(img); countEl.textContent = count > 0 ? count : 'leer';
}
chip.append(dot, nameEl, countEl);
detailPlayerList.appendChild(li); container.appendChild(chip);
} }
} }
// --- Update Server List Statuses --- // ── Player list ──
function updateServerListStatuses() { function updatePlayerList(players) {
const items = serversContainer.querySelectorAll('.server-item'); const grid = $('detailPlayerList');
items.forEach(item => { grid.innerHTML = '';
const s = servers.find(x => x.id === item.dataset.id);
if (!s) return; if (!players || players.length === 0) {
grid.innerHTML = '<div class="placeholder">Keine Spieler online.</div>';
const st = statuses[s.id]; return;
const prevSt = previousStatuses[s.id]; }
const statusChanged = !prevSt || for (const p of players) {
(prevSt.ok !== st?.ok) || const card = document.createElement('div');
(prevSt.data?.online !== st?.data?.online); card.className = 'player-card';
if (!statusChanged) return; let name = '', uuid = null, prefix = '', serverName = null;
if (!st || !st.ok || !st.data) { if (typeof p === 'object') {
item.statusBubble.textContent = 'Offline'; name = p.name || p.username || p.player || '';
item.statusBubble.style.backgroundColor = 'var(--offline)'; uuid = p.uuid || null;
} else if (st.data.online) { prefix = p.prefix || p.group || '';
item.statusBubble.textContent = 'Online'; serverName = p.server || p.current_server || p.connected_server || null;
item.statusBubble.style.backgroundColor = 'var(--online)';
} else { } else {
item.statusBubble.textContent = 'Offline'; name = String(p);
item.statusBubble.style.backgroundColor = 'var(--offline)';
} }
previousStatuses[s.id] = st ? JSON.parse(JSON.stringify(st)) : null; const img = document.createElement('img');
img.src = (typeof p === 'object' && p.avatar) ? p.avatar : get3DAvatarUrl(name, uuid);
img.className = 'player-avatar';
img.loading = 'lazy';
img.onerror = function() {
this.onerror = null;
this.src = `https://mc-heads.net/avatar/${encodeURIComponent(name)}/64`;
};
const info = document.createElement('div');
info.className = 'player-info';
if (prefix) {
const prefixEl = document.createElement('div');
prefixEl.className = 'player-prefix';
prefixEl.innerHTML = parseMinecraftColors(prefix);
info.appendChild(prefixEl);
}
const nameEl = document.createElement('div');
nameEl.className = 'player-name';
nameEl.textContent = name;
info.appendChild(nameEl);
if (serverName) {
const tag = document.createElement('span');
tag.className = 'player-server-tag';
tag.textContent = serverName;
info.appendChild(tag);
}
card.append(img, info);
grid.appendChild(card);
}
}
// ── Server list status bubbles ──
function updateServerListStatuses() {
const items = $('serversContainer').querySelectorAll('.server-item');
items.forEach(item => {
const s = servers.find(x => x.id === item.dataset.id);
if (!s || !item._bubble) return;
const st = statuses[s.id];
if (!st || !st.ok || !st.data || !st.data.online) {
item._bubble.textContent = 'Offline';
item._bubble.style.background = 'rgba(239,68,68,0.15)';
item._bubble.style.color = '#f87171';
} else {
item._bubble.textContent = 'Online';
item._bubble.style.background = 'rgba(34,197,94,0.15)';
item._bubble.style.color = '#4ade80';
}
}); });
} }
// --- Add / Edit / Delete --- // ── Edit / Add Mode ──
function enterEditMode(server) {
editingId = server.id;
$('settingsTitle').textContent = 'Server bearbeiten';
$('btnAddServer').textContent = 'Änderungen speichern';
$('inputName').value = server.name || '';
$('inputUrl').value = server.url || '';
$('inputWpSite').value = server.wpSite || '';
$('inputWpServerId').value = server.wpServerId || '';
}
function exitEditMode() {
editingId = null;
$('settingsTitle').textContent = 'Neuen Server hinzufügen';
$('btnAddServer').textContent = 'Hinzufügen';
$('inputName').value = '';
$('inputUrl').value = '';
$('inputWpSite').value = '';
$('inputWpServerId').value = '';
}
// ── Actions ──
async function handleAdd() { async function handleAdd() {
const name = inputName.value.trim(); const name = $('inputName').value.trim();
const url = inputUrl.value.trim(); const url = $('inputUrl').value.trim();
const wpSite = inputWpSite.value.trim(); const wpSite = $('inputWpSite').value.trim();
const wpServerId = inputWpServerId.value.trim(); const wpServerId = $('inputWpServerId').value.trim();
if (!url && !wpSite) { if (!url && !wpSite) {
alert('Bitte URL oder WP Site angeben'); alert('Bitte eine URL oder WordPress-Einstellungen angeben.');
return; return;
} }
const s = { if (editingId) {
id: uid(), // === BEARBEITEN ===
name: name || url || wpSite, const srv = servers.find(s => s.id === editingId);
url: url || null, if (srv) {
wpSite: wpSite || null, srv.name = name || url || wpSite;
wpServerId: wpServerId || null if (url) srv.url = url;
}; if (wpSite) srv.wpSite = wpSite;
if (wpServerId) srv.wpServerId = wpServerId;
servers.push(s); }
exitEditMode();
} else {
// === NEU HINZUFÜGEN ===
const s = {
id: uid(),
name: name || url || wpSite,
url: url || null,
wpSite: wpSite || null,
wpServerId: wpServerId || null
};
servers.push(s);
}
await saveServersToStorage(); await saveServersToStorage();
inputName.value = '';
inputUrl.value = '';
inputWpSite.value = '';
inputWpServerId.value = '';
renderServerList(); renderServerList();
if (selectedId) renderDetail(selectedId);
} }
async function handleEdit() { async function handleEdit() {
if (!selectedId) return; if (!selectedId) return;
const srv = servers.find(s => s.id === selectedId); const srv = servers.find(s => s.id === selectedId);
if (!srv) return; if (!srv) return;
const newName = prompt('Name:', srv.name) || srv.name; settingsVisible = true;
const newUrl = prompt('URL:', srv.url || '') || srv.url; applySettingsVisibility();
const newWpSite = prompt('WP Site:', srv.wpSite || '') || srv.wpSite; await saveSettingsVisibility();
const newWpServerId = prompt('WP Server ID:', srv.wpServerId || '') || srv.wpServerId;
enterEditMode(srv);
srv.name = newName.trim();
srv.url = newUrl.trim();
srv.wpSite = newWpSite.trim();
srv.wpServerId = newWpServerId.trim();
await saveServersToStorage();
renderServerList();
renderDetail(selectedId);
} }
async function handleDelete() { async function handleDelete() {
if (!selectedId) return; if (!selectedId || !confirm('Server wirklich löschen?')) return;
if (!confirm('Server wirklich löschen?')) return; servers = servers.filter(s => s.id !== selectedId);
servers = servers.filter(s => s.id !== selectedId);
selectedId = null; selectedId = null;
await saveServersToStorage();
await saveServersToStorage();
renderServerList(); renderServerList();
renderDetail(null); renderDetail(null);
exitEditMode();
} }
async function manualRefresh() { async function manualRefresh() {
try { try { chrome.runtime.sendMessage({ cmd: 'refreshNow' }); } catch(e) {}
chrome.runtime.sendMessage({ cmd: 'refreshNow' });
} catch(e) {
console.error('Refresh fehlgeschlagen:', e);
}
} }
// --- Storage Listener --- // ── Live updates from background ──
chrome.storage.onChanged.addListener((changes, area) => { chrome.storage.onChanged.addListener((changes, area) => {
if (area === 'local' && changes.serverStatuses) { if (area === 'local' && changes.serverStatuses) {
statuses = changes.serverStatuses.newValue || {}; statuses = changes.serverStatuses.newValue || {};
@@ -1,12 +1,21 @@
<?php <?php
/* /*
* 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). Plugin URI:https://git.viper.ipv64.net/M_Viper/Minecraft-BungeeCord-Status
* Tags: minecraft, bungeecord, server status, player list Description: Der ultimative Live-Status für dein BungeeCord Netzwerk.
* Version: 3.6.1 Version: 3.6.6
* Author: M_Viper Author: M_Viper
* Requires at least: 6.0 Author URI: https://m-viper.de
* Requires PHP: 7.4 Requires at least: 6.0
Requires PHP: 7.4
Tested up to: 6.8
PHP Version: 7.4
License: GPL2
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Text Domain: minecraft-bungeecord-status
Tags: minecraft, bungeecord, server status, player list
Support: [Discord Support](https://discord.com/invite/FdRs4BRd8D)
Support: [Telegram Support](https://t.me/M_Viper04)
*/ */
if (!defined('ABSPATH')) exit; if (!defined('ABSPATH')) exit;
@@ -59,144 +68,133 @@ function mcss_format_minecraft_colors($text) {
return $formatted; return $formatted;
} }
/* ---------------- AUTO UPDATE (MCSS) ---------------- */ /* ---------------- HELPER: 3D AVATAR URL ---------------- */
if ( ! class_exists( 'MCSS_Auto_Update' ) ) { function mcss_get_3d_avatar($name, $uuid = null) {
class MCSS_Auto_Update { // Bedrock-Spieler erkennen (Name enthält Punkt oder UUID beginnt mit xuid)
$is_bedrock = strpos($name, '.') !== false || ($uuid && strpos($uuid, 'xuid') === 0);
private $plugin_file;
private $repo_owner = 'M_Viper'; if ($is_bedrock) {
private $repo_name = 'Minecraft-BungeeCord-Status'; // Bedrock: mc-heads.net mit 3D Head (nutze UUID falls vorhanden)
private $api_url; if ($uuid && !empty($uuid)) {
private $transient_key; return 'https://mc-heads.net/head/' . rawurlencode($uuid) . '/100';
public function __construct( $plugin_file ) {
$this->plugin_file = $plugin_file;
$this->api_url = 'https://git.viper.ipv64.net/api/v1/repos/' . rawurlencode( $this->repo_owner ) . '/' . rawurlencode( $this->repo_name ) . '/releases';
$this->transient_key = 'mcss_update_check_' . md5( $this->repo_owner . '/' . $this->repo_name );
if ( is_admin() ) {
add_action( 'admin_init', array( $this, 'check_for_update' ) );
}
} }
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';
}
}
public function check_for_update() {
if ( ! function_exists( 'get_file_data' ) ) { // ===============================
require_once ABSPATH . 'wp-admin/includes/plugin.php'; // MCSS - UPDATE NOTICE SYSTEM
// ===============================
// Plugin-Version aus Header lesen
function mcss_get_plugin_version() {
if ( ! function_exists( 'get_plugin_data' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$plugin_data = get_plugin_data( __FILE__ );
return $plugin_data['Version'] ?? '0.0.0';
}
// Cache manuell leeren
function mcss_clear_update_cache() {
if ( isset($_GET['mcss_clear_cache']) && current_user_can('manage_options') ) {
check_admin_referer('mcss_clear_cache_action');
delete_transient('mcss_latest_release');
wp_redirect( admin_url('plugins.php') );
exit;
}
}
add_action('admin_init', 'mcss_clear_update_cache');
// Neueste Release-Infos von Gitea holen
function mcss_get_latest_release_info( $force_refresh = false ) {
$transient_key = 'mcss_latest_release';
if ( $force_refresh ) {
delete_transient( $transient_key );
}
$release_info = get_transient( $transient_key );
if ( false === $release_info ) {
$response = wp_remote_get(
'https://git.viper.ipv64.net/api/v1/repos/M_Viper/Minecraft-BungeeCord-Status/releases/latest',
['timeout' => 10]
);
if ( ! is_wp_error($response) && 200 === wp_remote_retrieve_response_code($response) ) {
$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);
if ( $data && isset($data['tag_name']) ) {
$tag = ltrim( $data['tag_name'], 'vV' );
$release_info = [
'version' => $tag,
'download_url' => $data['zipball_url'] ?? '',
'notes' => $data['body'] ?? '',
'published_at' => $data['published_at'] ?? '',
];
set_transient( $transient_key, $release_info, 6 * HOUR_IN_SECONDS );
} else {
set_transient( $transient_key, [], HOUR_IN_SECONDS );
} }
} else {
$current = get_file_data( $this->plugin_file, array( 'Version' => 'Version' ), 'plugin' ); set_transient( $transient_key, [], HOUR_IN_SECONDS );
$current_version = isset( $current['Version'] ) ? $current['Version'] : '0.0.0';
$latest = $this->get_latest_release_info();
if ( $latest && ! empty( $latest['version'] ) && ! empty( $latest['url'] ) ) {
if ( version_compare( $latest['version'], $current_version, '>' ) ) {
add_action( 'admin_notices', function() use ( $latest, $current_version ) {
?>
<div class="notice notice-warning is-dismissible">
<p>
<strong>Minecraft BungeeCord Status – Update verfügbar</strong><br>
Neue Version: <strong><?php echo esc_html( $latest['version'] ); ?></strong><br>
Installiert: <strong><?php echo esc_html( $current_version ); ?></strong><br>
<a href="<?php echo esc_url( $latest['url'] ); ?>" class="button button-primary" target="_blank" rel="noreferrer noopener">Direkter Download (ZIP)</a>
<a href="<?php echo esc_url( 'https://git.viper.ipv64.net/' . rawurlencode( $this->repo_owner ) . '/' . rawurlencode( $this->repo_name ) . '/releases' ); ?>" class="button" target="_blank" style="margin-left:8px;" rel="noreferrer noopener">Releases ansehen</a>
</p>
</div>
<?php
} );
}
}
}
private function get_latest_release_info() {
$cached = get_transient( $this->transient_key );
if ( false !== $cached && is_array( $cached ) && ! empty( $cached['version'] ) ) {
return $cached;
}
$result = false;
$response = wp_remote_get( $this->api_url, array(
'timeout' => 8,
'headers' => array(
'Accept' => 'application/json',
'User-Agent' => 'MCSS-Update-Checker/1.0',
),
) );
if ( is_wp_error( $response ) ) {
set_transient( $this->transient_key, array( 'version' => '', 'url' => '' ), HOUR_IN_SECONDS );
return false;
}
$code = wp_remote_retrieve_response_code( $response );
if ( 200 !== (int) $code ) {
set_transient( $this->transient_key, array( 'version' => '', 'url' => '' ), HOUR_IN_SECONDS );
return false;
}
$body = wp_remote_retrieve_body( $response );
$json = json_decode( $body, true );
if ( ! is_array( $json ) || empty( $json ) ) {
set_transient( $this->transient_key, array( 'version' => '', 'url' => '' ), HOUR_IN_SECONDS );
return false;
}
foreach ( $json as $release ) {
$tag = '';
if ( ! empty( $release['tag_name'] ) ) {
$tag = ltrim( (string) $release['tag_name'], 'vV' );
} elseif ( ! empty( $release['name'] ) ) {
$tag = ltrim( (string) $release['name'], 'vV' );
}
if ( ! empty( $release['assets'] ) && is_array( $release['assets'] ) ) {
foreach ( $release['assets'] as $asset ) {
if ( empty( $asset['name'] ) || empty( $asset['browser_download_url'] ) ) {
continue;
}
if ( strtolower( $asset['name'] ) === 'minecraft-bungeecord-status.zip' ) {
$version = $tag ?: $this->extract_version_from_string( $asset['name'] . ' ' . ( $release['name'] ?? '' ) );
if ( $version ) {
$result = array(
'version' => $version,
'url' => $asset['browser_download_url'],
);
break 2;
} else {
$result = array(
'version' => ( $tag ?: (string) ( $release['name'] ?? '' ) ),
'url' => $asset['browser_download_url'],
);
break 2;
}
}
}
}
}
if ( $result ) {
set_transient( $this->transient_key, $result, 12 * HOUR_IN_SECONDS );
return $result;
}
set_transient( $this->transient_key, array( 'version' => '', 'url' => '' ), 6 * HOUR_IN_SECONDS );
return false;
}
private function extract_version_from_string( $str ) {
if ( preg_match( '/([0-9]+\.[0-9]+(?:\.[0-9]+)?)/', $str, $m ) ) {
return $m[1];
}
return '';
} }
} }
new MCSS_Auto_Update( __FILE__ ); return $release_info;
} }
// Admin-Update-Hinweis anzeigen
function mcss_show_update_notice() {
if ( ! current_user_can('manage_options') ) {
return;
}
$current_version = mcss_get_plugin_version();
$latest_release = mcss_get_latest_release_info();
if ( ! empty($latest_release['version']) && version_compare($current_version, $latest_release['version'], '<') ) {
$refresh_url = wp_nonce_url(
admin_url('plugins.php?mcss_clear_cache=1'),
'mcss_clear_cache_action'
);
?>
<div class="notice notice-warning is-dismissible">
<h3>Minecraft BungeeCord Status – Update verfügbar</h3>
<p>
Installiert: <strong><?php echo esc_html($current_version); ?></strong><br>
Neueste Version: <strong><?php echo esc_html($latest_release['version']); ?></strong>
</p>
<p>
<a href="<?php echo esc_url($latest_release['download_url']); ?>" class="button button-primary" target="_blank">
Update herunterladen
</a>
<a href="https://git.viper.ipv64.net/M_Viper/Minecraft-BungeeCord-Status/releases" class="button" target="_blank">
Release Notes
</a>
<a href="<?php echo esc_url($refresh_url); ?>" class="button">
Jetzt neu prüfen
</a>
</p>
</div>
<?php
}
}
add_action('admin_notices', 'mcss_show_update_notice');
/* ---------------- Assets ---------------- */ /* ---------------- Assets ---------------- */
add_action('admin_enqueue_scripts', function($hook){ add_action('admin_enqueue_scripts', function($hook){
global $pagenow; global $pagenow;
@@ -499,25 +497,89 @@ function mcss_render_row($srv, $i, $font_sizes, $announcement_types) {
} }
/* ---------------- LOGIC: STATUS API FETCHER ---------------- */ /* ---------------- LOGIC: STATUS API FETCHER ---------------- */
function mcss_fetch_data_via_api($host, $port, $timeout = 2) { function mcss_socket_http_json($host, $port, $path = '/', $timeout = 2.0) {
$socket = @fsockopen($host, $port, $errno, $errstr, $timeout); $socket = @fsockopen($host, $port, $errno, $errstr, $timeout);
if (!$socket) return false; if (!$socket) {
return false;
}
$out = "GET / HTTP/1.1\r\n"; stream_set_timeout($socket, (int)ceil($timeout));
$out = "GET " . $path . " HTTP/1.1\r\n";
$out .= "Host: " . $host . "\r\n"; $out .= "Host: " . $host . "\r\n";
$out .= "Connection: Close\r\n\r\n"; $out .= "User-Agent: MCSS-WordPress/3.6.6\r\n";
fwrite($socket, $out); $out .= "Accept: application/json\r\n";
$out .= "Connection: close\r\n\r\n";
if (@fwrite($socket, $out) === false) {
@fclose($socket);
return false;
}
$response = ''; $response = '';
while (!feof($socket)) { while (!feof($socket)) {
$response .= fgets($socket, 128); $chunk = @fread($socket, 2048);
if ($chunk === false) {
break;
}
if ($chunk === '') {
$meta = stream_get_meta_data($socket);
if (!empty($meta['timed_out'])) {
break;
}
usleep(10000);
continue;
}
$response .= $chunk;
} }
fclose($socket);
$parts = explode("\r\n\r\n", $response); $meta = stream_get_meta_data($socket);
$body = end($parts); @fclose($socket);
$data = json_decode($body, true);
return $data; if (!empty($meta['timed_out'])) {
return false;
}
$parts = explode("\r\n\r\n", $response, 2);
if (count($parts) !== 2) {
return false;
}
$statusLine = strtok($parts[0], "\r\n");
if (!is_string($statusLine) || stripos($statusLine, '200') === false) {
return false;
}
$data = json_decode($parts[1], true);
return is_array($data) ? $data : false;
}
function mcss_fetch_data_via_api($host, $port, $timeout = 2) {
// Primärer Endpunkt mit Detaildaten
$full = mcss_socket_http_json($host, $port, '/', $timeout);
if (is_array($full) && isset($full['online'])) {
return $full;
}
// Fallback auf Health-Endpunkt (StatusAPI >= hardened build)
$health = mcss_socket_http_json($host, $port, '/health', $timeout);
if (is_array($health) && !empty($health['online'])) {
return [
'online' => true,
'players' => [],
'version' => 'BungeeCord',
'ping' => 1,
'motd' => 'StatusAPI erreichbar (Health)',
];
}
// Ein zweiter kurzer Versuch reduziert False Negatives bei Paketverlust.
$retry = mcss_socket_http_json($host, $port, '/', max(1.0, $timeout));
if (is_array($retry) && isset($retry['online'])) {
return $retry;
}
return false;
} }
function mcss_parse_bungeecord_version($raw_version) { function mcss_parse_bungeecord_version($raw_version) {
@@ -528,16 +590,26 @@ function mcss_parse_bungeecord_version($raw_version) {
} }
function mcss_fetch_server_with_ranks($srv) { function mcss_fetch_server_with_ranks($srv) {
$cache_key = 'mcss_data_' . md5($srv['host']); $host = $srv['host'];
$port = $srv['player_port'] ?? 9191;
$cache_key = 'mcss_data_' . md5($host . ':' . $port);
$last_good_key = 'mcss_last_good_' . md5($host . ':' . $port);
$cached = get_transient($cache_key); $cached = get_transient($cache_key);
if ($cached !== false) return $cached; if ($cached !== false) return $cached;
$host = $srv['host']; $api_data = mcss_fetch_data_via_api($host, $port, 2);
$port = $srv['player_port'] ?? 9191;
$api_data = mcss_fetch_data_via_api($host, $port, 1);
if (!$api_data || !isset($api_data['online'])) { if (!$api_data || !isset($api_data['online'])) {
$last_good = get_transient($last_good_key);
if (is_array($last_good) && !empty($last_good['online'])) {
$last_good['stale'] = true;
$last_good['stale_reason'] = 'temporary_connection_loss';
set_transient($cache_key, $last_good, 5);
return $last_good;
}
return ['online'=>false,'players'=>[],'version'=>'Offline','ping'=>9999,'motd'=>'Verbindung fehlgeschlagen']; return ['online'=>false,'players'=>[],'version'=>'Offline','ping'=>9999,'motd'=>'Verbindung fehlgeschlagen'];
} }
@@ -547,10 +619,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
@@ -559,12 +633,20 @@ function mcss_fetch_server_with_ranks($srv) {
// 2. Namen immer Schwarz darstellen (überschreibt eventuelle Farben aus dem Prefix) // 2. Namen immer Schwarz darstellen (überschreibt eventuelle Farben aus dem Prefix)
$name_html = '<span style="color:black;">' . esc_html($name) . '</span>'; $name_html = '<span style="color:black;">' . esc_html($name) . '</span>';
// 3. Zusammenbauen // 3. Zusammenbauen (Nur Abstand, wenn Prefix existiert)
$display_html = trim($prefix_html) . ' ' . $name_html; if (!empty($prefix_html)) {
$display_html = $prefix_html . ' ' . $name_html;
} else {
$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',
@@ -575,7 +657,7 @@ function mcss_fetch_server_with_ranks($srv) {
$clean_version = mcss_parse_bungeecord_version($api_data['version'] ?? 'BungeeCord'); $clean_version = mcss_parse_bungeecord_version($api_data['version'] ?? 'BungeeCord');
$result = [ $result = [
'online' => true, 'online' => !empty($api_data['online']),
'players' => $players_info, 'players' => $players_info,
'version' => $clean_version, 'version' => $clean_version,
'ping' => 1, 'ping' => 1,
@@ -584,6 +666,9 @@ function mcss_fetch_server_with_ranks($srv) {
$fast_cache_ttl = max(2, absint($srv['cache_ttl'] ?? 10)); $fast_cache_ttl = max(2, absint($srv['cache_ttl'] ?? 10));
set_transient($cache_key, $result, $fast_cache_ttl); set_transient($cache_key, $result, $fast_cache_ttl);
if (!empty($result['online'])) {
set_transient($last_good_key, $result, 180);
}
return $result; return $result;
} }
@@ -666,7 +751,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'])) {
@@ -752,13 +837,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: ?>
@@ -843,8 +928,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 {
@@ -870,4 +955,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' );
});
+92 -6
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,58 @@ 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)**
---
**Copyright © 2026 - M_Viper - Alle Rechte vorbehalten**
Die unbefugte Vervielfältigung, Verbreitung oder Weitergabe dieses Plugins ist strafbar und wird rechtlich verfolgt.
+4 -3
View File
@@ -7,7 +7,7 @@
<groupId>net.viper.bungee</groupId> <groupId>net.viper.bungee</groupId>
<artifactId>StatusAPI</artifactId> <artifactId>StatusAPI</artifactId>
<version>1.1</version> <version>3.6.2</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>StatusAPI</name> <name>StatusAPI</name>
@@ -20,7 +20,7 @@
</properties> </properties>
<dependencies> <dependencies>
<!-- BungeeCord API (lokal installiert) --> <!-- BungeeCord API -->
<dependency> <dependency>
<groupId>net.md-5</groupId> <groupId>net.md-5</groupId>
<artifactId>bungeecord-api</artifactId> <artifactId>bungeecord-api</artifactId>
@@ -28,12 +28,13 @@
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<!-- LuckPerms API für Prefix Support --> <!-- LuckPerms API (Optional) -->
<dependency> <dependency>
<groupId>net.luckperms</groupId> <groupId>net.luckperms</groupId>
<artifactId>api</artifactId> <artifactId>api</artifactId>
<version>5.4</version> <version>5.4</version>
<scope>provided</scope> <scope>provided</scope>
<optional>true</optional>
</dependency> </dependency>
</dependencies> </dependencies>
@@ -0,0 +1,59 @@
package net.viper.status;
import net.md_5.bungee.api.plugin.Plugin;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.util.function.Consumer;
/**
* FileDownloader: Lädt Dateien asynchron herunter (CMILib Style).
*/
public class FileDownloader {
private final Plugin plugin;
public FileDownloader(Plugin plugin) {
this.plugin = plugin;
}
/**
* Lädt eine Datei herunter.
* @param urlString Die Download URL
* @param destination Die Zieldatei
* @param onSuccess Callback, der im Hauptthread ausgeführt wird, wenn fertig.
*/
public void downloadFile(String urlString, File destination, Runnable onSuccess) {
plugin.getProxy().getScheduler().runAsync(plugin, () -> {
BufferedInputStream bufferedInputStream = null;
FileOutputStream fileOutputStream = null;
try {
URL url = new URL(urlString);
bufferedInputStream = new BufferedInputStream(url.openStream());
fileOutputStream = new FileOutputStream(destination);
byte[] buffer = new byte[1024];
int count;
while ((count = bufferedInputStream.read(buffer, 0, 1024)) != -1) {
fileOutputStream.write(buffer, 0, count);
}
// Schließen
fileOutputStream.close();
bufferedInputStream.close();
// Callback im Main Thread
plugin.getProxy().getScheduler().schedule(plugin, onSuccess, 1, java.util.concurrent.TimeUnit.MILLISECONDS);
} catch (Throwable e) {
plugin.getLogger().warning("Download fehlgeschlagen: " + e.getMessage());
if (destination.exists()) destination.delete();
} finally {
if (fileOutputStream != null) try { fileOutputStream.close(); } catch (IOException ignored) {}
if (bufferedInputStream != null) try { bufferedInputStream.close(); } catch (IOException ignored) {}
}
});
}
}
@@ -1,15 +1,16 @@
package net.viper.status; package net.viper.status;
import net.luckperms.api.LuckPerms;
import net.luckperms.api.LuckPermsProvider;
import net.luckperms.api.model.user.User;
import net.luckperms.api.query.QueryOptions;
import net.md_5.bungee.api.ProxyServer; import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.config.ListenerInfo; import net.md_5.bungee.api.config.ListenerInfo;
import net.md_5.bungee.api.connection.ProxiedPlayer; import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.plugin.Plugin; import net.md_5.bungee.api.plugin.Plugin;
import java.io.*; import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.net.Socket; import java.net.Socket;
import java.util.*; import java.util.*;
@@ -20,6 +21,7 @@ public class StatusAPI extends Plugin implements Runnable {
private Thread thread; private Thread thread;
private int port = 9191; private int port = 9191;
private UpdateChecker updateChecker; private UpdateChecker updateChecker;
private FileDownloader fileDownloader;
@Override @Override
public void onEnable() { public void onEnable() {
@@ -30,18 +32,33 @@ public class StatusAPI extends Plugin implements Runnable {
thread = new Thread(this, "StatusAPI-HTTP-Server"); thread = new Thread(this, "StatusAPI-HTTP-Server");
thread.start(); thread.start();
// Start UpdateChecker: initialer Check (async) + regelmäßiger Schedule
String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0"; String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0";
updateChecker = new UpdateChecker(this, currentVersion, 6); // 6 Stunden Intervall updateChecker = new UpdateChecker(this, currentVersion, 6); // 6 Stunden Intervall
fileDownloader = new FileDownloader(this);
// initialer sofortiger Start (async) File pluginFile = getFile();
ProxyServer.getInstance().getScheduler().runAsync(this, () -> updateChecker.checkNow()); File backupFile = new File(pluginFile.getParentFile(), "StatusAPI.jar.bak");
// planmäßiger Intervall (alle 6 Stunden) // --- AUTOMATISCHES BACKUP-CLEANUP ---
ProxyServer.getInstance().getScheduler().schedule(this, updateChecker, 6, 6, TimeUnit.HOURS); // Falls ein altes Update (.bak) im Ordner liegt, löschen wir es nach 1 Minute Startzeit.
// Wenn der Server kurz crashen würde, hast du noch 60 Sekunden Zeit, ihn zu stoppen,
// damit das Backup erhalten bleibt. Läuft er stabil, wird der Platz freigegeben.
if (backupFile.exists()) {
ProxyServer.getInstance().getScheduler().schedule(this, () -> {
if (backupFile.exists()) {
if (backupFile.delete()) {
getLogger().info("Altes Backup (.bak) wurde erfolgreich gelöscht.");
}
}
}, 1, TimeUnit.MINUTES);
}
// ---------------------------------------
// Register join listener to notify OPs on login // Sofortiger Start-Check
ProxyServer.getInstance().getPluginManager().registerListener(this, new UpdateListener(this, updateChecker)); checkAndMaybeUpdate();
// Regelmäßiger Check (alle 6 Stunden)
ProxyServer.getInstance().getScheduler().schedule(this, this::checkAndMaybeUpdate, 6, 6, TimeUnit.HOURS);
} }
@Override @Override
@@ -49,9 +66,99 @@ public class StatusAPI extends Plugin implements Runnable {
getLogger().info("Stoppe Web-Server..."); getLogger().info("Stoppe Web-Server...");
if (thread != null) { if (thread != null) {
thread.interrupt(); thread.interrupt();
try { thread.join(1000); } catch (InterruptedException ignored) {}
}
}
/**
* Prüft Update und startet Download falls nötig.
*/
private void checkAndMaybeUpdate() {
try {
updateChecker.checkNow();
String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0";
if (updateChecker.isUpdateAvailable(currentVersion)) {
String newVersion = updateChecker.getLatestVersion();
String url = updateChecker.getLatestUrl();
getLogger().warning("----------------------------------------");
getLogger().warning("Neue Version verfügbar: " + newVersion);
getLogger().warning("Starte automatisches Update...");
getLogger().warning("----------------------------------------");
// Download Starten
File pluginFile = getFile();
File newFile = new File(pluginFile.getParentFile(), "StatusAPI.jar.new");
fileDownloader.downloadFile(url, newFile, () -> {
// Callback: Wenn Download erfolgreich
triggerUpdateScript(pluginFile, newFile);
});
}
} catch (Exception e) {
getLogger().severe("Fehler beim Update-Check: " + e.getMessage());
}
}
/**
* Erstellt ein externes Batch-Skript, startet es und stoppt den Server.
* Das Skript führt den Datei-Tausch durch, wenn der Server weg ist.
*/
private void triggerUpdateScript(File currentFile, File newFile) {
try {
File pluginsFolder = currentFile.getParentFile();
// Wir legen das Skript neben die Haupt-JAR (root) damit es nicht im Plugin-Ordner liegt
File rootFolder = pluginsFolder.getParentFile();
File batFile = new File(rootFolder, "StatusAPI_Update_" + System.currentTimeMillis() + ".bat");
// Batch Inhalt
// 1. Wartet 5 Sekunden (Server fährt runter)
// 2. Geht in den Plugin Ordner
// 3. Sichert .jar zu .bak
// 4. Benennt .new zu .jar um
// 5. Löscht sich selbst
String batContent = "@echo off\n" +
"echo Bitte warten, der Server f\"ahrt herunter...\n" +
"timeout /t 5 /nobreak >nul\n" +
"cd /d \"" + pluginsFolder.getAbsolutePath().replace("\\", "/") + "\"\n" +
"echo Fuehre Datei-Tausch durch...\n" +
"if exist StatusAPI.jar.bak del StatusAPI.jar.bak\n" +
"if exist StatusAPI.jar (\n" +
" ren StatusAPI.jar StatusAPI.jar.bak\n" +
")\n" +
"if exist StatusAPI.new.jar (\n" +
" ren StatusAPI.new.jar StatusAPI.jar\n" +
" echo Update erfolgreich!\n" +
") else (\n" +
" echo FEHLER: StatusAPI.new.jar nicht gefunden!\n" +
" pause\n" +
")\n" +
"del \"%~f0\"";
try (PrintWriter out = new PrintWriter(batFile)) {
out.println(batContent);
}
// Spieler kicken
try { try {
thread.join(1000); for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
} catch (InterruptedException ignored) {} p.disconnect("§cServer f\"ahrt f\"ur ein Update neu herunter. Bitte etwas warten.");
}
} catch (Exception ignored) {}
// Skript starten
getLogger().info("Starte Update-Skript im Hintergrund...");
try {
Runtime.getRuntime().exec("cmd /c start \"Update_Proc\" \"" + batFile.getAbsolutePath() + "\"");
} catch (IOException e) {
getLogger().warning("Konnte Skript nicht starten. Update wird manuell ben\"otigt.");
}
// Server stoppen
ProxyServer.getInstance().stop();
} catch (Exception e) {
getLogger().severe("Fehler beim Vorbereiten des Updates: " + e.getMessage());
} }
} }
@@ -59,189 +166,124 @@ public class StatusAPI extends Plugin implements Runnable {
public void run() { public void run() {
try (ServerSocket serverSocket = new ServerSocket(port)) { try (ServerSocket serverSocket = new ServerSocket(port)) {
serverSocket.setSoTimeout(1000); serverSocket.setSoTimeout(1000);
while (!Thread.interrupted()) { while (!Thread.interrupted()) {
try { try {
Socket clientSocket = serverSocket.accept(); Socket clientSocket = serverSocket.accept();
handleConnection(clientSocket); handleConnection(clientSocket);
} catch (java.net.SocketTimeoutException e) { } catch (java.net.SocketTimeoutException e) {}
// Loop Check (timeout) - erlaubt Unterbrechungsschleife catch (IOException e) {
} catch (IOException e) {
getLogger().warning("Fehler beim Akzeptieren einer Verbindung: " + e.getMessage()); getLogger().warning("Fehler beim Akzeptieren einer Verbindung: " + e.getMessage());
} }
} }
} catch (IOException e) { } catch (IOException e) {
getLogger().severe("Konnte ServerSocket nicht starten auf Port " + port + ": " + e.getMessage()); getLogger().severe("Konnte ServerSocket nicht starten auf Port " + port + ": " + e.getMessage());
e.printStackTrace();
} }
} }
private void handleConnection(Socket clientSocket) { private void handleConnection(Socket clientSocket) {
BufferedReader in = null; try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), "UTF-8"));
OutputStream out = null; OutputStream out = clientSocket.getOutputStream()) {
try {
in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), "UTF-8"));
out = clientSocket.getOutputStream();
String inputLine = in.readLine(); String inputLine = in.readLine();
if (inputLine != null && inputLine.startsWith("GET")) { if (inputLine != null && inputLine.startsWith("GET")) {
Map<String, Object> data = new LinkedHashMap<>(); Map<String, Object> data = new LinkedHashMap<>();
data.put("online", true); data.put("online", true);
// --- VERSION CLEANUP START --- // Version
String versionRaw = ProxyServer.getInstance().getVersion(); String versionRaw = ProxyServer.getInstance().getVersion();
String versionClean = versionRaw; String versionClean = versionRaw;
if (versionRaw != null && versionRaw.contains(":")) { if (versionRaw != null && versionRaw.contains(":")) {
String[] parts = versionRaw.split(":"); String[] parts = versionRaw.split(":");
if (parts.length >= 3) { if (parts.length >= 3) versionClean = parts[2].trim();
versionClean = parts[2].trim();
}
} }
data.put("version", versionClean); data.put("version", versionClean);
// --- VERSION CLEANUP ENDE ---
data.put("max_players", String.valueOf(ProxyServer.getInstance().getConfig().getPlayerLimit())); data.put("max_players", String.valueOf(ProxyServer.getInstance().getConfig().getPlayerLimit()));
// Motd
String motd = "BungeeCord"; String motd = "BungeeCord";
try { try {
Iterator<ListenerInfo> it = ProxyServer.getInstance().getConfig().getListeners().iterator(); Iterator<ListenerInfo> it = ProxyServer.getInstance().getConfig().getListeners().iterator();
if (it.hasNext()) { if (it.hasNext()) {
ListenerInfo listener = it.next(); ListenerInfo listener = it.next();
if (listener != null && listener.getMotd() != null) { if (listener != null && listener.getMotd() != null) motd = listener.getMotd();
motd = listener.getMotd();
}
} }
} catch (Exception e) { } catch (Exception ignored) {}
// Fallback bleibt "BungeeCord"
}
data.put("motd", motd); data.put("motd", motd);
// --- LUCKPERMS INTEGRATION START --- // LuckPerms (Optional)
// LuckPerms API über Provider holen (Verhindert ClassCastException) boolean luckPermsEnabled = ProxyServer.getInstance().getPluginManager().getPlugin("LuckPerms") != null;
LuckPerms luckPermsApi = null;
try {
luckPermsApi = LuckPermsProvider.get();
} catch (IllegalStateException e) {
// LuckPerms ist nicht geladen
}
List<Map<String, String>> playersList = new ArrayList<>(); List<Map<String, String>> playersList = new ArrayList<>();
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
Map<String, String> playerInfo = new LinkedHashMap<>(); Map<String, String> playerInfo = new LinkedHashMap<>();
playerInfo.put("name", p.getName()); playerInfo.put("name", p.getName());
// Prefix abfragen, falls LP gefunden wurde
String prefix = ""; String prefix = "";
if (luckPermsApi != null) { if (luckPermsEnabled) {
User user = luckPermsApi.getUserManager().getUser(p.getUniqueId()); try {
if (user != null) { Class<?> providerClass = Class.forName("net.luckperms.api.LuckPermsProvider");
// Context bestimmen (Global oder Server-spezifisch) Object luckPermsApi = providerClass.getMethod("get").invoke(null);
QueryOptions queryOptions = luckPermsApi.getContextManager().getQueryOptions(user) Object userManager = luckPermsApi.getClass().getMethod("getUserManager").invoke(luckPermsApi);
.orElse(QueryOptions.defaultContextualOptions()); Object user = userManager.getClass().getMethod("getUser", UUID.class).invoke(userManager, p.getUniqueId());
if (user != null) {
String lpPrefix = user.getCachedData().getMetaData(queryOptions).getPrefix(); Class<?> queryOptionsClass = Class.forName("net.luckperms.api.query.QueryOptions");
if (lpPrefix != null) { Object queryOptions = queryOptionsClass.getMethod("defaultContextualOptions").invoke(null);
prefix = lpPrefix; Object cachedData = user.getClass().getMethod("getCachedData").invoke(user);
Object metaData = cachedData.getClass().getMethod("getMetaData", queryOptionsClass).invoke(cachedData, queryOptions);
Object result = metaData.getClass().getMethod("getPrefix").invoke(metaData);
if (result != null) prefix = (String) result;
} }
} } catch (Exception ignored) {}
} }
playerInfo.put("prefix", prefix); playerInfo.put("prefix", prefix);
playersList.add(playerInfo); playersList.add(playerInfo);
} }
data.put("players", playersList); data.put("players", playersList);
// --- LUCKPERMS INTEGRATION ENDE ---
// Response
String json = buildJsonString(data); String json = buildJsonString(data);
byte[] jsonBytes = json.getBytes("UTF-8"); byte[] jsonBytes = json.getBytes("UTF-8");
// HTTP Response mit korrekter Byte-Length
StringBuilder response = new StringBuilder(); StringBuilder response = new StringBuilder();
response.append("HTTP/1.1 200 OK\r\n"); response.append("HTTP/1.1 200 OK\r\n");
response.append("Content-Type: application/json; charset=UTF-8\r\n"); response.append("Content-Type: application/json; charset=UTF-8\r\n");
response.append("Access-Control-Allow-Origin: *\r\n"); response.append("Access-Control-Allow-Origin: *\r\n");
response.append("Content-Length: ").append(jsonBytes.length).append("\r\n"); response.append("Content-Length: ").append(jsonBytes.length).append("\r\n");
response.append("Connection: close\r\n"); response.append("Connection: close\r\n\r\n");
response.append("\r\n");
// Header senden
out.write(response.toString().getBytes("UTF-8")); out.write(response.toString().getBytes("UTF-8"));
// Body senden
out.write(jsonBytes); out.write(jsonBytes);
out.flush(); out.flush();
} else {
// Ungültige Anfrage -> 400
String resp = "HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n";
out.write(resp.getBytes("UTF-8"));
out.flush();
} }
} catch (Exception e) { } catch (Exception e) {
getLogger().severe("Fehler beim Verarbeiten der Anfrage: " + e.getMessage()); getLogger().severe("Fehler beim Verarbeiten der Anfrage: " + e.getMessage());
e.printStackTrace();
} finally {
// Sauber aufräumen
try {
if (out != null) out.close();
if (in != null) in.close();
if (clientSocket != null && !clientSocket.isClosed()) {
clientSocket.close();
}
} catch (IOException e) {
// Ignorieren
}
} }
} }
/**
* Rekursiver JSON Builder, der nun auch Listen von Objekten (Maps) verarbeiten kann.
*/
private String buildJsonString(Map<String, Object> data) { private String buildJsonString(Map<String, Object> data) {
StringBuilder sb = new StringBuilder("{"); StringBuilder sb = new StringBuilder("{");
boolean first = true; boolean first = true;
for (Map.Entry<String, Object> entry : data.entrySet()) { for (Map.Entry<String, Object> entry : data.entrySet()) {
if (!first) sb.append(","); if (!first) sb.append(",");
first = false; first = false;
sb.append("\"").append(escapeJson(entry.getKey())).append("\":"); sb.append("\"").append(escapeJson(entry.getKey())).append("\":").append(valueToString(entry.getValue()));
// Wert verarbeiten (String, Boolean, List oder Map)
sb.append(valueToString(entry.getValue()));
} }
sb.append("}"); sb.append("}");
return sb.toString(); return sb.toString();
} }
/**
* Hilfsmethode, um verschiedene Objekttypen in JSON-Strings umzuwandeln.
*/
private String valueToString(Object value) { private String valueToString(Object value) {
if (value == null) { if (value == null) return "null";
return "null"; else if (value instanceof Boolean) return value.toString();
} else if (value instanceof Boolean) { else if (value instanceof List) {
return value.toString();
} else if (value instanceof List) {
StringBuilder sb = new StringBuilder("["); StringBuilder sb = new StringBuilder("[");
List<?> list = (List<?>) value; List<?> list = (List<?>) value;
for (int i = 0; i < list.size(); i++) { for (int i = 0; i < list.size(); i++) {
if (i > 0) sb.append(","); if (i > 0) sb.append(",");
Object item = list.get(i); Object item = list.get(i);
// Wenn es eine Map ist (Player-Objekt), rufen wir buildJsonString rekursiv auf if (item instanceof Map) sb.append(buildJsonString((Map<String, Object>) item));
if (item instanceof Map) { else sb.append("\"").append(escapeJson(String.valueOf(item))).append("\"");
sb.append(buildJsonString((Map<String, Object>) item));
} else {
// Andernfalls String behandeln
sb.append("\"").append(escapeJson(String.valueOf(item))).append("\"");
}
} }
sb.append("]"); sb.append("]");
return sb.toString(); return sb.toString();
} else {
// Standard String Behandlung
return "\"" + escapeJson(String.valueOf(value)) + "\"";
} }
else return "\"" + escapeJson(String.valueOf(value)) + "\"";
} }
private String escapeJson(String s) { private String escapeJson(String s) {
@@ -1,211 +1,151 @@
package net.viper.status; package net.viper.status;
import net.md_5.bungee.api.plugin.Plugin; import net.md_5.bungee.api.plugin.Plugin;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.connection.ProxiedPlayer; import java.io.BufferedReader;
import net.md_5.bungee.api.chat.TextComponent; import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.io.BufferedReader; import java.net.URL;
import java.io.InputStreamReader; import java.util.logging.Level;
import java.net.HttpURLConnection; import java.util.regex.Matcher;
import java.net.URL; import java.util.regex.Pattern;
import java.util.logging.Level;
import java.util.regex.Matcher; public class UpdateChecker {
import java.util.regex.Pattern;
private final Plugin plugin;
/** private final String currentVersion;
* UpdateChecker: private final int intervalHours;
* - Fragt die Gitea Releases API ab
* - Sucht in den Releases nach dem Asset "StatusAPI.jar" private final String apiUrl = "https://git.viper.ipv64.net/api/v1/repos/M_Viper/Minecraft-BungeeCord-Status/releases";
* - Extrahiert die Version (tag_name bevorzugt, name als Fallback)
* - Cache die gefundene Version + URL (volatile fields) private volatile String latestVersion = "";
*/ private volatile String latestUrl = "";
public class UpdateChecker implements Runnable {
// Pattern für Dateinamen im Assets-Array
private final Plugin plugin; private static final Pattern ASSET_NAME_PATTERN = Pattern.compile("\"name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
private final String currentVersion; // Pattern für Download-URL
private final int intervalHours; private static final Pattern DOWNLOAD_PATTERN = Pattern.compile("\"browser_download_url\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
// Pattern für Tag Version (WICHTIG: Wir suchen global, um das Haupt-Release zu finden)
// Gitea Releases API für dein Repo private static final Pattern TAG_NAME_PATTERN = Pattern.compile("\"tag_name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
private final String apiUrl = "https://git.viper.ipv64.net/api/v1/repos/M_Viper/Minecraft-BungeeCord-Status/releases";
public UpdateChecker(Plugin plugin, String currentVersion, int intervalHours) {
// cached results (volatile für Thread-Sichtbarkeit) this.plugin = plugin;
private volatile String latestVersion = ""; this.currentVersion = currentVersion != null ? currentVersion : "0.0.0";
private volatile String latestUrl = ""; this.intervalHours = Math.max(1, intervalHours);
}
// Patterns for quick parse (we parse JSON as text; this is lightweight and robust for our needs)
private static final Pattern ASSET_NAME_PATTERN = Pattern.compile("\"name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE); public void checkNow() {
private static final Pattern DOWNLOAD_PATTERN = Pattern.compile("\"browser_download_url\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE); try {
private static final Pattern TAG_NAME_PATTERN = Pattern.compile("\"tag_name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE); HttpURLConnection conn = (HttpURLConnection) new URL(apiUrl).openConnection();
private static final Pattern NAME_FIELD_PATTERN = Pattern.compile("\"name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE); conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json");
public UpdateChecker(Plugin plugin, String currentVersion, int intervalHours) { conn.setRequestProperty("User-Agent", "StatusAPI-UpdateChecker/2.0");
this.plugin = plugin; conn.setConnectTimeout(5000);
this.currentVersion = currentVersion != null ? currentVersion : "0.0.0"; conn.setReadTimeout(5000);
this.intervalHours = Math.max(1, intervalHours);
} int code = conn.getResponseCode();
if (code != 200) {
@Override plugin.getLogger().warning("Gitea API nicht erreichbar (HTTP " + code + ")");
public void run() { return;
// scheduled task calls checkNow }
checkNow();
} StringBuilder sb = new StringBuilder();
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"))) {
/** String line;
* Führt einen sofortigen Check aus und updated cache (synchron for the calling thread). while ((line = br.readLine()) != null) sb.append(line).append("\n");
*/ }
public void checkNow() {
try { String body = sb.toString();
plugin.getLogger().info("Prüfe StatusAPI Releases via Gitea API...");
// 1. Die LATEST Version (Tag) finden
HttpURLConnection conn = (HttpURLConnection) new URL(apiUrl).openConnection(); // Wir suchen das erste "tag_name" im gesamten JSON, das ist meistens das neueste Release.
conn.setRequestMethod("GET"); String foundVersion = null;
conn.setRequestProperty("Accept", "application/json"); Matcher tagM = TAG_NAME_PATTERN.matcher(body);
conn.setRequestProperty("User-Agent", "StatusAPI-UpdateChecker/1.0"); if (tagM.find()) {
conn.setConnectTimeout(5000); foundVersion = tagM.group(1).trim();
conn.setReadTimeout(5000); }
int code = conn.getResponseCode(); if (foundVersion == null) {
if (code != 200) { plugin.getLogger().warning("Keine Version (Tag) im Release gefunden.");
plugin.getLogger().warning("Gitea API nicht erreichbar (HTTP " + code + ")"); return;
return; }
}
// Version säubern (v vorne entfernen)
StringBuilder sb = new StringBuilder(); if (foundVersion.startsWith("v") || foundVersion.startsWith("V")) {
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"))) { foundVersion = foundVersion.substring(1);
String line; }
while ((line = br.readLine()) != null) {
sb.append(line).append("\n"); // 2. Download URL für StatusAPI.jar finden
} // Wir suchen das Asset "StatusAPI.jar"
} String foundUrl = null;
String body = sb.toString(); Pattern releasePattern = Pattern.compile("(?s)\\{.*?\\}");
Matcher releaseMatcher = releasePattern.matcher(body);
// Suche Release mit Asset "StatusAPI.jar" while (releaseMatcher.find()) {
String foundVersion = null; String block = releaseMatcher.group();
String foundUrl = null; java.util.List<String> names = new java.util.ArrayList<>();
java.util.List<String> downloads = new java.util.ArrayList<>();
// Split releases by top-level objects: we simply find repeating {...} blocks and inspect them
// This is robust enough for Gitea JSON responses. Matcher nm = ASSET_NAME_PATTERN.matcher(block);
Pattern releasePattern = Pattern.compile("(?s)\\{.*?\\}"); while (nm.find()) names.add(nm.group(1));
Matcher releaseMatcher = releasePattern.matcher(body);
while (releaseMatcher.find()) { Matcher dm = DOWNLOAD_PATTERN.matcher(block);
String block = releaseMatcher.group(); while (dm.find()) downloads.add(dm.group(1));
// extract asset names & download urls inside the block int pairs = Math.min(names.size(), downloads.size());
java.util.List<String> names = new java.util.ArrayList<>(); for (int i = 0; i < pairs; i++) {
java.util.List<String> downloads = new java.util.ArrayList<>(); String name = names.get(i);
String dl = downloads.get(i);
Matcher nm = ASSET_NAME_PATTERN.matcher(block); if ("StatusAPI.jar".equalsIgnoreCase(name.trim())) {
while (nm.find()) names.add(nm.group(1)); foundUrl = dl;
break;
Matcher dm = DOWNLOAD_PATTERN.matcher(block); }
while (dm.find()) downloads.add(dm.group(1)); }
if (foundUrl != null) break;
int pairs = Math.min(names.size(), downloads.size()); }
for (int i = 0; i < pairs; i++) {
String name = names.get(i); if (foundUrl == null) {
String dl = downloads.get(i); plugin.getLogger().warning("Keine JAR-Datei für dieses Release gefunden.");
if ("StatusAPI.jar".equalsIgnoreCase(name.trim())) { return;
// find tag_name or name field in the same block }
Matcher tagM = TAG_NAME_PATTERN.matcher(block);
if (tagM.find()) { plugin.getLogger().info("Gefundene Version: " + foundVersion + " (Aktuell: " + currentVersion + ")");
foundVersion = tagM.group(1).trim();
} else { latestVersion = foundVersion;
Matcher nameFieldM = NAME_FIELD_PATTERN.matcher(block); latestUrl = foundUrl;
if (nameFieldM.find()) {
foundVersion = nameFieldM.group(1).trim(); } catch (Exception e) {
} plugin.getLogger().log(Level.SEVERE, "Fehler beim Update-Check: " + e.getMessage(), e);
} }
foundUrl = dl; }
break;
} public String getLatestVersion() {
} return latestVersion != null ? latestVersion : "";
}
if (foundUrl != null) break;
} public String getLatestUrl() {
return latestUrl != null ? latestUrl : "";
if (foundVersion == null || foundUrl == null) { }
plugin.getLogger().info("Kein Release mit Asset 'StatusAPI.jar' gefunden.");
return; public boolean isUpdateAvailable(String currentVer) {
} String lv = getLatestVersion();
if (lv.isEmpty()) return false;
// Normalize version return compareVersions(lv, currentVer) > 0;
if (foundVersion.startsWith("v") || foundVersion.startsWith("V")) { }
foundVersion = foundVersion.substring(1);
} private int compareVersions(String a, String b) {
try {
plugin.getLogger().info("Gefundene Version: " + foundVersion + " (aktuell: " + currentVersion + ")"); String[] aa = a.split("\\.");
String[] bb = b.split("\\.");
// set cache int len = Math.max(aa.length, bb.length);
latestVersion = foundVersion; for (int i = 0; i < len; i++) {
latestUrl = foundUrl; int ai = i < aa.length ? Integer.parseInt(aa[i].replaceAll("\\D","")) : 0;
int bi = i < bb.length ? Integer.parseInt(bb[i].replaceAll("\\D","")) : 0;
} catch (Exception e) { if (ai != bi) return Integer.compare(ai, bi);
plugin.getLogger().log(Level.SEVERE, "Fehler beim Update-Check: " + e.getMessage(), e); }
} return 0;
} } catch (Exception ex) {
return a.compareTo(b);
/** }
* Gibt die zuletzt gecachte Version (oder empty string). }
*/ }
public String getLatestVersion() {
return latestVersion != null ? latestVersion : "";
}
/**
* Gibt die zuletzt gecachte download-URL (oder empty string).
*/
public String getLatestUrl() {
return latestUrl != null ? latestUrl : "";
}
/**
* Prüft ob die gecachte latestVersion größer ist als übergebene version.
*/
public boolean isUpdateAvailable(String currentVer) {
String lv = getLatestVersion();
if (lv.isEmpty()) return false;
return compareVersions(lv, currentVer) > 0;
}
/**
* Einfacher SemVer-Vergleich (1.2.3). Liefert >0 wenn a>b, 0 wenn gleich, <0 wenn a<b.
*/
private int compareVersions(String a, String b) {
try {
String[] aa = a.split("\\.");
String[] bb = b.split("\\.");
int len = Math.max(aa.length, bb.length);
for (int i = 0; i < len; i++) {
int ai = i < aa.length ? Integer.parseInt(aa[i].replaceAll("\\D","")) : 0;
int bi = i < bb.length ? Integer.parseInt(bb[i].replaceAll("\\D","")) : 0;
if (ai != bi) return Integer.compare(ai, bi);
}
return 0;
} catch (Exception ex) {
return a.compareTo(b);
}
}
/**
* Convenience: prüft cached state und falls update vorhanden, notifies online players with given permission(s).
*/
public void notifyOnlineOpsIfAvailable(String[] perms) {
String lv = getLatestVersion();
String url = getLatestUrl();
if (lv.isEmpty()) return;
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
try {
for (String perm : perms) {
if (p.hasPermission(perm)) {
p.sendMessage(new TextComponent("§6[StatusAPI] §eNeue Version verfügbar: §a" + lv));
p.sendMessage(new TextComponent("§eDownload: §b" + url));
break;
}
}
} catch (Exception ignored) {}
}
}
}
@@ -1,67 +0,0 @@
package net.viper.status;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.event.PostLoginEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.event.EventHandler;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.plugin.Plugin;
import java.util.concurrent.TimeUnit;
/**
* Listener: Benachrichtigt OP-Spieler beim Login, falls ein Update bekannt ist oder führt
* asynchronen Einzelcheck durch und benachrichtigt den Spieler danach.
*/
public class UpdateListener implements Listener {
private final Plugin plugin;
private final UpdateChecker checker;
private final String[] notifyPerms = new String[] { "statusapi.update.notify", "statusapi.notify", "bungeecord.command.alert" };
public UpdateListener(Plugin plugin, UpdateChecker checker) {
this.plugin = plugin;
this.checker = checker;
}
@EventHandler
public void onPostLogin(PostLoginEvent event) {
ProxiedPlayer player = event.getPlayer();
// Only notify players with the notify permission(s)
boolean hasPerm = false;
for (String perm : notifyPerms) {
if (player.hasPermission(perm)) { hasPerm = true; break; }
}
if (!hasPerm) return;
String currentVersion = plugin.getDescription() != null ? plugin.getDescription().getVersion() : "0.0.0";
// If we already have a cached update and it's newer -> notify immediately
if (checker.isUpdateAvailable(currentVersion)) {
String lv = checker.getLatestVersion();
String url = checker.getLatestUrl();
player.sendMessage(new TextComponent("§6[StatusAPI] §eNeue Version verfügbar: §a" + lv));
player.sendMessage(new TextComponent("§eDownload: §b" + url));
return;
}
// No cached update yet -> run an async check for this player and notify if found
ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
// perform network call
checker.checkNow();
if (checker.isUpdateAvailable(currentVersion)) {
String lv = checker.getLatestVersion();
String url = checker.getLatestUrl();
// ensure player is still online before sending message
ProxiedPlayer p = ProxyServer.getInstance().getPlayer(player.getUniqueId());
if (p != null) {
p.sendMessage(new TextComponent("§6[StatusAPI] §eNeue Version verfügbar: §a" + lv));
p.sendMessage(new TextComponent("§eDownload: §b" + url));
}
}
});
}
}
+6 -2
View File
@@ -1,9 +1,13 @@
name: StatusAPI name: StatusAPI
main: net.viper.status.StatusAPI main: net.viper.status.StatusAPI
version: 1.1 version: 3.6.2
author: M_Viper author: M_Viper
description: StatusAPI für BungeeCord inkl. Update-Checker description: StatusAPI für BungeeCord inkl. Update-Checker
softdepend:
- LuckPerms
permissions: permissions:
statusapi.update.notify: statusapi.update.notify:
description: 'Erlaubt Update-Benachrichtigungen' description: 'Erlaubt Update-Benachrichtigungen'
default: op default: op