20 Commits
3.6.0 ... 3.6.2

Author SHA1 Message Date
056a98b6a9 StatusAPI/pom.xml aktualisiert 2026-01-03 11:32:09 +00:00
045eb29b18 StatusAPI/src/main/resources/plugin.yml aktualisiert 2026-01-03 11:31:57 +00:00
059a0d23f6 Dateien nach "StatusAPI/src/main/java/net/viper/status" hochladen 2026-01-03 11:31:32 +00:00
b2e1338597 StatusAPI/src/main/java/net/viper/status/UpdateListener.java gelöscht 2026-01-03 11:31:03 +00:00
0ec7710840 StatusAPI/src/main/java/net/viper/status/UpdateChecker.java aktualisiert 2026-01-03 11:30:53 +00:00
3a7534c4eb StatusAPI/src/main/java/net/viper/status/StatusAPI.java aktualisiert 2026-01-03 11:30:38 +00:00
3dc4ad4bb4 Minecraft-BungeeCord-Status/minecraft-bungeecord-status.php aktualisiert 2026-01-03 08:49:02 +00:00
954bc4d622 Minecraft-BungeeCord-Status/minecraft-bungeecord-status.php aktualisiert 2026-01-02 21:07:18 +00:00
187abfcbf5 Dateien nach "StatusAPI/src/main/java/net/viper/status" hochladen 2026-01-02 21:05:30 +00:00
5adb7f5752 StatusAPI/src/main/resources/plugin.yml aktualisiert 2026-01-02 21:04:58 +00:00
81165484b8 StatusAPI/pom.xml aktualisiert 2026-01-02 21:04:32 +00:00
280b0647a0 StatusAPI/src/main/java/net/viper/status/StatusAPI.java aktualisiert 2026-01-02 21:04:10 +00:00
88d22d8d08 StatusAPI/src/main/java/net/viper/status/StatusAPI.java aktualisiert 2026-01-02 21:02:57 +00:00
522551fc76 BungeeCord-Chrome/background.js aktualisiert 2026-01-01 22:28:56 +00:00
3be6b0b9a4 BungeeCord-Chrome/popup.js aktualisiert 2026-01-01 22:28:35 +00:00
f0e197ee8a Minecraft-BungeeCord-Status/minecraft-bungeecord-status.php aktualisiert 2026-01-01 21:49:48 +00:00
519bb5161e StatusAPI/src/main/java/net/viper/status/StatusAPI.java aktualisiert 2026-01-01 21:49:10 +00:00
395a26f023 Dateien nach "BungeeCord-Chrome/icons" hochladen 2026-01-01 13:31:35 +00:00
cc1b6115ce Dateien nach "BungeeCord-Chrome" hochladen 2026-01-01 13:31:09 +00:00
6915b8d807 README.md aktualisiert 2026-01-01 02:09:24 +00:00
15 changed files with 2385 additions and 887 deletions

View File

@@ -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

View File

@@ -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.0",
"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://*/*"
]
}

220
BungeeCord-Chrome/popup.css Normal file
View File

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

View File

@@ -0,0 +1,68 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Bungee Status</title>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div class="root">
<header>
<h1>Bungee Status</h1>
<div class="actions">
<button id="btnRefresh" title="Jetzt aktualisieren"></button>
<button id="btnToggleSettings"></button>
</div>
</header>
<section class="main">
<aside class="server-list">
<div class="add-form" id="settingsForm">
<input id="inputName" placeholder="Name (z. B. Lobby)" />
<input id="inputUrl" placeholder="URL (127.0.0.1:9191)" />
<input id="inputWpSite" placeholder="WP Site" />
<input id="inputWpServerId" placeholder="WP Server ID" />
<button id="btnAddServer">Hinzufügen</button>
</div>
<ul id="serversContainer"></ul>
</aside>
<div class="detail">
<div id="noSelection" class="placeholder">Wähle einen Server aus der linken Liste.</div>
<div id="detailContent" class="detailContent hidden">
<!-- NEUES LAYOUT ZEILE 1: Name, URL, Status (mit Pulsierendem Punkt) -->
<div class="detail-header">
<div class="server-identity">
<h2 id="detailName"></h2>
<span id="detailUrlText" class="server-url"></span>
</div>
<div class="status-wrapper">
<div id="detailPulse" class="pulsing-dot"></div>
<span id="detailStatus"></span>
</div>
</div>
<!-- NEUES LAYOUT ZEILE 2: Spieler, Version, Ping -->
<div class="detail-stats">
<div><strong>Spieler:</strong> <span id="detailPlayers"></span></div>
<div><strong>Version:</strong> <span id="detailVersion"></span></div>
<div><strong>Ping:</strong> <span id="detailPing">-</span></div>
</div>
<h3>Online Spieler</h3>
<ul id="detailPlayerList" class="playerList"></ul>
<div class="detailButtons">
<button id="btnEdit">Bearbeiten</button>
<button id="btnDelete">Löschen</button>
</div>
</div>
</div>
</section>
</div>
<script src="popup.js"></script>
</body>
</html>

384
BungeeCord-Chrome/popup.js Normal file
View File

@@ -0,0 +1,384 @@
const $ = id => document.getElementById(id);
const uid = () => 'srv_' + Math.random().toString(36).slice(2,9);
const inputName = $('inputName');
const inputUrl = $('inputUrl');
const inputWpSite = $('inputWpSite');
const inputWpServerId = $('inputWpServerId');
const btnAdd = $('btnAddServer');
const serversContainer = $('serversContainer');
const serverListPanel = document.querySelector('.server-list');
const btnRefresh = $('btnRefresh');
const btnToggleSettings = $('btnToggleSettings');
const settingsForm = $('settingsForm');
const noSelection = $('noSelection');
const detailContent = $('detailContent');
const detailName = $('detailName');
const detailUrlText = $('detailUrlText');
const detailStatus = $('detailStatus');
const detailPulse = $('detailPulse');
const detailVersion = $('detailVersion');
const detailPlayers = $('detailPlayers');
const detailPing = $('detailPing');
const detailPlayerList = $('detailPlayerList');
const btnEdit = $('btnEdit');
const btnDelete = $('btnDelete');
let servers = [];
let selectedId = null;
let statuses = {};
let previousStatuses = {};
let settingsVisible = false;
document.addEventListener('DOMContentLoaded', init);
btnAdd.addEventListener('click', handleAdd);
btnRefresh.addEventListener('click', manualRefresh);
btnToggleSettings.addEventListener('click', toggleSettings);
btnEdit.addEventListener('click', handleEdit);
btnDelete.addEventListener('click', handleDelete);
async function init() {
await loadServersFromStorage();
await loadStatusesFromStorage();
await loadSettingsVisibility();
renderServerList();
if (servers.length === 1) selectedId = servers[0].id;
renderDetail(selectedId);
adjustDetailLayout();
}
// --- Settings Visibility ---
async function loadSettingsVisibility() {
const obj = await chrome.storage.local.get(['settingsVisible']);
settingsVisible = obj.settingsVisible !== undefined ? obj.settingsVisible : false;
if (!settingsVisible) settingsForm.classList.add('hidden');
else settingsForm.classList.remove('hidden');
}
async function saveSettingsVisibility() {
await chrome.storage.local.set({ settingsVisible });
}
async function toggleSettings() {
settingsVisible = !settingsVisible;
settingsForm.classList.toggle('hidden');
await saveSettingsVisibility();
adjustDetailLayout();
}
// --- Storage ---
async function loadServersFromStorage() {
const obj = await chrome.storage.local.get(['servers']);
servers = obj.servers || [];
for (const s of servers) if (!s.id) s.id = uid();
await chrome.storage.local.set({ servers });
}
async function saveServersToStorage() {
await chrome.storage.local.set({ servers });
}
async function loadStatusesFromStorage() {
const obj = await chrome.storage.local.get(['serverStatuses']);
statuses = obj.serverStatuses || {};
}
// --- Layout ---
function adjustDetailLayout() {
const settingsHidden = settingsForm.classList.contains('hidden');
detailContent.classList.toggle('full-width', settingsHidden);
serverListPanel.classList.toggle('hidden', settingsHidden);
btnEdit.style.display = settingsHidden ? 'none' : 'inline-block';
btnDelete.style.display = settingsHidden ? 'none' : 'inline-block';
}
// --- Render Server List ---
function renderServerList() {
serversContainer.innerHTML = '';
if (servers.length === 0) {
serversContainer.innerHTML = '<div class="placeholder">Noch keine Server hinzugefügt.</div>';
return;
}
for (const s of servers) {
const item = document.createElement('li');
item.className = 'server-item';
item.dataset.id = s.id;
const meta = document.createElement('div');
meta.className = 'meta';
const name = document.createElement('div');
name.className = 'name';
name.textContent = s.name || '(Kein Name)';
let listUrl = s.url || (s.wpSite ? s.wpSite + ' (WP)' : '');
listUrl = listUrl.replace(':9191', '');
const url = document.createElement('div');
url.className = 'url';
url.textContent = listUrl;
meta.append(name, url);
const statusBubble = document.createElement('div');
statusBubble.className = 'status-bubble';
statusBubble.textContent = '—';
statusBubble.style.backgroundColor = 'transparent';
item.statusBubble = statusBubble;
item.append(meta, statusBubble);
item.addEventListener('click', () => {
selectedId = s.id;
renderDetail(selectedId);
});
serversContainer.appendChild(item);
}
updateServerListStatuses(false);
}
// --- Render 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 ? srv.wpSite : 'Lokal');
urlToShow = urlToShow.replace(':9191', '');
detailUrlText.textContent = urlToShow;
updateDetailForServer(srv, true);
}
// --- Update Detail ---
function updateDetailForServer(srv, force = false) {
const st = statuses[srv.id];
const prevSt = previousStatuses[srv.id];
const statusChanged = force ||
!prevSt ||
(prevSt.ok !== st?.ok) ||
(prevSt.data?.online !== st?.data?.online);
if (!st || !st.ok || !st.data) {
if (statusChanged) {
detailStatus.textContent = 'Offline';
detailPulse.classList.remove('online');
detailVersion.textContent = '-';
detailPlayers.textContent = '-';
detailPing.textContent = '-';
updatePlayerList([]);
}
} else {
const d = st.data;
if (statusChanged) {
detailStatus.textContent = d.online ? 'Online' : 'Offline';
if (d.online) {
detailPulse.classList.add('online');
} else {
detailPulse.classList.remove('online');
}
}
const newVersion = d.version || 'unknown';
if (force || detailVersion.textContent !== newVersion) {
detailVersion.textContent = newVersion;
}
const playersCount = Array.isArray(d.players) ? d.players.length : (typeof d.players === 'number' ? d.players : 0);
const maxPlayers = d.max_players;
let newPlayersText = String(playersCount);
if (maxPlayers && maxPlayers !== '-1') {
newPlayersText += ` / ${maxPlayers}`;
}
if (force || detailPlayers.textContent !== newPlayersText) {
detailPlayers.textContent = newPlayersText;
}
let pingVal = d.ping || d.latency || '-';
if (pingVal !== '-' && typeof pingVal === 'number') {
pingVal = pingVal + ' ms';
}
if (force || (pingVal !== '-' && detailPing.textContent !== pingVal)) {
detailPing.textContent = pingVal;
}
const currentPlayers = Array.isArray(d.players) ? d.players : [];
const prevPlayers = (prevSt && Array.isArray(prevSt.data?.players)) ? prevSt.data.players : [];
let playersChanged = false;
if (force) {
playersChanged = true;
} else {
playersChanged = JSON.stringify(currentPlayers) !== JSON.stringify(prevPlayers);
}
if (playersChanged) {
updatePlayerList(currentPlayers);
}
}
previousStatuses[srv.id] = st ? JSON.parse(JSON.stringify(st)) : null;
}
// --- Spielerliste ---
function updatePlayerList(players) {
detailPlayerList.innerHTML = '';
if (!players || players.length === 0) {
detailPlayerList.innerHTML = '<li class="placeholder">Keine Spieler online.</li>';
return;
}
for (const p of players) {
const li = document.createElement('li');
const name = typeof p === 'object' ? p.name || p.username || p.player || '' : String(p);
if (typeof p === 'object' && p.avatar) {
const img = document.createElement('img');
img.src = p.avatar;
img.className = 'player-avatar';
img.title = name;
li.appendChild(img);
} else {
// Falls kein Avatar vorhanden, generiere einen von mc-heads.net
const img = document.createElement('img');
img.src = `https://mc-heads.net/avatar/${encodeURIComponent(name)}/32`;
img.className = 'player-avatar';
img.title = name;
li.appendChild(img);
}
detailPlayerList.appendChild(li);
}
}
// --- Update Server List Statuses ---
function updateServerListStatuses() {
const items = serversContainer.querySelectorAll('.server-item');
items.forEach(item => {
const s = servers.find(x => x.id === item.dataset.id);
if (!s) return;
const st = statuses[s.id];
const prevSt = previousStatuses[s.id];
const statusChanged = !prevSt ||
(prevSt.ok !== st?.ok) ||
(prevSt.data?.online !== st?.data?.online);
if (!statusChanged) return;
if (!st || !st.ok || !st.data) {
item.statusBubble.textContent = 'Offline';
item.statusBubble.style.backgroundColor = 'var(--offline)';
} else if (st.data.online) {
item.statusBubble.textContent = 'Online';
item.statusBubble.style.backgroundColor = 'var(--online)';
} else {
item.statusBubble.textContent = 'Offline';
item.statusBubble.style.backgroundColor = 'var(--offline)';
}
previousStatuses[s.id] = st ? JSON.parse(JSON.stringify(st)) : null;
});
}
// --- Add / Edit / Delete ---
async function handleAdd() {
const name = inputName.value.trim();
const url = inputUrl.value.trim();
const wpSite = inputWpSite.value.trim();
const wpServerId = inputWpServerId.value.trim();
if (!url && !wpSite) {
alert('Bitte URL oder WP Site angeben');
return;
}
const s = {
id: uid(),
name: name || url || wpSite,
url: url || null,
wpSite: wpSite || null,
wpServerId: wpServerId || null
};
servers.push(s);
await saveServersToStorage();
inputName.value = '';
inputUrl.value = '';
inputWpSite.value = '';
inputWpServerId.value = '';
renderServerList();
}
async function handleEdit() {
if (!selectedId) return;
const srv = servers.find(s => s.id === selectedId);
if (!srv) return;
const newName = prompt('Name:', srv.name) || srv.name;
const newUrl = prompt('URL:', srv.url || '') || srv.url;
const newWpSite = prompt('WP Site:', srv.wpSite || '') || srv.wpSite;
const newWpServerId = prompt('WP Server ID:', srv.wpServerId || '') || srv.wpServerId;
srv.name = newName.trim();
srv.url = newUrl.trim();
srv.wpSite = newWpSite.trim();
srv.wpServerId = newWpServerId.trim();
await saveServersToStorage();
renderServerList();
renderDetail(selectedId);
}
async function handleDelete() {
if (!selectedId) return;
if (!confirm('Server wirklich löschen?')) return;
servers = servers.filter(s => s.id !== selectedId);
selectedId = null;
await saveServersToStorage();
renderServerList();
renderDetail(null);
}
async function manualRefresh() {
try {
chrome.runtime.sendMessage({ cmd: 'refreshNow' });
} catch(e) {
console.error('Refresh fehlgeschlagen:', e);
}
}
// --- Storage Listener ---
chrome.storage.onChanged.addListener((changes, area) => {
if (area === 'local' && changes.serverStatuses) {
statuses = changes.serverStatuses.newValue || {};
updateServerListStatuses();
if (selectedId) {
const srv = servers.find(s => s.id === selectedId);
if (srv) updateDetailForServer(srv);
}
}
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +1,58 @@
# 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.
## 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
Das Plugin **StatusAPI.jar** muss in den BungeeCord Plugin-Ordner kopiert werden, damit die API korrekt funktioniert.
### 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.
### 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.
## Beispielanzeige
**Status:** Online
**Version:** 1.8 - 1.21
**Ping:** 1 ms
**Spieler:** 1
**Avatare:**
![M_Viper Avatar](https://example.com/avatar/M_Viper.png) <!-- Platzhalter wird im Plugin dynamisch geladen -->
> Der Abstand zwischen dem Text „Spieler:“ und den Avataren ist bewusst etwas größer, um die Übersichtlichkeit zu erhöhen.
## 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
## Support
Bei Problemen überprüfe bitte:
- Ob das Plugin **StatusAPI.jar** korrekt im BungeeCord Plugin-Ordner liegt
- Ob der API-Port in WordPress korrekt auf **9191** eingestellt ist
- Ob der Server erreichbar ist und Spieler online sind

View File

@@ -1,43 +1,52 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" <project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd"> https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>net.viper.bungee</groupId> <groupId>net.viper.bungee</groupId>
<artifactId>StatusAPI</artifactId> <artifactId>StatusAPI</artifactId>
<version>1.0</version> <version>3.6.2</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>StatusAPI</name> <name>StatusAPI</name>
<description>BungeeCord Status API Plugin</description> <description>BungeeCord Status API Plugin</description>
<properties> <properties>
<maven.compiler.source>8</maven.compiler.source> <maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target> <maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> </properties>
<dependencies> <dependencies>
<!-- BungeeCord API (lokal installiert) --> <!-- BungeeCord API -->
<dependency> <dependency>
<groupId>net.md-5</groupId> <groupId>net.md-5</groupId>
<artifactId>bungeecord-api</artifactId> <artifactId>bungeecord-api</artifactId>
<version>1.20</version> <version>1.20</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
</dependencies>
<!-- LuckPerms API (Optional) -->
<build> <dependency>
<finalName>StatusAPI</finalName> <groupId>net.luckperms</groupId>
<artifactId>api</artifactId>
<resources> <version>5.4</version>
<resource> <scope>provided</scope>
<directory>src/main/resources</directory> <optional>true</optional>
<filtering>false</filtering> </dependency>
</resource> </dependencies>
</resources>
</build> <build>
<finalName>StatusAPI</finalName>
</project>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
</resource>
</resources>
</build>
</project>

View File

@@ -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) {}
}
});
}
}

View File

@@ -1,146 +1,293 @@
package net.viper.status; package net.viper.status;
import net.md_5.bungee.api.ProxyServer; import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.config.ListenerInfo; import net.md_5.bungee.api.config.ListenerInfo;
import net.md_5.bungee.api.plugin.Plugin; import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.plugin.Plugin;
import java.io.*;
import java.net.ServerSocket; import java.io.BufferedReader;
import java.net.Socket; import java.io.File;
import java.util.ArrayList; import java.io.IOException;
import java.util.HashMap; import java.io.InputStreamReader;
import java.util.List; import java.io.OutputStream;
import java.util.Map; import java.io.PrintWriter;
import java.net.ServerSocket;
public class StatusAPI extends Plugin implements Runnable { import java.net.Socket;
import java.util.*;
private Thread thread; import java.util.concurrent.TimeUnit;
private int port = 9191;
public class StatusAPI extends Plugin implements Runnable {
@Override
public void onEnable() { private Thread thread;
getLogger().info("StatusAPI wird aktiviert..."); private int port = 9191;
getLogger().info("Starte Web-Server auf Port " + port + "..."); private UpdateChecker updateChecker;
private FileDownloader fileDownloader;
thread = new Thread(this);
thread.start(); @Override
} public void onEnable() {
getLogger().info("StatusAPI wird aktiviert...");
@Override getLogger().info("Starte Web-Server auf Port " + port + "...");
public void onDisable() {
getLogger().info("Stoppe Web-Server..."); // Start HTTP server thread
if (thread != null) { thread = new Thread(this, "StatusAPI-HTTP-Server");
thread.interrupt(); thread.start();
}
} String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0";
updateChecker = new UpdateChecker(this, currentVersion, 6); // 6 Stunden Intervall
@Override fileDownloader = new FileDownloader(this);
public void run() {
try (ServerSocket serverSocket = new ServerSocket(port)) { File pluginFile = getFile();
serverSocket.setSoTimeout(1000); File backupFile = new File(pluginFile.getParentFile(), "StatusAPI.jar.bak");
while (!Thread.interrupted()) { // --- AUTOMATISCHES BACKUP-CLEANUP ---
try { // Falls ein altes Update (.bak) im Ordner liegt, löschen wir es nach 1 Minute Startzeit.
Socket clientSocket = serverSocket.accept(); // Wenn der Server kurz crashen würde, hast du noch 60 Sekunden Zeit, ihn zu stoppen,
handleConnection(clientSocket); // damit das Backup erhalten bleibt. Läuft er stabil, wird der Platz freigegeben.
} catch (java.net.SocketTimeoutException e) { if (backupFile.exists()) {
// Loop Check ProxyServer.getInstance().getScheduler().schedule(this, () -> {
} catch (IOException e) { if (backupFile.exists()) {
// Ignorieren if (backupFile.delete()) {
} getLogger().info("Altes Backup (.bak) wurde erfolgreich gelöscht.");
} }
} catch (IOException e) { }
getLogger().severe("Konnte ServerSocket nicht starten auf Port " + port); }, 1, TimeUnit.MINUTES);
e.printStackTrace(); }
} // ---------------------------------------
}
// Sofortiger Start-Check
private void handleConnection(Socket clientSocket) { checkAndMaybeUpdate();
try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) { // Regelmäßiger Check (alle 6 Stunden)
ProxyServer.getInstance().getScheduler().schedule(this, this::checkAndMaybeUpdate, 6, 6, TimeUnit.HOURS);
String inputLine = in.readLine(); }
if (inputLine != null && inputLine.startsWith("GET")) { @Override
public void onDisable() {
Map<String, Object> data = new HashMap<>(); getLogger().info("Stoppe Web-Server...");
data.put("online", true); if (thread != null) {
thread.interrupt();
// --- VERSION CLEANUP START --- try { thread.join(1000); } catch (InterruptedException ignored) {}
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 * Prüft Update und startet Download falls nötig.
if (versionRaw.matches("git-BungeeCord-Bootstrap:.*")) { */
String[] parts = versionRaw.split(":"); private void checkAndMaybeUpdate() {
if(parts.length > 2) { try {
versionClean = parts[2]; updateChecker.checkNow();
} String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0";
}
data.put("version", versionClean); if (updateChecker.isUpdateAvailable(currentVersion)) {
// --- VERSION CLEANUP ENDE --- String newVersion = updateChecker.getLatestVersion();
String url = updateChecker.getLatestUrl();
data.put("max_players", ProxyServer.getInstance().getConfig().getPlayerLimit()); getLogger().warning("----------------------------------------");
getLogger().warning("Neue Version verfügbar: " + newVersion);
String motd = "BungeeCord"; getLogger().warning("Starte automatisches Update...");
try { getLogger().warning("----------------------------------------");
ListenerInfo listener = ProxyServer.getInstance().getConfig().getListeners().iterator().next();
if (listener != null) { // Download Starten
motd = listener.getMotd(); File pluginFile = getFile();
} File newFile = new File(pluginFile.getParentFile(), "StatusAPI.jar.new");
} catch (Exception e) {
// Fallback fileDownloader.downloadFile(url, newFile, () -> {
} // Callback: Wenn Download erfolgreich
data.put("motd", motd); triggerUpdateScript(pluginFile, newFile);
});
List<String> playerNames = new ArrayList<>(); }
for (net.md_5.bungee.api.connection.ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { } catch (Exception e) {
playerNames.add(p.getName()); getLogger().severe("Fehler beim Update-Check: " + e.getMessage());
} }
data.put("players", playerNames); }
String json = buildJsonString(data); /**
* Erstellt ein externes Batch-Skript, startet es und stoppt den Server.
out.println("HTTP/1.1 200 OK"); * Das Skript führt den Datei-Tausch durch, wenn der Server weg ist.
out.println("Content-Type: application/json"); */
out.println("Access-Control-Allow-Origin: *"); private void triggerUpdateScript(File currentFile, File newFile) {
out.println("Content-Length: " + json.length()); try {
out.println("Connection: close"); File pluginsFolder = currentFile.getParentFile();
out.println(); // Wir legen das Skript neben die Haupt-JAR (root) damit es nicht im Plugin-Ordner liegt
out.println(json); File rootFolder = pluginsFolder.getParentFile();
} File batFile = new File(rootFolder, "StatusAPI_Update_" + System.currentTimeMillis() + ".bat");
} catch (Exception e) {
e.printStackTrace(); // Batch Inhalt
} // 1. Wartet 5 Sekunden (Server fährt runter)
} // 2. Geht in den Plugin Ordner
// 3. Sichert .jar zu .bak
private String buildJsonString(Map<String, Object> data) { // 4. Benennt .new zu .jar um
StringBuilder sb = new StringBuilder("{"); // 5. Löscht sich selbst
boolean first = true; String batContent = "@echo off\n" +
for (Map.Entry<String, Object> entry : data.entrySet()) { "echo Bitte warten, der Server f\"ahrt herunter...\n" +
if (!first) sb.append(","); "timeout /t 5 /nobreak >nul\n" +
first = false; "cd /d \"" + pluginsFolder.getAbsolutePath().replace("\\", "/") + "\"\n" +
sb.append("\"").append(entry.getKey()).append("\":"); "echo Fuehre Datei-Tausch durch...\n" +
"if exist StatusAPI.jar.bak del StatusAPI.jar.bak\n" +
Object value = entry.getValue(); "if exist StatusAPI.jar (\n" +
if (value instanceof List) { " ren StatusAPI.jar StatusAPI.jar.bak\n" +
sb.append("["); ")\n" +
List<?> list = (List<?>) value; "if exist StatusAPI.new.jar (\n" +
for (int i = 0; i < list.size(); i++) { " ren StatusAPI.new.jar StatusAPI.jar\n" +
if (i > 0) sb.append(","); " echo Update erfolgreich!\n" +
sb.append("\"").append(list.get(i)).append("\""); ") else (\n" +
} " echo FEHLER: StatusAPI.new.jar nicht gefunden!\n" +
sb.append("]"); " pause\n" +
} else if (value instanceof String) { ")\n" +
sb.append("\"").append(((String) value).replace("\"", "\\\"")).append("\""); "del \"%~f0\"";
} else if (value instanceof Boolean) {
sb.append(value); try (PrintWriter out = new PrintWriter(batFile)) {
} else { out.println(batContent);
sb.append("\"").append(value).append("\""); }
}
} // Spieler kicken
sb.append("}"); try {
return sb.toString(); 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());
}
}
@Override
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) {}
catch (IOException e) {
getLogger().warning("Fehler beim Akzeptieren einer Verbindung: " + e.getMessage());
}
}
} catch (IOException e) {
getLogger().severe("Konnte ServerSocket nicht starten auf Port " + port + ": " + e.getMessage());
}
}
private void handleConnection(Socket clientSocket) {
try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), "UTF-8"));
OutputStream out = clientSocket.getOutputStream()) {
String inputLine = in.readLine();
if (inputLine != null && inputLine.startsWith("GET")) {
Map<String, Object> data = new LinkedHashMap<>();
data.put("online", true);
// Version
String versionRaw = ProxyServer.getInstance().getVersion();
String versionClean = versionRaw;
if (versionRaw != null && versionRaw.contains(":")) {
String[] parts = versionRaw.split(":");
if (parts.length >= 3) versionClean = parts[2].trim();
}
data.put("version", versionClean);
data.put("max_players", String.valueOf(ProxyServer.getInstance().getConfig().getPlayerLimit()));
// Motd
String motd = "BungeeCord";
try {
Iterator<ListenerInfo> it = ProxyServer.getInstance().getConfig().getListeners().iterator();
if (it.hasNext()) {
ListenerInfo listener = it.next();
if (listener != null && listener.getMotd() != null) motd = listener.getMotd();
}
} catch (Exception ignored) {}
data.put("motd", motd);
// 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;
}
} catch (Exception ignored) {}
}
playerInfo.put("prefix", prefix);
playersList.add(playerInfo);
}
data.put("players", playersList);
// Response
String json = buildJsonString(data);
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) {
getLogger().severe("Fehler beim Verarbeiten der Anfrage: " + e.getMessage());
}
}
private String buildJsonString(Map<String, Object> data) {
StringBuilder sb = new StringBuilder("{");
boolean first = true;
for (Map.Entry<String, Object> entry : data.entrySet()) {
if (!first) sb.append(",");
first = false;
sb.append("\"").append(escapeJson(entry.getKey())).append("\":").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");
}
} }

View File

@@ -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);
}
}
}

View File

@@ -1,4 +1,13 @@
name: StatusAPI name: StatusAPI
version: 1.0 main: net.viper.status.StatusAPI
main: net.viper.status.StatusAPI version: 3.6.2
author: M_Viper author: M_Viper
description: StatusAPI für BungeeCord inkl. Update-Checker
softdepend:
- LuckPerms
permissions:
statusapi.update.notify:
description: 'Erlaubt Update-Benachrichtigungen'
default: op