Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5eb462e5a7 | ||
|
|
e5dda9531f | ||
|
|
b851cbc20b | ||
|
|
de04e58242 | ||
|
|
166bfad1d4 | ||
|
|
1de9e4b155 | ||
|
|
501be8649a | ||
|
|
3c80758b12 | ||
|
|
f7e0dcb23d | ||
|
|
65ea93e22d | ||
|
|
fde1947ae7 | ||
|
|
7556521a0e | ||
|
|
40c0ed7b0c | ||
|
|
f407604ff6 | ||
|
|
2fce65544b | ||
|
|
1c89b4fdfe | ||
|
|
700e6026b6 | ||
|
|
b4ee8b0564 | ||
|
|
111158967f | ||
|
|
f7e99f8625 | ||
|
|
e03ebffb64 | ||
|
|
056a98b6a9 | ||
|
|
045eb29b18 | ||
|
|
059a0d23f6 | ||
|
|
b2e1338597 | ||
|
|
0ec7710840 | ||
|
|
3a7534c4eb | ||
|
|
3dc4ad4bb4 | ||
|
|
954bc4d622 | ||
|
|
187abfcbf5 | ||
|
|
5adb7f5752 | ||
|
|
81165484b8 | ||
|
|
280b0647a0 | ||
|
|
88d22d8d08 | ||
|
|
522551fc76 | ||
|
|
3be6b0b9a4 | ||
|
|
f0e197ee8a | ||
|
|
519bb5161e | ||
|
|
395a26f023 | ||
|
|
cc1b6115ce | ||
|
|
6915b8d807 |
@@ -0,0 +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
|
||||
};
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"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.1",
|
||||
"action": {
|
||||
"default_popup": "popup.html",
|
||||
"default_icon": {
|
||||
"16": "icons/icon16.png",
|
||||
"48": "icons/icon48.png",
|
||||
"128": "icons/icon128.png"
|
||||
}
|
||||
},
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"permissions": [
|
||||
"storage",
|
||||
"alarms"
|
||||
],
|
||||
"host_permissions": [
|
||||
"http://*/*",
|
||||
"https://*/*"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,435 @@
|
||||
: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); }
|
||||
@@ -0,0 +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" 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>
|
||||
@@ -0,0 +1,430 @@
|
||||
// popup.js
|
||||
const $ = id => document.getElementById(id);
|
||||
const uid = () => 'srv_' + Math.random().toString(36).slice(2,9);
|
||||
|
||||
let servers = [];
|
||||
let selectedId = null;
|
||||
let statuses = {};
|
||||
let previousStatuses = {};
|
||||
let settingsVisible = false;
|
||||
let editingId = null; // Welcher Server gerade bearbeitet wird
|
||||
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
$('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);
|
||||
}
|
||||
|
||||
// ── 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;
|
||||
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;
|
||||
applySettingsVisibility();
|
||||
await saveSettingsVisibility();
|
||||
}
|
||||
|
||||
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 loadStatusesFromStorage() {
|
||||
const obj = await chrome.storage.local.get(['serverStatuses']);
|
||||
statuses = obj.serverStatuses || {};
|
||||
}
|
||||
|
||||
// ── Server list ──
|
||||
function renderServerList() {
|
||||
const container = $('serversContainer');
|
||||
container.innerHTML = '';
|
||||
if (servers.length === 0) {
|
||||
container.innerHTML = '<div class="placeholder" style="padding:8px 0;">Noch kein Server hinzugefügt.</div>';
|
||||
return;
|
||||
}
|
||||
for (const s of servers) {
|
||||
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 nameEl = document.createElement('div');
|
||||
nameEl.className = 'name';
|
||||
nameEl.textContent = s.name || '(Kein Name)';
|
||||
|
||||
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);
|
||||
});
|
||||
container.appendChild(li);
|
||||
}
|
||||
updateServerListStatuses();
|
||||
}
|
||||
|
||||
// ── Detail ──
|
||||
function renderDetail(id) {
|
||||
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;
|
||||
}
|
||||
$('noSelection').classList.add('hidden');
|
||||
$('detailContent').classList.remove('hidden');
|
||||
|
||||
$('detailName').textContent = srv.name || 'Unbenannter Server';
|
||||
let urlToShow = srv.url || (srv.wpSite || 'Lokal');
|
||||
urlToShow = urlToShow.replace(':9191', '');
|
||||
$('detailUrlText').textContent = urlToShow;
|
||||
|
||||
updateDetailForServer(srv, true);
|
||||
}
|
||||
|
||||
function updateDetailForServer(srv, force = false) {
|
||||
const st = statuses[srv.id];
|
||||
const prevSt = previousStatuses[srv.id];
|
||||
const changed = force || JSON.stringify(st) !== JSON.stringify(prevSt);
|
||||
if (!changed) return;
|
||||
|
||||
const badge = $('statusBadge');
|
||||
const pulse = $('detailPulse');
|
||||
|
||||
if (!st || !st.ok || !st.data) {
|
||||
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;
|
||||
const isOnline = !!d.online;
|
||||
|
||||
badge.className = 'online-badge ' + (isOnline ? 'online' : 'offline');
|
||||
pulse.className = 'pulsing-dot ' + (isOnline ? 'online' : 'offline');
|
||||
$('detailStatus').textContent = isOnline ? 'Online' : 'Offline';
|
||||
$('detailVersion').textContent = d.version || 'unknown';
|
||||
|
||||
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);
|
||||
|
||||
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 : [];
|
||||
updatePlayerList(currentPlayers);
|
||||
updateBackendServers(d.network?.backend_servers || null);
|
||||
}
|
||||
|
||||
previousStatuses[srv.id] = st ? JSON.parse(JSON.stringify(st)) : null;
|
||||
}
|
||||
|
||||
// ── Sub-Server chips ──
|
||||
function updateBackendServers(list) {
|
||||
const section = $('backendSection');
|
||||
const container = $('backendServersList');
|
||||
if (!list || list.length === 0) {
|
||||
section.classList.add('hidden');
|
||||
container.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 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 {
|
||||
name = String(p);
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── 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 eine URL oder WordPress-Einstellungen angeben.');
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
renderServerList();
|
||||
if (selectedId) renderDetail(selectedId);
|
||||
}
|
||||
|
||||
async function handleEdit() {
|
||||
if (!selectedId) return;
|
||||
const srv = servers.find(s => s.id === selectedId);
|
||||
if (!srv) return;
|
||||
|
||||
settingsVisible = true;
|
||||
applySettingsVisibility();
|
||||
await saveSettingsVisibility();
|
||||
|
||||
enterEditMode(srv);
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
if (!selectedId || !confirm('Server wirklich löschen?')) return;
|
||||
servers = servers.filter(s => s.id !== selectedId);
|
||||
selectedId = null;
|
||||
await saveServersToStorage();
|
||||
renderServerList();
|
||||
renderDetail(null);
|
||||
exitEditMode();
|
||||
}
|
||||
|
||||
async function manualRefresh() {
|
||||
try { chrome.runtime.sendMessage({ cmd: 'refreshNow' }); } catch(e) {}
|
||||
}
|
||||
|
||||
// ── Live updates from background ──
|
||||
chrome.storage.onChanged.addListener((changes, area) => {
|
||||
if (area === 'local' && changes.serverStatuses) {
|
||||
statuses = changes.serverStatuses.newValue || {};
|
||||
updateServerListStatuses();
|
||||
if (selectedId) {
|
||||
const srv = servers.find(s => s.id === selectedId);
|
||||
if (srv) updateDetailForServer(srv);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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.0 (Final Border Fix)
|
||||
* 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;
|
||||
@@ -16,6 +25,176 @@ define('MCSS_URL', plugin_dir_url(__FILE__));
|
||||
|
||||
require_once MCSS_DIR . 'rcon/Rcon.php';
|
||||
|
||||
/* ---------------- HELPER: MINECRAFT COLORS ---------------- */
|
||||
function mcss_format_minecraft_colors($text) {
|
||||
if (empty($text)) return '';
|
||||
|
||||
// Minecraft Color Map
|
||||
$color_map = [
|
||||
'0' => '#000000', // Black
|
||||
'1' => '#0000AA', // Dark Blue
|
||||
'2' => '#00AA00', // Dark Green
|
||||
'3' => '#00AAAA', // Dark Aqua
|
||||
'4' => '#AA0000', // Dark Red
|
||||
'5' => '#AA00AA', // Dark Purple
|
||||
'6' => '#FFAA00', // Gold
|
||||
'7' => '#AAAAAA', // Gray
|
||||
'8' => '#555555', // Dark Gray
|
||||
'9' => '#5555FF', // Blue
|
||||
'a' => '#55FF55', // Green
|
||||
'b' => '#55FFFF', // Aqua
|
||||
'c' => '#FF5555', // Red
|
||||
'd' => '#FF55FF', // Light Purple
|
||||
'e' => '#FFFF55', // Yellow
|
||||
'f' => '#FFFFFF' // White
|
||||
];
|
||||
|
||||
// Base Wrapper
|
||||
$formatted = '<span style="color:#AAAAAA;">'; // Standard Grau
|
||||
|
||||
// Ersetze Farben: &c -> </span><span style="color:#HEX">
|
||||
foreach ($color_map as $char => $hex) {
|
||||
$text = str_replace("&" . $char, "</span><span style=\"color:$hex;\">", $text);
|
||||
}
|
||||
|
||||
// Formatierungen
|
||||
$text = str_replace("&l", "</span><span style=\"font-weight:bold;\">", $text); // Bold
|
||||
$text = str_replace("&o", "</span><span style=\"font-style:italic;\">", $text); // Italic
|
||||
$text = str_replace("&n", "</span><span style=\"text-decoration:underline;\">", $text); // Underline
|
||||
$text = str_replace("&m", "</span><span style=\"text-decoration:line-through;\">", $text); // Strikethrough
|
||||
$text = str_replace("&r", "</span><span style=\"color:#AAAAAA;\">", $text); // Reset to Gray
|
||||
|
||||
$formatted .= $text . '</span>';
|
||||
return $formatted;
|
||||
}
|
||||
|
||||
/* ---------------- HELPER: 3D AVATAR URL ---------------- */
|
||||
function mcss_get_3d_avatar($name, $uuid = null) {
|
||||
// Bedrock-Spieler erkennen (Name enthält Punkt oder UUID beginnt mit xuid)
|
||||
$is_bedrock = strpos($name, '.') !== false || ($uuid && strpos($uuid, 'xuid') === 0);
|
||||
|
||||
if ($is_bedrock) {
|
||||
// Bedrock: mc-heads.net mit 3D Head (nutze UUID falls vorhanden)
|
||||
if ($uuid && !empty($uuid)) {
|
||||
return 'https://mc-heads.net/head/' . rawurlencode($uuid) . '/100';
|
||||
}
|
||||
return 'https://mc-heads.net/head/' . rawurlencode($name) . '/100';
|
||||
} else {
|
||||
// Java: mc-heads.net mit 3D Head
|
||||
return 'https://mc-heads.net/head/' . rawurlencode($name) . '/100';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ===============================
|
||||
// MCSS - UPDATE NOTICE SYSTEM
|
||||
// ===============================
|
||||
|
||||
// Plugin-Version aus Header lesen
|
||||
function mcss_get_plugin_version() {
|
||||
if ( ! function_exists( 'get_plugin_data' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||
}
|
||||
|
||||
$plugin_data = get_plugin_data( __FILE__ );
|
||||
return $plugin_data['Version'] ?? '0.0.0';
|
||||
}
|
||||
|
||||
// Cache manuell leeren
|
||||
function mcss_clear_update_cache() {
|
||||
if ( isset($_GET['mcss_clear_cache']) && current_user_can('manage_options') ) {
|
||||
check_admin_referer('mcss_clear_cache_action');
|
||||
delete_transient('mcss_latest_release');
|
||||
wp_redirect( admin_url('plugins.php') );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
add_action('admin_init', 'mcss_clear_update_cache');
|
||||
|
||||
// Neueste Release-Infos von Gitea holen
|
||||
function mcss_get_latest_release_info( $force_refresh = false ) {
|
||||
$transient_key = 'mcss_latest_release';
|
||||
|
||||
if ( $force_refresh ) {
|
||||
delete_transient( $transient_key );
|
||||
}
|
||||
|
||||
$release_info = get_transient( $transient_key );
|
||||
|
||||
if ( false === $release_info ) {
|
||||
$response = wp_remote_get(
|
||||
'https://git.viper.ipv64.net/api/v1/repos/M_Viper/Minecraft-BungeeCord-Status/releases/latest',
|
||||
['timeout' => 10]
|
||||
);
|
||||
|
||||
if ( ! is_wp_error($response) && 200 === wp_remote_retrieve_response_code($response) ) {
|
||||
$body = wp_remote_retrieve_body($response);
|
||||
$data = json_decode($body, true);
|
||||
|
||||
if ( $data && isset($data['tag_name']) ) {
|
||||
$tag = ltrim( $data['tag_name'], 'vV' );
|
||||
|
||||
$release_info = [
|
||||
'version' => $tag,
|
||||
'download_url' => $data['zipball_url'] ?? '',
|
||||
'notes' => $data['body'] ?? '',
|
||||
'published_at' => $data['published_at'] ?? '',
|
||||
];
|
||||
|
||||
set_transient( $transient_key, $release_info, 6 * HOUR_IN_SECONDS );
|
||||
} else {
|
||||
set_transient( $transient_key, [], HOUR_IN_SECONDS );
|
||||
}
|
||||
} else {
|
||||
set_transient( $transient_key, [], HOUR_IN_SECONDS );
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -30,8 +209,8 @@ add_action('admin_enqueue_scripts', function($hook){
|
||||
});
|
||||
|
||||
add_action('wp_enqueue_scripts', function(){
|
||||
wp_enqueue_style('mcss-style', MCSS_URL . 'css/style.css', [], '3.6.0');
|
||||
wp_enqueue_script('mcss-frontend-js', MCSS_URL . 'js/mcss-frontend.js', ['jquery'], '3.6.0', true);
|
||||
wp_enqueue_style('mcss-style', MCSS_URL . 'css/style.css', [], '3.8.1');
|
||||
wp_enqueue_script('mcss-frontend-js', MCSS_URL . 'js/mcss-frontend.js', ['jquery'], '3.8.1', true);
|
||||
});
|
||||
|
||||
/* ---------------- Settings ---------------- */
|
||||
@@ -62,8 +241,8 @@ function mcss_sanitize_servers($input) {
|
||||
'host' => sanitize_text_field($srv['host'] ?? ''),
|
||||
'rcon_port' => absint($srv['rcon_port'] ?? 25577),
|
||||
'rcon_pass' => sanitize_text_field($srv['rcon_pass'] ?? ''),
|
||||
'player_port' => sanitize_text_field($srv['player_port'] ?? '9191'), // API Port
|
||||
'player_port_copy' => sanitize_text_field($srv['player_port_copy'] ?? ''), // Display Port
|
||||
'player_port' => sanitize_text_field($srv['player_port'] ?? '9191'),
|
||||
'player_port_copy' => sanitize_text_field($srv['player_port_copy'] ?? ''),
|
||||
'copy_address' => sanitize_text_field($srv['copy_address'] ?? ''),
|
||||
'hide_port' => !empty($srv['hide_port']),
|
||||
'show_motd' => !empty($srv['show_motd']),
|
||||
@@ -71,14 +250,12 @@ function mcss_sanitize_servers($input) {
|
||||
'logo_id' => absint($srv['logo_id'] ?? 0),
|
||||
'logo_url' => esc_url_raw($srv['logo_url'] ?? ''),
|
||||
'custom_text' => wp_kses_post($srv['custom_text'] ?? ''),
|
||||
// Styles
|
||||
'ip_color' => sanitize_hex_color($srv['ip_color'] ?? '#1f2937'),
|
||||
'ct_color' => sanitize_hex_color($srv['ct_color'] ?? '#1e293b'),
|
||||
'ip_size' => sanitize_text_field($srv['ip_size'] ?? '1.5em'),
|
||||
'ct_size' => sanitize_text_field($srv['ct_size'] ?? '1.05em'),
|
||||
'name_color' => sanitize_hex_color($srv['name_color'] ?? '#333333'),
|
||||
'name_size' => sanitize_text_field($srv['name_size'] ?? '1.8em'),
|
||||
// Events & Maintenance
|
||||
'maintenance_mode' => !empty($srv['maintenance_mode']),
|
||||
'maintenance_message' => wp_kses_post($srv['maintenance_message'] ?? 'Wartung'),
|
||||
'announcement_enabled' => !empty($srv['announcement_enabled']),
|
||||
@@ -86,7 +263,6 @@ function mcss_sanitize_servers($input) {
|
||||
'announcement_start' => sanitize_text_field($srv['announcement_start'] ?? ''),
|
||||
'announcement_end' => sanitize_text_field($srv['announcement_end'] ?? ''),
|
||||
'announcement_type' => sanitize_text_field($srv['announcement_type'] ?? 'info'),
|
||||
// Ranks
|
||||
'ranks_json' => mcss_sanitize_ranks($srv['ranks_json'] ?? '[]'),
|
||||
];
|
||||
}
|
||||
@@ -136,7 +312,6 @@ function mcss_settings_page() {
|
||||
$servers = [['id'=>'default', 'name'=>'Mein Netzwerk', 'host'=>'127.0.0.1', 'player_port'=>'9191', 'cache_ttl'=>10, 'hide_port'=>true, 'show_motd'=>true]];
|
||||
}
|
||||
|
||||
// Vollständige Liste
|
||||
$font_sizes = [
|
||||
'0.7em'=>'Sehr klein','0.85em'=>'Klein','1em'=>'Normal','1.2em'=>'Etwas größer',
|
||||
'1.4em'=>'Groß','1.5em'=>'Sehr groß','1.7em'=>'Extra groß','2em'=>'Riesig',
|
||||
@@ -149,7 +324,7 @@ function mcss_settings_page() {
|
||||
<div class="wrap">
|
||||
<h1>BungeeCord Netzwerk Einstellungen</h1>
|
||||
<p style="color:#d97706;background:#fef3c7;padding:10px;border-radius:5px;">
|
||||
<strong>Final:</strong> Expliziter `border: none` im Inline-Style für Wartungsmodus.
|
||||
<strong>Wichtig:</strong> Bitte das Plugin StatusAPI.jar im Bungeecord Installieren.
|
||||
</p>
|
||||
<form method="post" action="options.php">
|
||||
<?php settings_fields('mcss_settings_group'); ?>
|
||||
@@ -322,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) {
|
||||
@@ -351,46 +590,85 @@ function mcss_parse_bungeecord_version($raw_version) {
|
||||
}
|
||||
|
||||
function mcss_fetch_server_with_ranks($srv) {
|
||||
$cache_key = 'mcss_data_' . md5($srv['host']);
|
||||
$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);
|
||||
$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;
|
||||
|
||||
$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'];
|
||||
}
|
||||
|
||||
$players_info = [];
|
||||
$user_defined_ranks = json_decode($srv['ranks_json'] ?? '[]', true);
|
||||
if (!is_array($user_defined_ranks)) $user_defined_ranks = [];
|
||||
|
||||
foreach ($api_data['players'] as $name) {
|
||||
$rank = 'Spieler';
|
||||
$color = '#566d8dff';
|
||||
foreach ($api_data['players'] as $player_data) {
|
||||
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
|
||||
$prefix_html = mcss_format_minecraft_colors($prefix);
|
||||
|
||||
// 2. Namen immer Schwarz darstellen (überschreibt eventuelle Farben aus dem Prefix)
|
||||
$name_html = '<span style="color:black;">' . esc_html($name) . '</span>';
|
||||
|
||||
// 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",
|
||||
'rank' => $rank,
|
||||
'color' => $color,
|
||||
'uuid' => $uuid,
|
||||
'avatar' => $avatar_url,
|
||||
'prefix' => $prefix,
|
||||
'display_html' => $display_html,
|
||||
'rank' => $prefix ?: 'Spieler',
|
||||
'color' => '#566d8dff',
|
||||
];
|
||||
}
|
||||
|
||||
$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,
|
||||
'motd' => is_array($api_data['motd']) ? implode(' ', $api_data['motd']) : $api_data['motd']
|
||||
];
|
||||
|
||||
$fast_cache_ttl = min(2, $srv['cache_tl'] ?? $srv['cache_ttl']);
|
||||
$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;
|
||||
}
|
||||
|
||||
@@ -421,7 +699,7 @@ function mcss_shortcode($atts) {
|
||||
}
|
||||
if (!$srv) return 'Server nicht gefunden';
|
||||
|
||||
// MAINTENANCE MODE (Rich Style - No Border)
|
||||
// MAINTENANCE MODE
|
||||
$maintenance_mode = !empty($srv['maintenance_mode']);
|
||||
$maintenance_message = $srv['maintenance_message'] ?? 'Der Server befindet sich derzeit im Wartungsmodus. Wir sind bald wieder für dich da!';
|
||||
if ($maintenance_mode) {
|
||||
@@ -439,10 +717,8 @@ function mcss_shortcode($atts) {
|
||||
}
|
||||
.mcss-status-maintenance { animation: pulse 2s infinite; }
|
||||
</style>
|
||||
<!-- FIX: border:none hinzugefügt -->
|
||||
<div id="mcss-widget-<?php echo esc_attr($uid); ?>" style="max-width:650px;margin:30px auto;padding:0; background:#fef3c7;border:none;border-radius:20px; overflow:hidden;box-shadow:0 16px 40px rgba(0,0,0,0.12); font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;">
|
||||
|
||||
<!-- Rich Header -->
|
||||
<div style="padding:28px 32px;background:#fef3c7;border-bottom:1px solid #fbbf24;display:flex;align-items:center;gap:20px;">
|
||||
<img src="<?php echo esc_url($logo); ?>" alt="Logo" loading="lazy" style="width:60px;height:60px;border-radius:12px;box-shadow:0 8px 20px rgba(0,0,0,0.15);" onerror="this.src='<?php echo MCSS_URL.'img/default-server-logo.png'; ?>'" />
|
||||
<div style="flex:1;">
|
||||
@@ -453,7 +729,6 @@ function mcss_shortcode($atts) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Rich Body -->
|
||||
<div style="padding:24px 32px;background:#fef3c7;">
|
||||
<div style="margin-bottom:16px;font-weight:700;color:#92400e;font-size:1.05em;">Wartungshinweis:</div>
|
||||
<div style="font-size:1.1em;color:#78350f;line-height:1.6;"><?php echo wp_kses_post($maintenance_message); ?></div>
|
||||
@@ -465,11 +740,10 @@ function mcss_shortcode($atts) {
|
||||
<?php return ob_get_clean();
|
||||
}
|
||||
|
||||
// NORMAL MODE (No Border)
|
||||
// NORMAL MODE
|
||||
$data = mcss_fetch_server_with_ranks($srv);
|
||||
$uid = md5($srv['host']);
|
||||
|
||||
// STYLES
|
||||
$name_color = $srv['name_color'] ?? '#333333';
|
||||
$name_size = $srv['name_size'] ?? '1.3em';
|
||||
$ct_color = $srv['ct_color'] ?? '#555555';
|
||||
@@ -477,15 +751,13 @@ 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 Logic (player_port_copy)
|
||||
$copy_addr = !empty($srv['copy_address']) ? $srv['copy_address'] : $srv['host'];
|
||||
if (empty($srv['hide_port']) && !empty($srv['player_port_copy'])) {
|
||||
$copy_addr .= ':' . $srv['player_port_copy'];
|
||||
}
|
||||
|
||||
// ANNOUNCEMENT CHECK
|
||||
$show_announcement = mcss_should_show_announcement($srv);
|
||||
$announcement_text = $srv['announcement_text'] ?? '';
|
||||
$announcement_type = $srv['announcement_type'] ?? 'info';
|
||||
@@ -499,7 +771,6 @@ function mcss_shortcode($atts) {
|
||||
.mcss-announcement { animation: slideDown 0.5s ease-out; }
|
||||
</style>
|
||||
|
||||
<!-- FIX: border:none hinzugefügt -->
|
||||
<div id="mcss-widget-<?php echo esc_attr($uid); ?>" style="max-width:<?php echo $widget_width; ?>;margin:20px auto;padding:<?php echo $widget_padding; ?>;background:white;border:none;border-radius:10px;box-shadow:0 4px 15px rgba(0,0,0,0.1);font-family:sans-serif;position:relative;">
|
||||
|
||||
<!-- POPUP / TOAST -->
|
||||
@@ -518,7 +789,6 @@ function mcss_shortcode($atts) {
|
||||
<div style="display:flex;align-items:center;gap:12px;border-bottom:1px solid #eee;padding-bottom:12px;">
|
||||
<img src="<?php echo esc_url($srv['logo_url'] ?: MCSS_URL.'img/default-server-logo.png'); ?>" style="width:<?php echo $logo_size; ?>;height:<?php echo $logo_size; ?>;border-radius:6px;">
|
||||
|
||||
<!-- GEÄNDERTER HEADER: Name -> IP + Status -> Zusatztext -->
|
||||
<div style="flex:1;display:flex;flex-direction:column;gap:6px;">
|
||||
|
||||
<!-- NAME -->
|
||||
@@ -531,13 +801,10 @@ function mcss_shortcode($atts) {
|
||||
|
||||
<!-- IP + STATUS -->
|
||||
<div style="display:flex;align-items:center;gap:10px;font-size:0.9em;font-weight:600;">
|
||||
|
||||
<!-- IP -->
|
||||
<span style="color:#374151;" id="mcss-ip-<?php echo esc_attr($uid); ?>">
|
||||
<?php echo esc_html($copy_addr); ?>
|
||||
</span>
|
||||
|
||||
<!-- PULSIERENDER STATUSPUNKT -->
|
||||
<span id="mcss-status-dot-<?php echo esc_attr($uid); ?>"
|
||||
class="<?php echo $data['online'] ? 'mcss-status-online' : ''; ?>"
|
||||
style="width:10px;height:10px;border-radius:50%;
|
||||
@@ -545,14 +812,13 @@ function mcss_shortcode($atts) {
|
||||
box-shadow:0 0 8px <?php echo $data['online'] ? 'rgba(16,185,129,.7)' : 'rgba(239,68,68,.7)'; ?>;">
|
||||
</span>
|
||||
|
||||
<!-- STATUS TEXT (EINDEUTIG) -->
|
||||
<span id="mcss-status-text-<?php echo esc_attr($uid); ?>"
|
||||
style="color:<?php echo $data['online'] ? '#10b981' : '#ef4444'; ?>;">
|
||||
<?php echo $data['online'] ? 'Online' : 'Offline'; ?>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- ZUSATZTEXT (wird NICHT von JS angefasst) -->
|
||||
<!-- ZUSATZTEXT -->
|
||||
<?php if (!empty($srv['custom_text'])): ?>
|
||||
<div id="mcss-custom-text-<?php echo esc_attr($uid); ?>" style="font-size:<?php echo $ct_size; ?>;color:<?php echo $ct_color; ?>;font-weight:500;">
|
||||
<?php echo wp_kses_post($srv['custom_text']); ?>
|
||||
@@ -571,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 esc_html($p['name']); ?></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: ?>
|
||||
@@ -605,11 +871,9 @@ function mcss_shortcode($atts) {
|
||||
});
|
||||
}
|
||||
|
||||
// SET CLICK ON NAME
|
||||
const nameEl = document.getElementById('mcss-name-<?php echo esc_attr($uid); ?>');
|
||||
if(nameEl) nameEl.addEventListener('click', mcss_copy_<?php echo esc_attr($uid); ?>);
|
||||
|
||||
// CLOSE ANNOUNCEMENT FUNCTION
|
||||
document.querySelectorAll('.mcss-announcement-close').forEach(function(btn){
|
||||
btn.addEventListener('click', function(e){
|
||||
e.preventDefault();
|
||||
@@ -662,9 +926,10 @@ function mcss_shortcode($atts) {
|
||||
var html = '';
|
||||
if (d.players && Array.isArray(d.players) && d.players.length > 0) {
|
||||
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;">' + (p.name ? p.name : '') + '</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 {
|
||||
@@ -692,3 +957,183 @@ 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' );
|
||||
});
|
||||
@@ -1,2 +1,144 @@
|
||||
# Minecraft-BungeeCord-Status
|
||||
# Minecraft BungeeCord Status
|
||||
|
||||
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**
|
||||
- Zeigt Version und Ping
|
||||
- Zeigt die Anzahl der Spieler und ihre Avatare
|
||||
- Klick auf Servername kopiert die Adresse automatisch
|
||||
- Spielerübersicht wird automatisch alle **2 Sekunden** aktualisiert
|
||||
- Hinweise/Banner können vom Nutzer geschlossen werden
|
||||
- Anpassbare Darstellung für verschiedene Layouts
|
||||
|
||||
---
|
||||
|
||||
## Wichtige Hinweise
|
||||
|
||||
### 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
|
||||
**Version:** 1.8 - 1.21
|
||||
**Ping:** 1 ms
|
||||
**Spieler:** 1
|
||||
|
||||
**Avatare:**
|
||||
 <!-- 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.
|
||||
|
||||
---
|
||||
|
||||
## 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](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 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.
|
||||
+11
-2
@@ -7,7 +7,7 @@
|
||||
|
||||
<groupId>net.viper.bungee</groupId>
|
||||
<artifactId>StatusAPI</artifactId>
|
||||
<version>1.0</version>
|
||||
<version>3.6.2</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>StatusAPI</name>
|
||||
@@ -20,13 +20,22 @@
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- BungeeCord API (lokal installiert) -->
|
||||
<!-- BungeeCord API -->
|
||||
<dependency>
|
||||
<groupId>net.md-5</groupId>
|
||||
<artifactId>bungeecord-api</artifactId>
|
||||
<version>1.20</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- LuckPerms API (Optional) -->
|
||||
<dependency>
|
||||
<groupId>net.luckperms</groupId>
|
||||
<artifactId>api</artifactId>
|
||||
<version>5.4</version>
|
||||
<scope>provided</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,28 +2,63 @@ package net.viper.status;
|
||||
|
||||
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.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class StatusAPI extends Plugin implements Runnable {
|
||||
|
||||
private Thread thread;
|
||||
private int port = 9191;
|
||||
private UpdateChecker updateChecker;
|
||||
private FileDownloader fileDownloader;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
getLogger().info("StatusAPI wird aktiviert...");
|
||||
getLogger().info("Starte Web-Server auf Port " + port + "...");
|
||||
|
||||
thread = new Thread(this);
|
||||
// Start HTTP server thread
|
||||
thread = new Thread(this, "StatusAPI-HTTP-Server");
|
||||
thread.start();
|
||||
|
||||
String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0";
|
||||
updateChecker = new UpdateChecker(this, currentVersion, 6); // 6 Stunden Intervall
|
||||
fileDownloader = new FileDownloader(this);
|
||||
|
||||
File pluginFile = getFile();
|
||||
File backupFile = new File(pluginFile.getParentFile(), "StatusAPI.jar.bak");
|
||||
|
||||
// --- 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);
|
||||
}
|
||||
// ---------------------------------------
|
||||
|
||||
// Sofortiger Start-Check
|
||||
checkAndMaybeUpdate();
|
||||
|
||||
// Regelmäßiger Check (alle 6 Stunden)
|
||||
ProxyServer.getInstance().getScheduler().schedule(this, this::checkAndMaybeUpdate, 6, 6, TimeUnit.HOURS);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -31,6 +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 {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,80 +166,93 @@ 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
|
||||
} catch (IOException e) {
|
||||
// Ignorieren
|
||||
} 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.printStackTrace();
|
||||
getLogger().severe("Konnte ServerSocket nicht starten auf Port " + port + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleConnection(Socket clientSocket) {
|
||||
try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
|
||||
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) {
|
||||
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 HashMap<>();
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("online", true);
|
||||
|
||||
// --- VERSION CLEANUP START ---
|
||||
// Version
|
||||
String versionRaw = ProxyServer.getInstance().getVersion();
|
||||
String versionClean = versionRaw; // Fallback
|
||||
|
||||
// Regex um die saubere Version zu extrahieren (z.B. 1.21-R0.5-SNAPSHOT)
|
||||
// Aus: git:BungeeCord-Bootstrap:1.21-R0.5-SNAPSHOT:36e6154:2012
|
||||
if (versionRaw.matches("git-BungeeCord-Bootstrap:.*")) {
|
||||
String versionClean = versionRaw;
|
||||
if (versionRaw != null && versionRaw.contains(":")) {
|
||||
String[] parts = versionRaw.split(":");
|
||||
if(parts.length > 2) {
|
||||
versionClean = parts[2];
|
||||
}
|
||||
if (parts.length >= 3) versionClean = parts[2].trim();
|
||||
}
|
||||
data.put("version", versionClean);
|
||||
// --- VERSION CLEANUP ENDE ---
|
||||
|
||||
data.put("max_players", ProxyServer.getInstance().getConfig().getPlayerLimit());
|
||||
data.put("max_players", String.valueOf(ProxyServer.getInstance().getConfig().getPlayerLimit()));
|
||||
|
||||
// Motd
|
||||
String motd = "BungeeCord";
|
||||
try {
|
||||
ListenerInfo listener = ProxyServer.getInstance().getConfig().getListeners().iterator().next();
|
||||
if (listener != null) {
|
||||
motd = listener.getMotd();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Fallback
|
||||
Iterator<ListenerInfo> it = ProxyServer.getInstance().getConfig().getListeners().iterator();
|
||||
if (it.hasNext()) {
|
||||
ListenerInfo listener = it.next();
|
||||
if (listener != null && listener.getMotd() != null) motd = listener.getMotd();
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
data.put("motd", motd);
|
||||
|
||||
List<String> playerNames = new ArrayList<>();
|
||||
for (net.md_5.bungee.api.connection.ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
||||
playerNames.add(p.getName());
|
||||
// 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());
|
||||
String prefix = "";
|
||||
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;
|
||||
}
|
||||
data.put("players", playerNames);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
playerInfo.put("prefix", prefix);
|
||||
playersList.add(playerInfo);
|
||||
}
|
||||
data.put("players", playersList);
|
||||
|
||||
// Response
|
||||
String json = buildJsonString(data);
|
||||
|
||||
out.println("HTTP/1.1 200 OK");
|
||||
out.println("Content-Type: application/json");
|
||||
out.println("Access-Control-Allow-Origin: *");
|
||||
out.println("Content-Length: " + json.length());
|
||||
out.println("Connection: close");
|
||||
out.println();
|
||||
out.println(json);
|
||||
byte[] jsonBytes = json.getBytes("UTF-8");
|
||||
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\r\n");
|
||||
out.write(response.toString().getBytes("UTF-8"));
|
||||
out.write(jsonBytes);
|
||||
out.flush();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
getLogger().severe("Fehler beim Verarbeiten der Anfrage: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,26 +262,32 @@ public class StatusAPI extends Plugin implements Runnable {
|
||||
for (Map.Entry<String, Object> entry : data.entrySet()) {
|
||||
if (!first) sb.append(",");
|
||||
first = false;
|
||||
sb.append("\"").append(entry.getKey()).append("\":");
|
||||
|
||||
Object value = entry.getValue();
|
||||
if (value instanceof List) {
|
||||
sb.append("[");
|
||||
List<?> list = (List<?>) value;
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
if (i > 0) sb.append(",");
|
||||
sb.append("\"").append(list.get(i)).append("\"");
|
||||
}
|
||||
sb.append("]");
|
||||
} else if (value instanceof String) {
|
||||
sb.append("\"").append(((String) value).replace("\"", "\\\"")).append("\"");
|
||||
} else if (value instanceof Boolean) {
|
||||
sb.append(value);
|
||||
} else {
|
||||
sb.append("\"").append(value).append("\"");
|
||||
}
|
||||
sb.append("\"").append(escapeJson(entry.getKey())).append("\":").append(valueToString(entry.getValue()));
|
||||
}
|
||||
sb.append("}");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String valueToString(Object value) {
|
||||
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);
|
||||
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 return "\"" + escapeJson(String.valueOf(value)) + "\"";
|
||||
}
|
||||
|
||||
private String escapeJson(String s) {
|
||||
if (s == null) return "";
|
||||
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
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,4 +1,13 @@
|
||||
name: StatusAPI
|
||||
version: 1.0
|
||||
main: net.viper.status.StatusAPI
|
||||
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
|
||||
Reference in New Issue
Block a user