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
const POLL_INTERVAL_SEC = 2;
// Alarm erstellen
chrome.runtime.onInstalled.addListener(() => {
chrome.alarms.create('periodicRefresh', { periodInMinutes: POLL_INTERVAL_SEC / 60 });
});
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'periodicRefresh') {
refreshAllServers().catch(console.error);
}
});
// Message Listener
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg && msg.cmd === 'refreshNow') {
refreshAllServers()
.then(statuses => sendResponse({ ok: true, statuses }))
.catch(err => sendResponse({ ok: false, error: String(err) }));
return true;
}
});
async function refreshAllServers() {
const obj = await chrome.storage.local.get('servers');
const servers = obj.servers || [];
if (servers.length === 0) {
await chrome.storage.local.set({ serverStatuses: {} });
chrome.action.setBadgeText({ text: '' });
return {};
}
const results = {};
let totalOnlinePlayers = 0;
const tasks = servers.map(async (srv) => {
try {
const data = await fetchServerStatus(srv, 2000);
results[srv.id] = { ok: true, server: srv, fetched: Date.now(), data };
if (data) {
if (Array.isArray(data.players)) totalOnlinePlayers += data.players.length;
else if (typeof data.players === 'number') totalOnlinePlayers += data.players;
}
} catch (e) {
results[srv.id] = { ok: false, server: srv, fetched: Date.now(), error: String(e) };
}
});
await Promise.all(tasks);
await chrome.storage.local.set({ serverStatuses: results });
const badgeText = totalOnlinePlayers > 0 ? String(totalOnlinePlayers) : '';
chrome.action.setBadgeText({ text: badgeText });
chrome.action.setBadgeBackgroundColor({ color: '#3b82f6' });
return results;
}
async function fetchServerStatus(server, timeoutMs = 2000) {
let serverObj = typeof server === 'string' ? { id: 'legacy', url: server } : server;
let fetchUrl = serverObj.url || '';
// 1. Direkte URL-Abfrage
if (fetchUrl) {
if (!/^https?:\/\//i.test(fetchUrl)) fetchUrl = 'http://' + fetchUrl;
try {
const startTime = performance.now();
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeoutMs);
const resp = await fetch(fetchUrl, { method: 'GET', signal: controller.signal });
clearTimeout(id);
const endTime = performance.now();
const latency = Math.round(endTime - startTime);
if (resp.ok) {
let text = await resp.text();
const lastBrace = text.lastIndexOf('}');
if (lastBrace !== -1) text = text.substring(0, lastBrace + 1);
text = text.trim();
try {
const parsed = JSON.parse(text);
if (parsed && (typeof parsed.online !== 'undefined' || Array.isArray(parsed.players) || parsed.version)) {
parsed.ping = latency; // Ping hinzufügen
return parsed;
}
} catch (e) {
// Parsing fehlgeschlagen → Fallback-Objekt mit Ping
return {
online: false,
players: [],
max_players: 0,
version: 'unknown',
motd: '',
ping: latency > timeoutMs ? null : latency // bei Timeout kein sinnvoller Ping
};
}
}
// HTTP nicht ok → Offline mit gemessener Latenz (falls unter Timeout)
return {
online: false,
players: [],
max_players: 0,
version: 'unknown',
motd: '',
ping: latency > timeoutMs ? null : latency
};
} catch (e) {
// Timeout oder Netzwerkfehler
return {
online: false,
players: [],
max_players: 0,
version: 'unknown',
motd: '',
ping: null
};
}
}
// 2. WordPress AJAX-Abfrage
const wpSite = serverObj.wpSite ? String(serverObj.wpSite).replace(/\/$/, '') : null;
const wpServerId = serverObj.wpServerId ? String(serverObj.wpServerId) : null;
if (wpSite && wpServerId) {
try {
const ajaxUrl = wpSite + '/wp-admin/admin-ajax.php';
const body = 'action=mcss_fetch&server_id=' + encodeURIComponent(wpServerId);
const startTime = performance.now();
const controller2 = new AbortController();
const id2 = setTimeout(() => controller2.abort(), timeoutMs);
const resp2 = await fetch(ajaxUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
signal: controller2.signal
});
clearTimeout(id2);
const endTime = performance.now();
const latency = Math.round(endTime - startTime);
if (!resp2.ok) throw new Error('WP AJAX HTTP ' + resp2.status);
const json = await resp2.json();
if (json && (typeof json.online !== 'undefined' || Array.isArray(json.players) || json.version)) {
json.ping = latency; // Ping hinzufügen
return json;
}
// JSON nicht im erwarteten Format
return {
online: false,
players: [],
max_players: 0,
version: 'unknown',
motd: '',
ping: latency > timeoutMs ? null : latency
};
} catch (e) {
return {
online: false,
players: [],
max_players: 0,
version: 'unknown',
motd: '',
ping: null
};
}
}
// Keine gültige Konfiguration
return {
online: false,
players: [],
max_players: 0,
version: 'unknown',
motd: '',
ping: null
};
// background.js
const POLL_INTERVAL_SEC = 2;
// Alarm erstellen
chrome.runtime.onInstalled.addListener(() => {
chrome.alarms.create('periodicRefresh', { periodInMinutes: POLL_INTERVAL_SEC / 60 });
});
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'periodicRefresh') {
refreshAllServers().catch(console.error);
}
});
// Message Listener
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg && msg.cmd === 'refreshNow') {
refreshAllServers()
.then(statuses => sendResponse({ ok: true, statuses }))
.catch(err => sendResponse({ ok: false, error: String(err) }));
return true;
}
});
async function refreshAllServers() {
const obj = await chrome.storage.local.get('servers');
const servers = obj.servers || [];
if (servers.length === 0) {
await chrome.storage.local.set({ serverStatuses: {} });
chrome.action.setBadgeText({ text: '' });
return {};
}
const results = {};
let totalOnlinePlayers = 0;
const tasks = servers.map(async (srv) => {
try {
const data = await fetchServerStatus(srv, 2000);
results[srv.id] = { ok: true, server: srv, fetched: Date.now(), data };
if (data) {
if (Array.isArray(data.players)) totalOnlinePlayers += data.players.length;
else if (typeof data.players === 'number') totalOnlinePlayers += data.players;
}
} catch (e) {
results[srv.id] = { ok: false, server: srv, fetched: Date.now(), error: String(e) };
}
});
await Promise.all(tasks);
await chrome.storage.local.set({ serverStatuses: results });
const badgeText = totalOnlinePlayers > 0 ? String(totalOnlinePlayers) : '';
chrome.action.setBadgeText({ text: badgeText });
chrome.action.setBadgeBackgroundColor({ color: '#3b82f6' });
return results;
}
async function fetchServerStatus(server, timeoutMs = 2000) {
let serverObj = typeof server === 'string' ? { id: 'legacy', url: server } : server;
let fetchUrl = serverObj.url || '';
// 1. Direkte URL-Abfrage
if (fetchUrl) {
if (!/^https?:\/\//i.test(fetchUrl)) fetchUrl = 'http://' + fetchUrl;
try {
const startTime = performance.now();
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeoutMs);
const resp = await fetch(fetchUrl, { method: 'GET', signal: controller.signal });
clearTimeout(id);
const endTime = performance.now();
const latency = Math.round(endTime - startTime);
if (resp.ok) {
let text = await resp.text();
const lastBrace = text.lastIndexOf('}');
if (lastBrace !== -1) text = text.substring(0, lastBrace + 1);
text = text.trim();
try {
const parsed = JSON.parse(text);
if (parsed && (typeof parsed.online !== 'undefined' || Array.isArray(parsed.players) || parsed.version)) {
parsed.ping = latency; // Ping hinzufügen
return parsed;
}
} catch (e) {
// Parsing fehlgeschlagen → Fallback-Objekt mit Ping
return {
online: false,
players: [],
max_players: 0,
version: 'unknown',
motd: '',
ping: latency > timeoutMs ? null : latency // bei Timeout kein sinnvoller Ping
};
}
}
// HTTP nicht ok → Offline mit gemessener Latenz (falls unter Timeout)
return {
online: false,
players: [],
max_players: 0,
version: 'unknown',
motd: '',
ping: latency > timeoutMs ? null : latency
};
} catch (e) {
// Timeout oder Netzwerkfehler
return {
online: false,
players: [],
max_players: 0,
version: 'unknown',
motd: '',
ping: null
};
}
}
// 2. WordPress AJAX-Abfrage
const wpSite = serverObj.wpSite ? String(serverObj.wpSite).replace(/\/$/, '') : null;
const wpServerId = serverObj.wpServerId ? String(serverObj.wpServerId) : null;
if (wpSite && wpServerId) {
try {
const ajaxUrl = wpSite + '/wp-admin/admin-ajax.php';
const body = 'action=mcss_fetch&server_id=' + encodeURIComponent(wpServerId);
const startTime = performance.now();
const controller2 = new AbortController();
const id2 = setTimeout(() => controller2.abort(), timeoutMs);
const resp2 = await fetch(ajaxUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
signal: controller2.signal
});
clearTimeout(id2);
const endTime = performance.now();
const latency = Math.round(endTime - startTime);
if (!resp2.ok) throw new Error('WP AJAX HTTP ' + resp2.status);
const json = await resp2.json();
if (json && (typeof json.online !== 'undefined' || Array.isArray(json.players) || json.version)) {
json.ping = latency; // Ping hinzufügen
return json;
}
// JSON nicht im erwarteten Format
return {
online: false,
players: [],
max_players: 0,
version: 'unknown',
motd: '',
ping: latency > timeoutMs ? null : latency
};
} catch (e) {
return {
online: false,
players: [],
max_players: 0,
version: 'unknown',
motd: '',
ping: null
};
}
}
// Keine gültige Konfiguration
return {
online: false,
players: [],
max_players: 0,
version: 'unknown',
motd: '',
ping: null
};
}
+1 -1
View File
@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "Minecraft Bungee Status",
"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": {
"default_popup": "popup.html",
"default_icon": {
+435 -220
View File
@@ -1,220 +1,435 @@
:root {
--bg: #111827;
--panel: #0b1220;
--text: #e6eef8;
--muted: #9aa7b3;
--accent: #3b82f6;
--danger: #ef4444;
--radius: 8px;
--online: #22c55e;
--offline: #ef4444;
}
* { box-sizing:border-box; }
html,body { margin:0; padding:0; font-family:Inter, Arial, sans-serif; background:var(--bg); color:var(--text);}
.root { width:520px; max-width:520px; padding:12px; }
header { display:flex; align-items:center; justify-content:space-between; margin-bottom:8px; }
header h1 { font-size:16px; margin:0; }
header .actions button { background:transparent; border:1px solid var(--muted); color:var(--text); padding:4px 8px; border-radius:6px; cursor:pointer; }
.main {
display:flex;
gap:12px;
position: relative;
}
.server-list {
width:45%;
background:var(--panel);
padding:8px;
border-radius:var(--radius);
flex-shrink: 0;
}
.detail {
flex:1;
background:var(--panel);
padding:8px;
border-radius:var(--radius);
min-height:220px;
transition: all 0.3s ease;
}
.detail.full-width {
width: 100%;
flex: 1 1 100%;
}
.add-form { display:flex; flex-direction:column; gap:6px; margin-bottom:8px; }
.add-form input { padding:6px 8px; border-radius:6px; border:1px solid #21303b; background:#071019; color:var(--text); }
.add-form button { padding:6px 8px; border-radius:6px; border:none; background:var(--accent); color:white; cursor:pointer; }
#serversContainer { list-style:none; margin:0; padding:0; max-height:260px; overflow:auto; }
.server-item {
display:flex;
align-items:center;
justify-content:space-between;
padding:6px;
margin-bottom:6px;
border-radius:6px;
cursor:pointer;
background: rgba(255,255,255,0.02);
transition: background 0.2s ease;
}
.server-item:hover { background: rgba(255,255,255,0.05); }
.server-item .meta { display:flex; flex-direction:column; }
.server-item .meta .name { font-weight:600; }
.server-item .meta .url { font-size:12px; color:var(--muted); }
.server-item .status-bubble {
font-size:12px;
padding:3px 6px;
border-radius:999px;
color:#fff;
transition: background-color 0.3s ease;
}
.placeholder { color:var(--muted); padding:12px; }
.hidden { display:none; }
/* --- Detail Header (Zeile 1) --- */
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.server-identity h2 {
margin: 0;
font-size: 18px;
line-height: 1.2;
}
.server-url {
display: block;
font-size: 12px;
color: var(--muted);
margin-top: 2px;
}
.status-wrapper {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
}
/* --- Pulsierender Punkt Animation --- */
.pulsing-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: var(--offline); /* Standard Offline Rot */
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7);
animation: pulse-red 2s infinite;
}
.pulsing-dot.online {
background-color: var(--online); /* Online Grün */
animation: pulse-green 2s infinite;
}
@keyframes pulse-green {
0% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.7); }
70% { box-shadow: 0 0 0 10px rgba(34, 197, 94, 0); }
100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); }
}
@keyframes pulse-red {
0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); }
70% { box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); }
100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); }
}
/* --- Detail Stats (Zeile 2) --- */
.detail-stats {
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(255,255,255,0.05);
padding: 8px;
border-radius: var(--radius);
margin-bottom: 16px;
font-size: 13px;
}
/* ---------------------------------- */
.playerList {
list-style:none;
padding-left:16px;
max-height:140px;
overflow:auto;
margin:6px 0;
display:flex;
flex-wrap:wrap;
gap:8px;
}
.playerList li {
display:flex;
align-items:center;
gap:4px;
font-size:13px;
color:var(--text);
}
.player-avatar {
width:24px;
height:24px;
border-radius:4px;
transition: transform 0.2s ease;
}
.player-avatar:hover { transform: scale(1.1); }
.detailButtons {
display:flex;
gap:8px;
margin-top:8px;
transition: opacity 0.3s ease;
}
.detailButtons button {
padding:6px 8px;
border-radius:6px;
border:none;
cursor:pointer;
transition: opacity 0.2s ease, transform 0.1s ease;
}
.detailButtons button:hover { opacity: 0.9; transform: translateY(-1px); }
.detailButtons button:active { transform: translateY(0); }
#btnEdit { background: #f59e0b; color:#000; }
#btnDelete { background: var(--danger); color:#fff; }
#serversContainer::-webkit-scrollbar,
.playerList::-webkit-scrollbar {
width: 6px;
}
#serversContainer::-webkit-scrollbar-track,
.playerList::-webkit-scrollbar-track {
background: rgba(255,255,255,0.05);
border-radius: 3px;
}
#serversContainer::-webkit-scrollbar-thumb,
.playerList::-webkit-scrollbar-thumb {
background: var(--muted);
border-radius: 3px;
}
#serversContainer::-webkit-scrollbar-thumb:hover,
.playerList::-webkit-scrollbar-thumb:hover {
background: var(--accent);
}
:root {
--bg: #0d1117;
--panel: #161b27;
--panel2: #111620;
--text: #e6eef8;
--muted: #5a6a7a;
--muted2: #8899aa;
--accent: #3b82f6;
--radius: 9px;
--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);
overflow-x: hidden;
}
.root {
width: 560px;
display: flex;
flex-direction: column;
}
/* ── Header ── */
header {
background: var(--panel);
display: flex;
align-items: center;
justify-content: space-between;
padding: 13px 20px;
border-bottom: 1px solid var(--border);
}
header h1 {
font-size: 14px;
font-weight: 700;
margin: 0;
letter-spacing: 0.03em;
color: #fff;
}
header .actions { display: flex; gap: 7px; }
header .actions button {
background: rgba(255,255,255,0.07);
border: 1px solid rgba(255,255,255,0.1);
color: var(--muted2);
border-radius: 7px;
width: 32px; height: 32px;
cursor: pointer;
font-size: 16px;
display: flex; align-items: center; justify-content: center;
transition: background 0.15s, color 0.15s;
}
header .actions button:hover {
background: rgba(255,255,255,0.12);
color: #fff;
}
/* ── Main layout ── */
.main {
display: flex;
flex-direction: column;
}
/* ── Settings / add-form panel ── */
.settings-panel {
background: var(--panel2);
border-bottom: 1px solid var(--border);
padding: 14px 20px;
display: flex;
flex-direction: column;
gap: 8px;
}
.settings-panel input {
padding: 8px 12px;
border-radius: 7px;
border: 1px solid rgba(255,255,255,0.09);
background: rgba(0,0,0,0.3);
color: var(--text);
font-size: 13px;
outline: none;
}
.settings-panel input:focus {
border-color: var(--accent);
}
.settings-panel button {
padding: 8px 12px;
border-radius: 7px;
border: none;
background: var(--accent);
color: white;
font-size: 13px;
font-weight: 600;
cursor: pointer;
}
/* ── Server list ── */
.server-list-panel {
background: var(--panel2);
border-bottom: 1px solid var(--border);
padding: 10px 12px;
}
#serversContainer {
list-style: none;
margin: 0; padding: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.server-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 9px 12px;
border-radius: 8px;
cursor: pointer;
background: transparent;
border: 1px solid transparent;
transition: background 0.15s, border-color 0.15s;
}
.server-item:hover { background: rgba(255,255,255,0.04); }
.server-item.selected { background: rgba(59,130,246,0.08); border-color: rgba(59,130,246,0.25); }
.server-item .meta { display: flex; flex-direction: column; gap: 2px; }
.server-item .meta .name { font-size: 13px; font-weight: 600; color: #fff; }
.server-item .meta .url { font-size: 11px; color: var(--muted); }
.status-bubble {
font-size: 11px;
font-weight: 600;
padding: 3px 10px;
border-radius: 999px;
color: #fff;
background: transparent;
}
/* ── Detail panel ── */
.detail-panel {
padding: 18px 20px;
display: flex;
flex-direction: column;
gap: 16px;
min-height: 200px;
}
.placeholder {
color: var(--muted);
font-size: 13px;
padding: 24px 0;
text-align: center;
}
/* Server identity + online badge */
.srv-identity {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.srv-name { font-size: 20px; font-weight: 700; color: #fff; margin: 0 0 4px; }
.srv-url { font-size: 12px; color: var(--muted); }
.online-badge {
display: flex;
align-items: center;
gap: 7px;
border-radius: 999px;
padding: 5px 14px;
font-size: 13px;
font-weight: 600;
}
.online-badge.online {
background: rgba(34,197,94,0.12);
border: 1px solid rgba(34,197,94,0.25);
color: #4ade80;
}
.online-badge.offline {
background: rgba(239,68,68,0.1);
border: 1px solid rgba(239,68,68,0.2);
color: #f87171;
}
.pulsing-dot {
width: 7px; height: 7px;
border-radius: 50%;
flex-shrink: 0;
}
.pulsing-dot.online {
background: var(--online);
animation: pulse-green 2s infinite;
}
.pulsing-dot.offline {
background: var(--offline);
animation: pulse-red 2s infinite;
}
@keyframes pulse-green {
0%,100% { box-shadow: 0 0 0 0 rgba(34,197,94,0.5); }
50% { box-shadow: 0 0 0 5px rgba(34,197,94,0); }
}
@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>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Bungee Status</title>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div class="root">
<header>
<h1>Bungee Status</h1>
<div class="actions">
<button id="btnRefresh" title="Jetzt aktualisieren">↻</button>
<button id="btnToggleSettings">⚙</button>
</div>
</header>
<section class="main">
<aside class="server-list">
<div class="add-form" id="settingsForm">
<input id="inputName" placeholder="Name (z. B. Lobby)" />
<input id="inputUrl" placeholder="URL (127.0.0.1:9191)" />
<input id="inputWpSite" placeholder="WP Site" />
<input id="inputWpServerId" placeholder="WP Server ID" />
<button id="btnAddServer">Hinzufügen</button>
</div>
<ul id="serversContainer"></ul>
</aside>
<div class="detail">
<div id="noSelection" class="placeholder">Wähle einen Server aus der linken Liste.</div>
<div id="detailContent" class="detailContent hidden">
<!-- NEUES LAYOUT ZEILE 1: Name, URL, Status (mit Pulsierendem Punkt) -->
<div class="detail-header">
<div class="server-identity">
<h2 id="detailName"></h2>
<span id="detailUrlText" class="server-url"></span>
</div>
<div class="status-wrapper">
<div id="detailPulse" class="pulsing-dot"></div>
<span id="detailStatus"></span>
</div>
</div>
<!-- NEUES LAYOUT ZEILE 2: Spieler, Version, Ping -->
<div class="detail-stats">
<div><strong>Spieler:</strong> <span id="detailPlayers"></span></div>
<div><strong>Version:</strong> <span id="detailVersion"></span></div>
<div><strong>Ping:</strong> <span id="detailPing">-</span></div>
</div>
<h3>Online Spieler</h3>
<ul id="detailPlayerList" class="playerList"></ul>
<div class="detailButtons">
<button id="btnEdit">Bearbeiten</button>
<button id="btnDelete">Löschen</button>
</div>
</div>
</div>
</section>
</div>
<script src="popup.js"></script>
</body>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Bungee Status</title>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div class="root">
<header>
<h1>Bungee Status</h1>
<div class="actions">
<button id="btnRefresh" title="Jetzt aktualisieren">↻</button>
<button id="btnToggleSettings" title="Einstellungen">⚙</button>
</div>
</header>
<div class="main">
<div id="settingsForm" class="settings-panel hidden">
<h3 id="settingsTitle">Neuen Server hinzufügen</h3>
<input id="inputName" placeholder="Name (z. B. Lobby)" />
<input id="inputUrl" placeholder="URL (z. B. 192.168.1.1:9191)" />
<input id="inputWpSite" placeholder="WP Site (z. B. https://meine-seite.de)" />
<input id="inputWpServerId" placeholder="WP Server ID" />
<button id="btnAddServer">Hinzufügen</button>
</div>
<div id="serverListPanel" class="server-list-panel hidden">
<ul id="serversContainer"></ul>
</div>
<div class="detail-panel">
<div id="noSelection" class="placeholder">Noch kein Server ausgewählt.</div>
<div id="detailContent" class="hidden">
<!-- Server Name + Status Badge -->
<div class="srv-identity">
<div>
<div class="srv-name" id="detailName"></div>
<div class="srv-url" id="detailUrlText"></div>
</div>
<div class="online-badge offline" id="statusBadge">
<div class="pulsing-dot offline" id="detailPulse"></div>
<span id="detailStatus">Offline</span>
</div>
</div>
<!-- MOTD -->
<div id="motdBar" class="motd-bar hidden"></div>
<!-- Stats: Spieler / Version / Ping -->
<div class="stats-row">
<div class="stat-card">
<div class="stat-label">Spieler</div>
<div class="stat-val" id="detailPlayers">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Version</div>
<div class="stat-val small" id="detailVersion">-</div>
</div>
<div class="stat-card">
<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>
+312 -266
View File
@@ -1,377 +1,423 @@
// popup.js
const $ = id => document.getElementById(id);
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 selectedId = null;
let statuses = {};
let previousStatuses = {};
let previousStatuses = {};
let settingsVisible = false;
let editingId = null; // Welcher Server gerade bearbeitet wird
document.addEventListener('DOMContentLoaded', init);
btnAdd.addEventListener('click', handleAdd);
btnRefresh.addEventListener('click', manualRefresh);
btnToggleSettings.addEventListener('click', toggleSettings);
btnEdit.addEventListener('click', handleEdit);
btnDelete.addEventListener('click', handleDelete);
$('btnAdd')?.addEventListener('click', handleAdd);
$('btnAddServer')?.addEventListener('click', handleAdd);
$('btnRefresh').addEventListener('click', manualRefresh);
$('btnToggleSettings').addEventListener('click', toggleSettings);
$('btnEdit').addEventListener('click', handleEdit);
$('btnDelete').addEventListener('click', handleDelete);
async function init() {
await loadServersFromStorage();
await loadStatusesFromStorage();
await loadSettingsVisibility();
renderServerList();
if (servers.length === 1) selectedId = servers[0].id;
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() {
const obj = await chrome.storage.local.get(['settingsVisible']);
settingsVisible = obj.settingsVisible !== undefined ? obj.settingsVisible : false;
if (!settingsVisible) settingsForm.classList.add('hidden');
else settingsForm.classList.remove('hidden');
settingsVisible = !!obj.settingsVisible;
applySettingsVisibility();
}
async function saveSettingsVisibility() {
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() {
settingsVisible = !settingsVisible;
settingsForm.classList.toggle('hidden');
applySettingsVisibility();
await saveSettingsVisibility();
adjustDetailLayout();
}
// --- Storage ---
async function loadServersFromStorage() {
const obj = await chrome.storage.local.get(['servers']);
servers = obj.servers || [];
for (const s of servers) if (!s.id) s.id = uid();
await chrome.storage.local.set({ servers });
}
async function saveServersToStorage() {
await chrome.storage.local.set({ servers });
async function saveServersToStorage() {
await chrome.storage.local.set({ servers });
}
async function loadStatusesFromStorage() {
const obj = await chrome.storage.local.get(['serverStatuses']);
statuses = obj.serverStatuses || {};
}
// --- Layout ---
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 ---
// ── Server list ──
function renderServerList() {
serversContainer.innerHTML = '';
const container = $('serversContainer');
container.innerHTML = '';
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;
}
for (const s of servers) {
const item = document.createElement('li');
item.className = 'server-item';
item.dataset.id = s.id;
const li = document.createElement('li');
li.className = 'server-item' + (s.id === selectedId ? ' selected' : '');
li.dataset.id = s.id;
const meta = document.createElement('div');
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');
statusBubble.className = 'status-bubble';
statusBubble.textContent = '—';
statusBubble.style.backgroundColor = 'transparent';
item.statusBubble = statusBubble;
const nameEl = document.createElement('div');
nameEl.className = 'name';
nameEl.textContent = s.name || '(Kein Name)';
item.append(meta, statusBubble);
item.addEventListener('click', () => {
selectedId = s.id;
renderDetail(selectedId);
let urlText = s.url || (s.wpSite || '');
urlText = urlText.replace(':9191', '');
const urlEl = document.createElement('div');
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);
});
serversContainer.appendChild(item);
container.appendChild(li);
}
updateServerListStatuses(false);
updateServerListStatuses();
}
// --- Render Detail ---
// ── Detail ──
function renderDetail(id) {
if (!id) {
noSelection.classList.remove('hidden');
detailContent.classList.add('hidden');
return;
if (!id) {
$('noSelection').classList.remove('hidden');
$('detailContent').classList.add('hidden');
return;
}
const srv = servers.find(x => x.id === id);
if (!srv) {
noSelection.classList.remove('hidden');
detailContent.classList.add('hidden');
return;
if (!srv) {
$('noSelection').classList.remove('hidden');
$('detailContent').classList.add('hidden');
return;
}
noSelection.classList.add('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;
$('noSelection').classList.add('hidden');
$('detailContent').classList.remove('hidden');
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) {
const st = statuses[srv.id];
const prevSt = previousStatuses[srv.id];
const statusChanged = force ||
!prevSt ||
(prevSt.ok !== st?.ok) ||
(prevSt.data?.online !== st?.data?.online);
const changed = force || JSON.stringify(st) !== JSON.stringify(prevSt);
if (!changed) return;
const badge = $('statusBadge');
const pulse = $('detailPulse');
if (!st || !st.ok || !st.data) {
if (statusChanged) {
detailStatus.textContent = 'Offline';
detailPulse.classList.remove('online');
detailVersion.textContent = '-';
detailPlayers.textContent = '-';
detailPing.textContent = '-';
updatePlayerList([]);
}
badge.className = 'online-badge offline';
pulse.className = 'pulsing-dot offline';
$('detailStatus').textContent = 'Offline';
$('detailVersion').textContent = '-';
$('detailPlayers').textContent = '-';
$('detailPing').textContent = '-';
$('motdBar').classList.add('hidden');
updatePlayerList([]);
updateBackendServers(null);
} else {
const d = st.data;
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 isOnline = !!d.online;
const playersCount = Array.isArray(d.players) ? d.players.length : (typeof d.players === 'number' ? d.players : 0);
const maxPlayers = d.max_players;
let newPlayersText = String(playersCount);
if (maxPlayers && maxPlayers !== '-1') {
newPlayersText += ` / ${maxPlayers}`;
}
badge.className = 'online-badge ' + (isOnline ? 'online' : 'offline');
pulse.className = 'pulsing-dot ' + (isOnline ? 'online' : 'offline');
$('detailStatus').textContent = isOnline ? 'Online' : 'Offline';
$('detailVersion').textContent = d.version || 'unknown';
if (force || detailPlayers.textContent !== newPlayersText) {
detailPlayers.textContent = newPlayersText;
}
const count = Array.isArray(d.players) ? d.players.length
: (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 || '-';
if (pingVal !== '-' && typeof pingVal === 'number') {
pingVal = pingVal + ' ms';
}
if (force || (pingVal !== '-' && detailPing.textContent !== pingVal)) {
detailPing.textContent = pingVal;
const ping = d.ping || d.latency;
$('detailPing').textContent = typeof ping === 'number' ? ping + ' ms' : '-';
const motdRaw = d.motd || (d.network && d.network.motd) || '';
if (motdRaw) {
const motdEl = $('motdBar');
motdEl.innerHTML = parseMinecraftColors(motdRaw);
motdEl.classList.remove('hidden');
} else {
$('motdBar').classList.add('hidden');
}
const currentPlayers = Array.isArray(d.players) ? d.players : [];
const prevPlayers = (prevSt && Array.isArray(prevSt.data?.players)) ? prevSt.data.players : [];
let playersChanged = false;
if (force) {
playersChanged = true;
} else {
playersChanged = JSON.stringify(currentPlayers) !== JSON.stringify(prevPlayers);
}
if (playersChanged) {
updatePlayerList(currentPlayers);
}
updatePlayerList(currentPlayers);
updateBackendServers(d.network?.backend_servers || null);
}
previousStatuses[srv.id] = st ? JSON.parse(JSON.stringify(st)) : null;
}
// --- Spielerliste ---
function updatePlayerList(players) {
detailPlayerList.innerHTML = '';
if (!players || players.length === 0) {
detailPlayerList.innerHTML = '<li class="placeholder">Keine Spieler online.</li>';
// ── Sub-Server chips ──
function updateBackendServers(list) {
const section = $('backendSection');
const container = $('backendServersList');
if (!list || list.length === 0) {
section.classList.add('hidden');
container.innerHTML = '';
return;
}
for (const p of players) {
const li = document.createElement('li');
const name = typeof p === 'object' ? p.name || p.username || p.player || '' : String(p);
if (typeof p === 'object' && p.avatar) {
const img = document.createElement('img');
img.src = p.avatar;
img.className = 'player-avatar';
img.title = name;
li.appendChild(img);
} else {
// Falls kein Avatar vorhanden, generiere einen von mc-heads.net
const img = document.createElement('img');
img.src = `https://mc-heads.net/avatar/${encodeURIComponent(name)}/32`;
img.className = 'player-avatar';
img.title = name;
li.appendChild(img);
}
detailPlayerList.appendChild(li);
section.classList.remove('hidden');
container.innerHTML = '';
for (const bs of list) {
const count = bs.online_players || 0;
const chip = document.createElement('div');
chip.className = 'sub-chip' + (count > 0 ? ' active' : '');
const dot = document.createElement('span');
dot.className = 'sub-chip-dot';
const nameEl = document.createElement('span');
nameEl.className = 'sub-chip-name';
nameEl.textContent = bs.name;
const countEl = document.createElement('span');
countEl.className = 'sub-chip-count';
countEl.textContent = count > 0 ? count : 'leer';
chip.append(dot, nameEl, countEl);
container.appendChild(chip);
}
}
// --- Update Server List Statuses ---
function updateServerListStatuses() {
const items = serversContainer.querySelectorAll('.server-item');
items.forEach(item => {
const s = servers.find(x => x.id === item.dataset.id);
if (!s) return;
const st = statuses[s.id];
const prevSt = previousStatuses[s.id];
const statusChanged = !prevSt ||
(prevSt.ok !== st?.ok) ||
(prevSt.data?.online !== st?.data?.online);
if (!statusChanged) return;
if (!st || !st.ok || !st.data) {
item.statusBubble.textContent = 'Offline';
item.statusBubble.style.backgroundColor = 'var(--offline)';
} else if (st.data.online) {
item.statusBubble.textContent = 'Online';
item.statusBubble.style.backgroundColor = 'var(--online)';
// ── Player list ──
function updatePlayerList(players) {
const grid = $('detailPlayerList');
grid.innerHTML = '';
if (!players || players.length === 0) {
grid.innerHTML = '<div class="placeholder">Keine Spieler online.</div>';
return;
}
for (const p of players) {
const card = document.createElement('div');
card.className = 'player-card';
let name = '', uuid = null, prefix = '', serverName = null;
if (typeof p === 'object') {
name = p.name || p.username || p.player || '';
uuid = p.uuid || null;
prefix = p.prefix || p.group || '';
serverName = p.server || p.current_server || p.connected_server || null;
} else {
item.statusBubble.textContent = 'Offline';
item.statusBubble.style.backgroundColor = 'var(--offline)';
name = String(p);
}
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() {
const name = inputName.value.trim();
const url = inputUrl.value.trim();
const wpSite = inputWpSite.value.trim();
const wpServerId = inputWpServerId.value.trim();
if (!url && !wpSite) {
alert('Bitte URL oder WP Site angeben');
return;
const name = $('inputName').value.trim();
const url = $('inputUrl').value.trim();
const wpSite = $('inputWpSite').value.trim();
const wpServerId = $('inputWpServerId').value.trim();
if (!url && !wpSite) {
alert('Bitte eine URL oder WordPress-Einstellungen angeben.');
return;
}
const s = {
id: uid(),
name: name || url || wpSite,
url: url || null,
wpSite: wpSite || null,
wpServerId: wpServerId || null
};
servers.push(s);
if (editingId) {
// === BEARBEITEN ===
const srv = servers.find(s => s.id === editingId);
if (srv) {
srv.name = name || url || wpSite;
if (url) srv.url = url;
if (wpSite) srv.wpSite = wpSite;
if (wpServerId) srv.wpServerId = wpServerId;
}
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();
inputName.value = '';
inputUrl.value = '';
inputWpSite.value = '';
inputWpServerId.value = '';
renderServerList();
if (selectedId) renderDetail(selectedId);
}
async function handleEdit() {
if (!selectedId) return;
const srv = servers.find(s => s.id === selectedId);
if (!selectedId) return;
const srv = servers.find(s => s.id === selectedId);
if (!srv) return;
const newName = prompt('Name:', srv.name) || srv.name;
const newUrl = prompt('URL:', srv.url || '') || srv.url;
const newWpSite = prompt('WP Site:', srv.wpSite || '') || srv.wpSite;
const newWpServerId = prompt('WP Server ID:', srv.wpServerId || '') || srv.wpServerId;
srv.name = newName.trim();
srv.url = newUrl.trim();
srv.wpSite = newWpSite.trim();
srv.wpServerId = newWpServerId.trim();
await saveServersToStorage();
renderServerList();
renderDetail(selectedId);
settingsVisible = true;
applySettingsVisibility();
await saveSettingsVisibility();
enterEditMode(srv);
}
async function handleDelete() {
if (!selectedId) return;
if (!confirm('Server wirklich löschen?')) return;
servers = servers.filter(s => s.id !== selectedId);
if (!selectedId || !confirm('Server wirklich löschen?')) return;
servers = servers.filter(s => s.id !== selectedId);
selectedId = null;
await saveServersToStorage();
await saveServersToStorage();
renderServerList();
renderDetail(null);
exitEditMode();
}
async function manualRefresh() {
try {
chrome.runtime.sendMessage({ cmd: 'refreshNow' });
} catch(e) {
console.error('Refresh fehlgeschlagen:', e);
}
async function manualRefresh() {
try { chrome.runtime.sendMessage({ cmd: 'refreshNow' }); } catch(e) {}
}
// --- Storage Listener ---
// ── Live updates from background ──
chrome.storage.onChanged.addListener((changes, area) => {
if (area === 'local' && changes.serverStatuses) {
statuses = changes.serverStatuses.newValue || {};
@@ -1,12 +1,21 @@
<?php
/*
* Plugin Name: Minecraft BungeeCord Status – Network Edition
* Description: Der ultimative Live-Status für dein BungeeCord Netzwerk (Border None Fix).
* Tags: minecraft, bungeecord, server status, player list
* Version: 3.6.1
* Author: M_Viper
* Requires at least: 6.0
* Requires PHP: 7.4
Plugin Name: Minecraft BungeeCord Status – Network Edition
Plugin URI:https://git.viper.ipv64.net/M_Viper/Minecraft-BungeeCord-Status
Description: Der ultimative Live-Status für dein BungeeCord Netzwerk.
Version: 3.6.6
Author: M_Viper
Author URI: https://m-viper.de
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;
@@ -59,144 +68,133 @@ function mcss_format_minecraft_colors($text) {
return $formatted;
}
/* ---------------- AUTO UPDATE (MCSS) ---------------- */
if ( ! class_exists( 'MCSS_Auto_Update' ) ) {
class MCSS_Auto_Update {
private $plugin_file;
private $repo_owner = 'M_Viper';
private $repo_name = 'Minecraft-BungeeCord-Status';
private $api_url;
private $transient_key;
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' ) );
}
/* ---------------- HELPER: 3D AVATAR URL ---------------- */
function mcss_get_3d_avatar($name, $uuid = null) {
// Bedrock-Spieler erkennen (Name enthält Punkt oder UUID beginnt mit xuid)
$is_bedrock = strpos($name, '.') !== false || ($uuid && strpos($uuid, 'xuid') === 0);
if ($is_bedrock) {
// Bedrock: mc-heads.net mit 3D Head (nutze UUID falls vorhanden)
if ($uuid && !empty($uuid)) {
return 'https://mc-heads.net/head/' . rawurlencode($uuid) . '/100';
}
return 'https://mc-heads.net/head/' . rawurlencode($name) . '/100';
} else {
// Java: mc-heads.net mit 3D Head
return 'https://mc-heads.net/head/' . rawurlencode($name) . '/100';
}
}
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 );
}
$current = get_file_data( $this->plugin_file, array( 'Version' => 'Version' ), 'plugin' );
$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 '';
} else {
set_transient( $transient_key, [], HOUR_IN_SECONDS );
}
}
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 ---------------- */
add_action('admin_enqueue_scripts', function($hook){
global $pagenow;
@@ -499,25 +497,89 @@ function mcss_render_row($srv, $i, $font_sizes, $announcement_types) {
}
/* ---------------- 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);
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 .= "Connection: Close\r\n\r\n";
fwrite($socket, $out);
$out .= "User-Agent: MCSS-WordPress/3.6.6\r\n";
$out .= "Accept: application/json\r\n";
$out .= "Connection: close\r\n\r\n";
if (@fwrite($socket, $out) === false) {
@fclose($socket);
return false;
}
$response = '';
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);
$body = end($parts);
$data = json_decode($body, true);
return $data;
$meta = stream_get_meta_data($socket);
@fclose($socket);
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) {
@@ -528,16 +590,26 @@ function mcss_parse_bungeecord_version($raw_version) {
}
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);
if ($cached !== false) return $cached;
$host = $srv['host'];
$port = $srv['player_port'] ?? 9191;
$api_data = mcss_fetch_data_via_api($host, $port, 1);
$api_data = mcss_fetch_data_via_api($host, $port, 2);
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'];
}
@@ -547,10 +619,12 @@ function mcss_fetch_server_with_ranks($srv) {
if (is_array($player_data)) {
$name = $player_data['name'];
$prefix = $player_data['prefix'] ?? '';
$uuid = $player_data['uuid'] ?? null;
} else {
// Fallback für alte API
$name = $player_data;
$prefix = '';
$uuid = null;
}
// 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)
$name_html = '<span style="color:black;">' . esc_html($name) . '</span>';
// 3. Zusammenbauen
$display_html = trim($prefix_html) . ' ' . $name_html;
// 3. Zusammenbauen (Nur Abstand, wenn Prefix existiert)
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[] = [
'name' => $name,
'avatar' => "https://mc-heads.net/avatar/" . rawurlencode($name) . "/64",
'uuid' => $uuid,
'avatar' => $avatar_url,
'prefix' => $prefix,
'display_html' => $display_html,
'rank' => $prefix ?: 'Spieler',
@@ -575,7 +657,7 @@ function mcss_fetch_server_with_ranks($srv) {
$clean_version = mcss_parse_bungeecord_version($api_data['version'] ?? 'BungeeCord');
$result = [
'online' => true,
'online' => !empty($api_data['online']),
'players' => $players_info,
'version' => $clean_version,
'ping' => 1,
@@ -584,6 +666,9 @@ function mcss_fetch_server_with_ranks($srv) {
$fast_cache_ttl = max(2, absint($srv['cache_ttl'] ?? 10));
set_transient($cache_key, $result, $fast_cache_ttl);
if (!empty($result['online'])) {
set_transient($last_good_key, $result, 180);
}
return $result;
}
@@ -666,7 +751,7 @@ function mcss_shortcode($atts) {
$widget_width = "650px";
$widget_padding = "15px";
$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'];
if (empty($srv['hide_port']) && !empty($srv['player_port_copy'])) {
@@ -752,13 +837,13 @@ function mcss_shortcode($atts) {
<!-- PLAYER GRID -->
<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 foreach ($data['players'] as $p): ?>
<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;">
<div style="font-size:0.75em;"><?php echo $p['display_html']; ?></div>
<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;margin-top:4px;"><?php echo $p['display_html']; ?></div>
</div>
<?php endforeach; ?>
<?php else: ?>
@@ -843,8 +928,8 @@ function mcss_shortcode($atts) {
d.players.forEach(function(p){
var content = p.display_html ? p.display_html : p.name;
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;">' +
'<div style="font-size:0.75em;">' + content + '</div>' +
'<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;margin-top:4px;">' + content + '</div>' +
'</div>';
});
} else {
@@ -870,4 +955,185 @@ function mcss_shortcode($atts) {
<?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 Repository:** [https://git.viper.ipv64.net/M_Viper/StatusAPI](https://git.viper.ipv64.net/M_Viper/StatusAPI)
---
## Features
- 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
- Anpassbare Darstellung für verschiedene Layouts
---
## 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.
```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
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.
Die StatusAPI stellt einen HTTP-Endpunkt bereit (Standard: `http://localhost:9191`), den das WordPress-Plugin abfragt.
### Shortcode für WordPress
Um den Serverstatus auf deiner WordPress-Seite anzuzeigen, füge einfach folgenden Shortcode ein:
```html
[bungeecord_status id="Bungeecord"]
```
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
**Status:** Online
@@ -42,17 +87,58 @@ Ersetze `"Bungeecord"` durch die ID deines Servers, falls mehrere Server eingebu
**Avatare:**
![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
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
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
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 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>
<artifactId>StatusAPI</artifactId>
<version>1.1</version>
<version>3.6.2</version>
<packaging>jar</packaging>
<name>StatusAPI</name>
@@ -20,7 +20,7 @@
</properties>
<dependencies>
<!-- BungeeCord API (lokal installiert) -->
<!-- BungeeCord API -->
<dependency>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-api</artifactId>
@@ -28,12 +28,13 @@
<scope>provided</scope>
</dependency>
<!-- LuckPerms API für Prefix Support -->
<!-- LuckPerms API (Optional) -->
<dependency>
<groupId>net.luckperms</groupId>
<artifactId>api</artifactId>
<version>5.4</version>
<scope>provided</scope>
<optional>true</optional>
</dependency>
</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;
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.config.ListenerInfo;
import net.md_5.bungee.api.connection.ProxiedPlayer;
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.Socket;
import java.util.*;
@@ -20,6 +21,7 @@ public class StatusAPI extends Plugin implements Runnable {
private Thread thread;
private int port = 9191;
private UpdateChecker updateChecker;
private FileDownloader fileDownloader;
@Override
public void onEnable() {
@@ -30,18 +32,33 @@ public class StatusAPI extends Plugin implements Runnable {
thread = new Thread(this, "StatusAPI-HTTP-Server");
thread.start();
// Start UpdateChecker: initialer Check (async) + regelmäßiger Schedule
String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0";
updateChecker = new UpdateChecker(this, currentVersion, 6); // 6 Stunden Intervall
fileDownloader = new FileDownloader(this);
// initialer sofortiger Start (async)
ProxyServer.getInstance().getScheduler().runAsync(this, () -> updateChecker.checkNow());
File pluginFile = getFile();
File backupFile = new File(pluginFile.getParentFile(), "StatusAPI.jar.bak");
// planmäßiger Intervall (alle 6 Stunden)
ProxyServer.getInstance().getScheduler().schedule(this, updateChecker, 6, 6, TimeUnit.HOURS);
// --- AUTOMATISCHES BACKUP-CLEANUP ---
// 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
ProxyServer.getInstance().getPluginManager().registerListener(this, new UpdateListener(this, updateChecker));
// Sofortiger Start-Check
checkAndMaybeUpdate();
// Regelmäßiger Check (alle 6 Stunden)
ProxyServer.getInstance().getScheduler().schedule(this, this::checkAndMaybeUpdate, 6, 6, TimeUnit.HOURS);
}
@Override
@@ -49,9 +66,99 @@ public class StatusAPI extends Plugin implements Runnable {
getLogger().info("Stoppe Web-Server...");
if (thread != null) {
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 {
thread.join(1000);
} catch (InterruptedException ignored) {}
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
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() {
try (ServerSocket serverSocket = new ServerSocket(port)) {
serverSocket.setSoTimeout(1000);
while (!Thread.interrupted()) {
try {
Socket clientSocket = serverSocket.accept();
handleConnection(clientSocket);
} catch (java.net.SocketTimeoutException e) {
// Loop Check (timeout) - erlaubt Unterbrechungsschleife
} catch (IOException e) {
} catch (java.net.SocketTimeoutException e) {}
catch (IOException e) {
getLogger().warning("Fehler beim Akzeptieren einer Verbindung: " + e.getMessage());
}
}
} catch (IOException e) {
getLogger().severe("Konnte ServerSocket nicht starten auf Port " + port + ": " + e.getMessage());
e.printStackTrace();
}
}
private void handleConnection(Socket clientSocket) {
BufferedReader in = null;
OutputStream out = null;
try {
in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), "UTF-8"));
out = clientSocket.getOutputStream();
try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), "UTF-8"));
OutputStream out = clientSocket.getOutputStream()) {
String inputLine = in.readLine();
if (inputLine != null && inputLine.startsWith("GET")) {
Map<String, Object> data = new LinkedHashMap<>();
data.put("online", true);
// --- VERSION CLEANUP START ---
// Version
String versionRaw = ProxyServer.getInstance().getVersion();
String versionClean = versionRaw;
if (versionRaw != null && versionRaw.contains(":")) {
String[] parts = versionRaw.split(":");
if (parts.length >= 3) {
versionClean = parts[2].trim();
}
if (parts.length >= 3) versionClean = parts[2].trim();
}
data.put("version", versionClean);
// --- VERSION CLEANUP ENDE ---
data.put("max_players", String.valueOf(ProxyServer.getInstance().getConfig().getPlayerLimit()));
// Motd
String motd = "BungeeCord";
try {
Iterator<ListenerInfo> it = ProxyServer.getInstance().getConfig().getListeners().iterator();
if (it.hasNext()) {
ListenerInfo listener = it.next();
if (listener != null && listener.getMotd() != null) {
motd = listener.getMotd();
}
if (listener != null && listener.getMotd() != null) motd = listener.getMotd();
}
} catch (Exception e) {
// Fallback bleibt "BungeeCord"
}
} catch (Exception ignored) {}
data.put("motd", motd);
// --- LUCKPERMS INTEGRATION START ---
// LuckPerms API über Provider holen (Verhindert ClassCastException)
LuckPerms luckPermsApi = null;
try {
luckPermsApi = LuckPermsProvider.get();
} catch (IllegalStateException e) {
// LuckPerms ist nicht geladen
}
// LuckPerms (Optional)
boolean luckPermsEnabled = ProxyServer.getInstance().getPluginManager().getPlugin("LuckPerms") != null;
List<Map<String, String>> playersList = new ArrayList<>();
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
Map<String, String> playerInfo = new LinkedHashMap<>();
playerInfo.put("name", p.getName());
// Prefix abfragen, falls LP gefunden wurde
String prefix = "";
if (luckPermsApi != null) {
User user = luckPermsApi.getUserManager().getUser(p.getUniqueId());
if (user != null) {
// Context bestimmen (Global oder Server-spezifisch)
QueryOptions queryOptions = luckPermsApi.getContextManager().getQueryOptions(user)
.orElse(QueryOptions.defaultContextualOptions());
String lpPrefix = user.getCachedData().getMetaData(queryOptions).getPrefix();
if (lpPrefix != null) {
prefix = lpPrefix;
if (luckPermsEnabled) {
try {
Class<?> providerClass = Class.forName("net.luckperms.api.LuckPermsProvider");
Object luckPermsApi = providerClass.getMethod("get").invoke(null);
Object userManager = luckPermsApi.getClass().getMethod("getUserManager").invoke(luckPermsApi);
Object user = userManager.getClass().getMethod("getUser", UUID.class).invoke(userManager, p.getUniqueId());
if (user != null) {
Class<?> queryOptionsClass = Class.forName("net.luckperms.api.query.QueryOptions");
Object queryOptions = queryOptionsClass.getMethod("defaultContextualOptions").invoke(null);
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);
playersList.add(playerInfo);
}
data.put("players", playersList);
// --- LUCKPERMS INTEGRATION ENDE ---
// Response
String json = buildJsonString(data);
byte[] jsonBytes = json.getBytes("UTF-8");
// HTTP Response mit korrekter Byte-Length
StringBuilder response = new StringBuilder();
response.append("HTTP/1.1 200 OK\r\n");
response.append("Content-Type: application/json; charset=UTF-8\r\n");
response.append("Access-Control-Allow-Origin: *\r\n");
response.append("Content-Length: ").append(jsonBytes.length).append("\r\n");
response.append("Connection: close\r\n");
response.append("\r\n");
// Header senden
response.append("Connection: close\r\n\r\n");
out.write(response.toString().getBytes("UTF-8"));
// Body senden
out.write(jsonBytes);
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) {
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) {
StringBuilder sb = new StringBuilder("{");
boolean first = true;
for (Map.Entry<String, Object> entry : data.entrySet()) {
if (!first) sb.append(",");
first = false;
sb.append("\"").append(escapeJson(entry.getKey())).append("\":");
// Wert verarbeiten (String, Boolean, List oder Map)
sb.append(valueToString(entry.getValue()));
sb.append("\"").append(escapeJson(entry.getKey())).append("\":").append(valueToString(entry.getValue()));
}
sb.append("}");
return sb.toString();
}
/**
* Hilfsmethode, um verschiedene Objekttypen in JSON-Strings umzuwandeln.
*/
private String valueToString(Object value) {
if (value == null) {
return "null";
} else if (value instanceof Boolean) {
return value.toString();
} else if (value instanceof List) {
if (value == null) return "null";
else if (value instanceof Boolean) return value.toString();
else if (value instanceof List) {
StringBuilder sb = new StringBuilder("[");
List<?> list = (List<?>) value;
for (int i = 0; i < list.size(); i++) {
if (i > 0) sb.append(",");
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));
} else {
// Andernfalls String behandeln
sb.append("\"").append(escapeJson(String.valueOf(item))).append("\"");
}
if (item instanceof Map) sb.append(buildJsonString((Map<String, Object>) item));
else sb.append("\"").append(escapeJson(String.valueOf(item))).append("\"");
}
sb.append("]");
return sb.toString();
} else {
// Standard String Behandlung
return "\"" + escapeJson(String.valueOf(value)) + "\"";
}
else return "\"" + escapeJson(String.valueOf(value)) + "\"";
}
private String escapeJson(String s) {
@@ -1,211 +1,151 @@
package net.viper.status;
import net.md_5.bungee.api.plugin.Plugin;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.chat.TextComponent;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* UpdateChecker:
* - Fragt die Gitea Releases API ab
* - Sucht in den Releases nach dem Asset "StatusAPI.jar"
* - Extrahiert die Version (tag_name bevorzugt, name als Fallback)
* - Cache die gefundene Version + URL (volatile fields)
*/
public class UpdateChecker implements Runnable {
private final Plugin plugin;
private final String currentVersion;
private final int intervalHours;
// Gitea Releases API für dein Repo
private final String apiUrl = "https://git.viper.ipv64.net/api/v1/repos/M_Viper/Minecraft-BungeeCord-Status/releases";
// cached results (volatile für Thread-Sichtbarkeit)
private volatile String latestVersion = "";
private volatile String latestUrl = "";
// 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);
private static final Pattern DOWNLOAD_PATTERN = Pattern.compile("\"browser_download_url\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
private static final Pattern TAG_NAME_PATTERN = Pattern.compile("\"tag_name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
private static final Pattern NAME_FIELD_PATTERN = Pattern.compile("\"name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
public UpdateChecker(Plugin plugin, String currentVersion, int intervalHours) {
this.plugin = plugin;
this.currentVersion = currentVersion != null ? currentVersion : "0.0.0";
this.intervalHours = Math.max(1, intervalHours);
}
@Override
public void run() {
// scheduled task calls checkNow
checkNow();
}
/**
* Führt einen sofortigen Check aus und updated cache (synchron for the calling thread).
*/
public void checkNow() {
try {
plugin.getLogger().info("Prüfe StatusAPI Releases via Gitea API...");
HttpURLConnection conn = (HttpURLConnection) new URL(apiUrl).openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json");
conn.setRequestProperty("User-Agent", "StatusAPI-UpdateChecker/1.0");
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
int code = conn.getResponseCode();
if (code != 200) {
plugin.getLogger().warning("Gitea API nicht erreichbar (HTTP " + code + ")");
return;
}
StringBuilder sb = new StringBuilder();
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"))) {
String line;
while ((line = br.readLine()) != null) {
sb.append(line).append("\n");
}
}
String body = sb.toString();
// Suche Release mit Asset "StatusAPI.jar"
String foundVersion = null;
String foundUrl = null;
// Split releases by top-level objects: we simply find repeating {...} blocks and inspect them
// This is robust enough for Gitea JSON responses.
Pattern releasePattern = Pattern.compile("(?s)\\{.*?\\}");
Matcher releaseMatcher = releasePattern.matcher(body);
while (releaseMatcher.find()) {
String block = releaseMatcher.group();
// extract asset names & download urls inside the block
java.util.List<String> names = new java.util.ArrayList<>();
java.util.List<String> downloads = new java.util.ArrayList<>();
Matcher nm = ASSET_NAME_PATTERN.matcher(block);
while (nm.find()) names.add(nm.group(1));
Matcher dm = DOWNLOAD_PATTERN.matcher(block);
while (dm.find()) downloads.add(dm.group(1));
int pairs = Math.min(names.size(), downloads.size());
for (int i = 0; i < pairs; i++) {
String name = names.get(i);
String dl = downloads.get(i);
if ("StatusAPI.jar".equalsIgnoreCase(name.trim())) {
// find tag_name or name field in the same block
Matcher tagM = TAG_NAME_PATTERN.matcher(block);
if (tagM.find()) {
foundVersion = tagM.group(1).trim();
} else {
Matcher nameFieldM = NAME_FIELD_PATTERN.matcher(block);
if (nameFieldM.find()) {
foundVersion = nameFieldM.group(1).trim();
}
}
foundUrl = dl;
break;
}
}
if (foundUrl != null) break;
}
if (foundVersion == null || foundUrl == null) {
plugin.getLogger().info("Kein Release mit Asset 'StatusAPI.jar' gefunden.");
return;
}
// Normalize version
if (foundVersion.startsWith("v") || foundVersion.startsWith("V")) {
foundVersion = foundVersion.substring(1);
}
plugin.getLogger().info("Gefundene Version: " + foundVersion + " (aktuell: " + currentVersion + ")");
// set cache
latestVersion = foundVersion;
latestUrl = foundUrl;
} catch (Exception e) {
plugin.getLogger().log(Level.SEVERE, "Fehler beim Update-Check: " + e.getMessage(), e);
}
}
/**
* 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) {}
}
}
}
package net.viper.status;
import net.md_5.bungee.api.plugin.Plugin;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class UpdateChecker {
private final Plugin plugin;
private final String currentVersion;
private final int intervalHours;
private final String apiUrl = "https://git.viper.ipv64.net/api/v1/repos/M_Viper/Minecraft-BungeeCord-Status/releases";
private volatile String latestVersion = "";
private volatile String latestUrl = "";
// Pattern für Dateinamen im Assets-Array
private static final Pattern ASSET_NAME_PATTERN = Pattern.compile("\"name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
// Pattern für Download-URL
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)
private static final Pattern TAG_NAME_PATTERN = Pattern.compile("\"tag_name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
public UpdateChecker(Plugin plugin, String currentVersion, int intervalHours) {
this.plugin = plugin;
this.currentVersion = currentVersion != null ? currentVersion : "0.0.0";
this.intervalHours = Math.max(1, intervalHours);
}
public void checkNow() {
try {
HttpURLConnection conn = (HttpURLConnection) new URL(apiUrl).openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json");
conn.setRequestProperty("User-Agent", "StatusAPI-UpdateChecker/2.0");
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
int code = conn.getResponseCode();
if (code != 200) {
plugin.getLogger().warning("Gitea API nicht erreichbar (HTTP " + code + ")");
return;
}
StringBuilder sb = new StringBuilder();
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"))) {
String line;
while ((line = br.readLine()) != null) sb.append(line).append("\n");
}
String body = sb.toString();
// 1. Die LATEST Version (Tag) finden
// Wir suchen das erste "tag_name" im gesamten JSON, das ist meistens das neueste Release.
String foundVersion = null;
Matcher tagM = TAG_NAME_PATTERN.matcher(body);
if (tagM.find()) {
foundVersion = tagM.group(1).trim();
}
if (foundVersion == null) {
plugin.getLogger().warning("Keine Version (Tag) im Release gefunden.");
return;
}
// Version säubern (v vorne entfernen)
if (foundVersion.startsWith("v") || foundVersion.startsWith("V")) {
foundVersion = foundVersion.substring(1);
}
// 2. Download URL für StatusAPI.jar finden
// Wir suchen das Asset "StatusAPI.jar"
String foundUrl = null;
Pattern releasePattern = Pattern.compile("(?s)\\{.*?\\}");
Matcher releaseMatcher = releasePattern.matcher(body);
while (releaseMatcher.find()) {
String block = releaseMatcher.group();
java.util.List<String> names = new java.util.ArrayList<>();
java.util.List<String> downloads = new java.util.ArrayList<>();
Matcher nm = ASSET_NAME_PATTERN.matcher(block);
while (nm.find()) names.add(nm.group(1));
Matcher dm = DOWNLOAD_PATTERN.matcher(block);
while (dm.find()) downloads.add(dm.group(1));
int pairs = Math.min(names.size(), downloads.size());
for (int i = 0; i < pairs; i++) {
String name = names.get(i);
String dl = downloads.get(i);
if ("StatusAPI.jar".equalsIgnoreCase(name.trim())) {
foundUrl = dl;
break;
}
}
if (foundUrl != null) break;
}
if (foundUrl == null) {
plugin.getLogger().warning("Keine JAR-Datei für dieses Release gefunden.");
return;
}
plugin.getLogger().info("Gefundene Version: " + foundVersion + " (Aktuell: " + currentVersion + ")");
latestVersion = foundVersion;
latestUrl = foundUrl;
} catch (Exception e) {
plugin.getLogger().log(Level.SEVERE, "Fehler beim Update-Check: " + e.getMessage(), e);
}
}
public String getLatestVersion() {
return latestVersion != null ? latestVersion : "";
}
public String getLatestUrl() {
return latestUrl != null ? latestUrl : "";
}
public boolean isUpdateAvailable(String currentVer) {
String lv = getLatestVersion();
if (lv.isEmpty()) return false;
return compareVersions(lv, currentVer) > 0;
}
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);
}
}
}
@@ -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
main: net.viper.status.StatusAPI
version: 1.1
version: 3.6.2
author: M_Viper
description: StatusAPI für BungeeCord inkl. Update-Checker
softdepend:
- LuckPerms
permissions:
statusapi.update.notify:
description: 'Erlaubt Update-Benachrichtigungen'
default: op
default: op