Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 40c0ed7b0c | |||
| f407604ff6 | |||
| 2fce65544b | |||
| 1c89b4fdfe | |||
| 700e6026b6 | |||
| b4ee8b0564 | |||
| 111158967f | |||
| f7e99f8625 | |||
| e03ebffb64 | |||
| 056a98b6a9 | |||
| 045eb29b18 | |||
| 059a0d23f6 | |||
| b2e1338597 | |||
| 0ec7710840 | |||
| 3a7534c4eb | |||
| 3dc4ad4bb4 |
BIN
BungeeCord-Chrome/BungeeCord-Chrome.zip
Normal file
BIN
BungeeCord-Chrome/BungeeCord-Chrome.zip
Normal file
Binary file not shown.
@@ -10,34 +10,63 @@
|
|||||||
--offline: #ef4444;
|
--offline: #ef4444;
|
||||||
}
|
}
|
||||||
|
|
||||||
* { box-sizing:border-box; }
|
* { box-sizing: border-box; }
|
||||||
html,body { margin:0; padding:0; font-family:Inter, Arial, sans-serif; background:var(--bg); color:var(--text);}
|
|
||||||
.root { width:520px; max-width:520px; padding:12px; }
|
|
||||||
|
|
||||||
header { display:flex; align-items:center; justify-content:space-between; margin-bottom:8px; }
|
html, body {
|
||||||
header h1 { font-size:16px; margin:0; }
|
margin: 0;
|
||||||
header .actions button { background:transparent; border:1px solid var(--muted); color:var(--text); padding:4px 8px; border-radius:6px; cursor:pointer; }
|
padding: 0;
|
||||||
|
font-family: Inter, Arial, sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root {
|
||||||
|
width: 520px;
|
||||||
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 { font-size: 16px; margin: 0; }
|
||||||
|
|
||||||
|
header .actions button {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--muted);
|
||||||
|
color: var(--text);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
display:flex;
|
display: flex;
|
||||||
gap:12px;
|
gap: 12px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-list {
|
.server-list {
|
||||||
width:45%;
|
width: 45%;
|
||||||
background:var(--panel);
|
background: var(--panel);
|
||||||
padding:8px;
|
padding: 8px;
|
||||||
border-radius:var(--radius);
|
border-radius: var(--radius);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail {
|
.detail {
|
||||||
flex:1;
|
flex: 1;
|
||||||
background:var(--panel);
|
background: var(--panel);
|
||||||
padding:8px;
|
padding: 8px;
|
||||||
border-radius:var(--radius);
|
border-radius: var(--radius);
|
||||||
min-height:220px;
|
min-height: 220px;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,175 +75,143 @@ header .actions button { background:transparent; border:1px solid var(--muted);
|
|||||||
flex: 1 1 100%;
|
flex: 1 1 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-form { display:flex; flex-direction:column; gap:6px; margin-bottom:8px; }
|
.add-form { display: flex; flex-direction: column; gap: 6px; margin-bottom: 8px; }
|
||||||
.add-form input { padding:6px 8px; border-radius:6px; border:1px solid #21303b; background:#071019; color:var(--text); }
|
.add-form input { padding: 6px 8px; border-radius: 6px; border: 1px solid #21303b; background: #071019; color: var(--text); }
|
||||||
.add-form button { padding:6px 8px; border-radius:6px; border:none; background:var(--accent); color:white; cursor:pointer; }
|
.add-form button { padding: 6px 8px; border-radius: 6px; border: none; background: var(--accent); color: white; cursor: pointer; }
|
||||||
|
|
||||||
|
#serversContainer {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
#serversContainer { list-style:none; margin:0; padding:0; max-height:260px; overflow:auto; }
|
|
||||||
.server-item {
|
.server-item {
|
||||||
display:flex;
|
display: flex;
|
||||||
align-items:center;
|
align-items: center;
|
||||||
justify-content:space-between;
|
justify-content: space-between;
|
||||||
padding:6px;
|
padding: 8px;
|
||||||
margin-bottom:6px;
|
margin-bottom: 6px;
|
||||||
border-radius:6px;
|
border-radius: 6px;
|
||||||
cursor:pointer;
|
cursor: pointer;
|
||||||
background: rgba(255,255,255,0.02);
|
background: rgba(255,255,255,0.02);
|
||||||
transition: background 0.2s ease;
|
transition: background 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-item:hover { background: rgba(255,255,255,0.05); }
|
.server-item:hover { background: rgba(255,255,255,0.05); }
|
||||||
|
.server-item .meta { display: flex; flex-direction: column; }
|
||||||
|
.server-item .meta .name { font-weight: 600; }
|
||||||
|
.server-item .meta .url { font-size: 11px; color: var(--muted); }
|
||||||
|
|
||||||
.server-item .meta { display:flex; flex-direction:column; }
|
|
||||||
.server-item .meta .name { font-weight:600; }
|
|
||||||
.server-item .meta .url { font-size:12px; color:var(--muted); }
|
|
||||||
.server-item .status-bubble {
|
.server-item .status-bubble {
|
||||||
font-size:12px;
|
font-size: 11px;
|
||||||
padding:3px 6px;
|
padding: 2px 8px;
|
||||||
border-radius:999px;
|
border-radius: 999px;
|
||||||
color:#fff;
|
color: #fff;
|
||||||
transition: background-color 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder { color:var(--muted); padding:12px; }
|
.placeholder { color: var(--muted); padding: 12px; font-size: 13px; }
|
||||||
.hidden { display:none; }
|
.hidden { display: none; }
|
||||||
|
|
||||||
/* --- Detail Header (Zeile 1) --- */
|
|
||||||
.detail-header {
|
.detail-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-identity h2 {
|
.server-identity h2 { margin: 0; font-size: 18px; line-height: 1.2; }
|
||||||
margin: 0;
|
.server-url { display: block; font-size: 12px; color: var(--muted); margin-top: 2px; }
|
||||||
font-size: 18px;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-url {
|
|
||||||
display: block;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--muted);
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-wrapper {
|
.status-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
|
||||||
|
|
||||||
/* --- 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;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------------------------- */
|
.pulsing-dot {
|
||||||
|
width: 10px; height: 10px; border-radius: 50%;
|
||||||
.playerList {
|
background-color: var(--offline);
|
||||||
list-style:none;
|
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7);
|
||||||
padding-left:16px;
|
animation: pulse-red 2s infinite;
|
||||||
max-height:140px;
|
}
|
||||||
overflow:auto;
|
.pulsing-dot.online {
|
||||||
margin:6px 0;
|
background-color: var(--online);
|
||||||
display:flex;
|
animation: pulse-green 2s infinite;
|
||||||
flex-wrap:wrap;
|
|
||||||
gap:8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.playerList li {
|
@keyframes pulse-green { 0% {box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.7);} 70% {box-shadow: 0 0 0 8px rgba(34, 197, 94, 0);} 100% {box-shadow: 0 0 0 0 rgba(34, 197, 94, 0);} }
|
||||||
display:flex;
|
@keyframes pulse-red { 0% {box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7);} 70% {box-shadow: 0 0 0 8px rgba(239, 68, 68, 0);} 100% {box-shadow: 0 0 0 0 rgba(239, 68, 68, 0);} }
|
||||||
align-items:center;
|
|
||||||
gap:4px;
|
.detail-stats {
|
||||||
font-size:13px;
|
display: flex;
|
||||||
color:var(--text);
|
justify-content: space-between;
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playerList {
|
||||||
|
list-style: none;
|
||||||
|
padding: 10px 0;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: center; /* Das hier setzt die Köpfe wieder in die Mitte */
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-item {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-avatar {
|
.player-avatar {
|
||||||
width:24px;
|
width: 42px; height: 42px;
|
||||||
height:24px;
|
border-radius: 6px;
|
||||||
border-radius:4px;
|
|
||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
|
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
|
||||||
|
border: 2px solid rgba(255,255,255,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-avatar:hover { transform: scale(1.1); }
|
.player-avatar:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
.detailButtons {
|
border-color: var(--accent);
|
||||||
display:flex;
|
|
||||||
gap:8px;
|
|
||||||
margin-top:8px;
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.detailButtons button {
|
.player-hover-info {
|
||||||
padding:6px 8px;
|
position: absolute;
|
||||||
border-radius:6px;
|
bottom: -28px;
|
||||||
border:none;
|
background: rgba(0, 0, 0, 0.95);
|
||||||
cursor:pointer;
|
color: white;
|
||||||
transition: opacity 0.2s ease, transform 0.1s ease;
|
padding: 3px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
white-space: nowrap;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(5px);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
z-index: 100;
|
||||||
|
border: 1px solid rgba(255,255,255,0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.detailButtons button:hover { opacity: 0.9; transform: translateY(-1px); }
|
.player-item:hover .player-hover-info {
|
||||||
.detailButtons button:active { transform: translateY(0); }
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
#btnEdit { background: #f59e0b; color:#000; }
|
|
||||||
#btnDelete { background: var(--danger); color:#fff; }
|
|
||||||
|
|
||||||
#serversContainer::-webkit-scrollbar,
|
|
||||||
.playerList::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#serversContainer::-webkit-scrollbar-track,
|
.detailButtons { display: flex; gap: 8px; margin-top: 15px; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 10px; }
|
||||||
.playerList::-webkit-scrollbar-track {
|
.detailButtons button { padding: 6px 12px; border-radius: 6px; border: none; cursor: pointer; font-size: 12px; font-weight: 600; }
|
||||||
background: rgba(255,255,255,0.05);
|
#btnEdit { background: #f59e0b; color: #000; }
|
||||||
border-radius: 3px;
|
#btnDelete { background: var(--danger); color: #fff; }
|
||||||
}
|
|
||||||
|
|
||||||
#serversContainer::-webkit-scrollbar-thumb,
|
::-webkit-scrollbar { width: 6px; }
|
||||||
.playerList::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-track { background: transparent; }
|
||||||
background: var(--muted);
|
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 10px; }
|
||||||
border-radius: 3px;
|
::-webkit-scrollbar-thumb:hover { background: var(--accent); }
|
||||||
}
|
|
||||||
|
|
||||||
#serversContainer::-webkit-scrollbar-thumb:hover,
|
|
||||||
.playerList::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: var(--accent);
|
|
||||||
}
|
|
||||||
@@ -33,7 +33,6 @@
|
|||||||
<div id="noSelection" class="placeholder">Wähle einen Server aus der linken Liste.</div>
|
<div id="noSelection" class="placeholder">Wähle einen Server aus der linken Liste.</div>
|
||||||
<div id="detailContent" class="detailContent hidden">
|
<div id="detailContent" class="detailContent hidden">
|
||||||
|
|
||||||
<!-- NEUES LAYOUT ZEILE 1: Name, URL, Status (mit Pulsierendem Punkt) -->
|
|
||||||
<div class="detail-header">
|
<div class="detail-header">
|
||||||
<div class="server-identity">
|
<div class="server-identity">
|
||||||
<h2 id="detailName"></h2>
|
<h2 id="detailName"></h2>
|
||||||
@@ -45,7 +44,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- NEUES LAYOUT ZEILE 2: Spieler, Version, Ping -->
|
|
||||||
<div class="detail-stats">
|
<div class="detail-stats">
|
||||||
<div><strong>Spieler:</strong> <span id="detailPlayers"></span></div>
|
<div><strong>Spieler:</strong> <span id="detailPlayers"></span></div>
|
||||||
<div><strong>Version:</strong> <span id="detailVersion"></span></div>
|
<div><strong>Version:</strong> <span id="detailVersion"></span></div>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
//popup.js
|
||||||
const $ = id => document.getElementById(id);
|
const $ = id => document.getElementById(id);
|
||||||
const uid = () => 'srv_' + Math.random().toString(36).slice(2,9);
|
const uid = () => 'srv_' + Math.random().toString(36).slice(2,9);
|
||||||
|
|
||||||
@@ -50,7 +51,37 @@ async function init() {
|
|||||||
adjustDetailLayout();
|
adjustDetailLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Settings Visibility ---
|
// --- NEU: Minecraft Color Parser (Eingebaut ohne Löschung) ---
|
||||||
|
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'
|
||||||
|
};
|
||||||
|
let html = text.replace(/[<>]/g, '');
|
||||||
|
Object.keys(map).forEach(code => {
|
||||||
|
const color = map[code];
|
||||||
|
const regex = new RegExp(code, 'g');
|
||||||
|
html = html.replace(regex, `</span><span style="color: ${color}">`);
|
||||||
|
});
|
||||||
|
return `<span>${html}</span>`.replace(/&l/g, '<b>').replace(/&r/g, '</b>');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 3D Avatar URL Generator ---
|
||||||
|
function get3DAvatarUrl(playerName, uuid = null) {
|
||||||
|
const isBedrock = playerName.includes('.') || (uuid && uuid.startsWith('xuid'));
|
||||||
|
if (isBedrock) {
|
||||||
|
if (uuid && uuid.length > 0) {
|
||||||
|
return `https://mc-heads.net/head/${encodeURIComponent(uuid)}/64`;
|
||||||
|
}
|
||||||
|
return `https://mc-heads.net/head/${encodeURIComponent(playerName)}/64`;
|
||||||
|
} else {
|
||||||
|
return `https://mc-heads.net/head/${encodeURIComponent(playerName)}/64`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadSettingsVisibility() {
|
async function loadSettingsVisibility() {
|
||||||
const obj = await chrome.storage.local.get(['settingsVisible']);
|
const obj = await chrome.storage.local.get(['settingsVisible']);
|
||||||
settingsVisible = obj.settingsVisible !== undefined ? obj.settingsVisible : false;
|
settingsVisible = obj.settingsVisible !== undefined ? obj.settingsVisible : false;
|
||||||
@@ -69,7 +100,6 @@ async function toggleSettings() {
|
|||||||
adjustDetailLayout();
|
adjustDetailLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Storage ---
|
|
||||||
async function loadServersFromStorage() {
|
async function loadServersFromStorage() {
|
||||||
const obj = await chrome.storage.local.get(['servers']);
|
const obj = await chrome.storage.local.get(['servers']);
|
||||||
servers = obj.servers || [];
|
servers = obj.servers || [];
|
||||||
@@ -86,7 +116,6 @@ async function loadStatusesFromStorage() {
|
|||||||
statuses = obj.serverStatuses || {};
|
statuses = obj.serverStatuses || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Layout ---
|
|
||||||
function adjustDetailLayout() {
|
function adjustDetailLayout() {
|
||||||
const settingsHidden = settingsForm.classList.contains('hidden');
|
const settingsHidden = settingsForm.classList.contains('hidden');
|
||||||
detailContent.classList.toggle('full-width', settingsHidden);
|
detailContent.classList.toggle('full-width', settingsHidden);
|
||||||
@@ -95,51 +124,44 @@ function adjustDetailLayout() {
|
|||||||
btnDelete.style.display = settingsHidden ? 'none' : 'inline-block';
|
btnDelete.style.display = settingsHidden ? 'none' : 'inline-block';
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Render Server List ---
|
|
||||||
function renderServerList() {
|
function renderServerList() {
|
||||||
serversContainer.innerHTML = '';
|
serversContainer.innerHTML = '';
|
||||||
if (servers.length === 0) {
|
if (servers.length === 0) {
|
||||||
serversContainer.innerHTML = '<div class="placeholder">Noch keine Server hinzugefügt.</div>';
|
serversContainer.innerHTML = '<div class="placeholder">Noch keine Server hinzugefügt.</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const s of servers) {
|
for (const s of servers) {
|
||||||
const item = document.createElement('li');
|
const item = document.createElement('li');
|
||||||
item.className = 'server-item';
|
item.className = 'server-item';
|
||||||
item.dataset.id = s.id;
|
item.dataset.id = s.id;
|
||||||
|
|
||||||
const meta = document.createElement('div');
|
const meta = document.createElement('div');
|
||||||
meta.className = 'meta';
|
meta.className = 'meta';
|
||||||
const name = document.createElement('div');
|
const name = document.createElement('div');
|
||||||
name.className = 'name';
|
name.className = 'name';
|
||||||
name.textContent = s.name || '(Kein Name)';
|
name.textContent = s.name || '(Kein Name)';
|
||||||
|
|
||||||
let listUrl = s.url || (s.wpSite ? s.wpSite + ' (WP)' : '');
|
let listUrl = s.url || (s.wpSite ? s.wpSite + ' (WP)' : '');
|
||||||
listUrl = listUrl.replace(':9191', '');
|
listUrl = listUrl.replace(':9191', '');
|
||||||
|
|
||||||
const url = document.createElement('div');
|
const url = document.createElement('div');
|
||||||
url.className = 'url';
|
url.className = 'url';
|
||||||
url.textContent = listUrl;
|
url.textContent = listUrl;
|
||||||
meta.append(name, url);
|
meta.append(name, url);
|
||||||
|
|
||||||
const statusBubble = document.createElement('div');
|
const statusBubble = document.createElement('div');
|
||||||
statusBubble.className = 'status-bubble';
|
statusBubble.className = 'status-bubble';
|
||||||
statusBubble.textContent = '—';
|
statusBubble.textContent = '—';
|
||||||
statusBubble.style.backgroundColor = 'transparent';
|
statusBubble.style.backgroundColor = 'transparent';
|
||||||
item.statusBubble = statusBubble;
|
item.statusBubble = statusBubble;
|
||||||
|
|
||||||
item.append(meta, statusBubble);
|
item.append(meta, statusBubble);
|
||||||
item.addEventListener('click', () => {
|
item.addEventListener('click', () => {
|
||||||
selectedId = s.id;
|
selectedId = s.id;
|
||||||
renderDetail(selectedId);
|
renderDetail(selectedId);
|
||||||
});
|
});
|
||||||
|
|
||||||
serversContainer.appendChild(item);
|
serversContainer.appendChild(item);
|
||||||
}
|
}
|
||||||
updateServerListStatuses(false);
|
updateServerListStatuses();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Render Detail ---
|
// --- Fortsetzung popup.js ---
|
||||||
|
|
||||||
function renderDetail(id) {
|
function renderDetail(id) {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
noSelection.classList.remove('hidden');
|
noSelection.classList.remove('hidden');
|
||||||
@@ -154,26 +176,17 @@ function renderDetail(id) {
|
|||||||
}
|
}
|
||||||
noSelection.classList.add('hidden');
|
noSelection.classList.add('hidden');
|
||||||
detailContent.classList.remove('hidden');
|
detailContent.classList.remove('hidden');
|
||||||
|
|
||||||
detailName.textContent = srv.name || 'Unbenannter Server';
|
detailName.textContent = srv.name || 'Unbenannter Server';
|
||||||
|
|
||||||
let urlToShow = srv.url || (srv.wpSite ? srv.wpSite : 'Lokal');
|
let urlToShow = srv.url || (srv.wpSite ? srv.wpSite : 'Lokal');
|
||||||
urlToShow = urlToShow.replace(':9191', '');
|
urlToShow = urlToShow.replace(':9191', '');
|
||||||
|
|
||||||
detailUrlText.textContent = urlToShow;
|
detailUrlText.textContent = urlToShow;
|
||||||
|
|
||||||
updateDetailForServer(srv, true);
|
updateDetailForServer(srv, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Update Detail ---
|
|
||||||
function updateDetailForServer(srv, force = false) {
|
function updateDetailForServer(srv, force = false) {
|
||||||
const st = statuses[srv.id];
|
const st = statuses[srv.id];
|
||||||
const prevSt = previousStatuses[srv.id];
|
const prevSt = previousStatuses[srv.id];
|
||||||
|
const statusChanged = force || !prevSt || (prevSt.ok !== st?.ok) || (prevSt.data?.online !== st?.data?.online);
|
||||||
const statusChanged = force ||
|
|
||||||
!prevSt ||
|
|
||||||
(prevSt.ok !== st?.ok) ||
|
|
||||||
(prevSt.data?.online !== st?.data?.online);
|
|
||||||
|
|
||||||
if (!st || !st.ok || !st.data) {
|
if (!st || !st.ok || !st.data) {
|
||||||
if (statusChanged) {
|
if (statusChanged) {
|
||||||
@@ -186,105 +199,80 @@ function updateDetailForServer(srv, force = false) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const d = st.data;
|
const d = st.data;
|
||||||
|
|
||||||
if (statusChanged) {
|
if (statusChanged) {
|
||||||
detailStatus.textContent = d.online ? 'Online' : 'Offline';
|
detailStatus.textContent = d.online ? 'Online' : 'Offline';
|
||||||
if (d.online) {
|
if (d.online) detailPulse.classList.add('online');
|
||||||
detailPulse.classList.add('online');
|
else detailPulse.classList.remove('online');
|
||||||
} else {
|
|
||||||
detailPulse.classList.remove('online');
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const newVersion = d.version || 'unknown';
|
const newVersion = d.version || 'unknown';
|
||||||
if (force || detailVersion.textContent !== newVersion) {
|
if (force || detailVersion.textContent !== newVersion) detailVersion.textContent = newVersion;
|
||||||
detailVersion.textContent = newVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
const playersCount = Array.isArray(d.players) ? d.players.length : (typeof d.players === 'number' ? d.players : 0);
|
const playersCount = Array.isArray(d.players) ? d.players.length : (typeof d.players === 'number' ? d.players : 0);
|
||||||
const maxPlayers = d.max_players;
|
const maxPlayers = d.max_players;
|
||||||
let newPlayersText = String(playersCount);
|
let newPlayersText = String(playersCount);
|
||||||
if (maxPlayers && maxPlayers !== '-1') {
|
if (maxPlayers && maxPlayers !== '-1') newPlayersText += ` / ${maxPlayers}`;
|
||||||
newPlayersText += ` / ${maxPlayers}`;
|
if (force || detailPlayers.textContent !== newPlayersText) detailPlayers.textContent = newPlayersText;
|
||||||
}
|
|
||||||
|
|
||||||
if (force || detailPlayers.textContent !== newPlayersText) {
|
|
||||||
detailPlayers.textContent = newPlayersText;
|
|
||||||
}
|
|
||||||
|
|
||||||
let pingVal = d.ping || d.latency || '-';
|
let pingVal = d.ping || d.latency || '-';
|
||||||
if (pingVal !== '-' && typeof pingVal === 'number') {
|
if (pingVal !== '-' && typeof pingVal === 'number') pingVal = pingVal + ' ms';
|
||||||
pingVal = pingVal + ' ms';
|
if (force || (pingVal !== '-' && detailPing.textContent !== pingVal)) detailPing.textContent = pingVal;
|
||||||
}
|
|
||||||
|
|
||||||
if (force || (pingVal !== '-' && detailPing.textContent !== pingVal)) {
|
|
||||||
detailPing.textContent = pingVal;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentPlayers = Array.isArray(d.players) ? d.players : [];
|
const currentPlayers = Array.isArray(d.players) ? d.players : [];
|
||||||
const prevPlayers = (prevSt && Array.isArray(prevSt.data?.players)) ? prevSt.data.players : [];
|
const prevPlayers = (prevSt && Array.isArray(prevSt.data?.players)) ? prevSt.data.players : [];
|
||||||
|
let playersChanged = force || JSON.stringify(currentPlayers) !== JSON.stringify(prevPlayers);
|
||||||
let playersChanged = false;
|
if (playersChanged) updatePlayerList(currentPlayers);
|
||||||
if (force) {
|
|
||||||
playersChanged = true;
|
|
||||||
} else {
|
|
||||||
playersChanged = JSON.stringify(currentPlayers) !== JSON.stringify(prevPlayers);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playersChanged) {
|
|
||||||
updatePlayerList(currentPlayers);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
previousStatuses[srv.id] = st ? JSON.parse(JSON.stringify(st)) : null;
|
previousStatuses[srv.id] = st ? JSON.parse(JSON.stringify(st)) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Spielerliste ---
|
// --- Spielerliste mit Prefix-Hover und Farbsupport ---
|
||||||
function updatePlayerList(players) {
|
function updatePlayerList(players) {
|
||||||
detailPlayerList.innerHTML = '';
|
detailPlayerList.innerHTML = '';
|
||||||
if (!players || players.length === 0) {
|
if (!players || players.length === 0) {
|
||||||
detailPlayerList.innerHTML = '<li class="placeholder">Keine Spieler online.</li>';
|
detailPlayerList.innerHTML = '<li class="placeholder">Keine Spieler online.</li>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const p of players) {
|
for (const p of players) {
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
const name = typeof p === 'object' ? p.name || p.username || p.player || '' : String(p);
|
li.className = 'player-item'; // CSS Klasse für den Hover-Container
|
||||||
|
|
||||||
if (typeof p === 'object' && p.avatar) {
|
let name = '';
|
||||||
const img = document.createElement('img');
|
let uuid = null;
|
||||||
img.src = p.avatar;
|
let prefix = '';
|
||||||
img.className = 'player-avatar';
|
|
||||||
img.title = name;
|
if (typeof p === 'object') {
|
||||||
li.appendChild(img);
|
name = p.name || p.username || p.player || '';
|
||||||
|
uuid = p.uuid || null;
|
||||||
|
prefix = p.prefix || '';
|
||||||
} else {
|
} else {
|
||||||
// Falls kein Avatar vorhanden, generiere einen von mc-heads.net
|
name = String(p);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Das Hover-Element mit farbigem Prefix + Name
|
||||||
|
const info = document.createElement('div');
|
||||||
|
info.className = 'player-hover-info';
|
||||||
|
info.innerHTML = `${parseMinecraftColors(prefix)} ${name}`.trim();
|
||||||
|
|
||||||
|
li.appendChild(img);
|
||||||
|
li.appendChild(info);
|
||||||
detailPlayerList.appendChild(li);
|
detailPlayerList.appendChild(li);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Update Server List Statuses ---
|
|
||||||
function updateServerListStatuses() {
|
function updateServerListStatuses() {
|
||||||
const items = serversContainer.querySelectorAll('.server-item');
|
const items = serversContainer.querySelectorAll('.server-item');
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
const s = servers.find(x => x.id === item.dataset.id);
|
const s = servers.find(x => x.id === item.dataset.id);
|
||||||
if (!s) return;
|
if (!s) return;
|
||||||
|
|
||||||
const st = statuses[s.id];
|
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) {
|
if (!st || !st.ok || !st.data) {
|
||||||
item.statusBubble.textContent = 'Offline';
|
item.statusBubble.textContent = 'Offline';
|
||||||
item.statusBubble.style.backgroundColor = 'var(--offline)';
|
item.statusBubble.style.backgroundColor = 'var(--offline)';
|
||||||
@@ -295,39 +283,19 @@ function updateServerListStatuses() {
|
|||||||
item.statusBubble.textContent = 'Offline';
|
item.statusBubble.textContent = 'Offline';
|
||||||
item.statusBubble.style.backgroundColor = 'var(--offline)';
|
item.statusBubble.style.backgroundColor = 'var(--offline)';
|
||||||
}
|
}
|
||||||
|
|
||||||
previousStatuses[s.id] = st ? JSON.parse(JSON.stringify(st)) : null;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Add / Edit / Delete ---
|
|
||||||
async function handleAdd() {
|
async function handleAdd() {
|
||||||
const name = inputName.value.trim();
|
const name = inputName.value.trim();
|
||||||
const url = inputUrl.value.trim();
|
const url = inputUrl.value.trim();
|
||||||
const wpSite = inputWpSite.value.trim();
|
const wpSite = inputWpSite.value.trim();
|
||||||
const wpServerId = inputWpServerId.value.trim();
|
const wpServerId = inputWpServerId.value.trim();
|
||||||
|
if (!url && !wpSite) return;
|
||||||
if (!url && !wpSite) {
|
const s = { id: uid(), name: name || url || wpSite, url: url || null, wpSite: wpSite || null, wpServerId: wpServerId || null };
|
||||||
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);
|
servers.push(s);
|
||||||
await saveServersToStorage();
|
await saveServersToStorage();
|
||||||
|
inputName.value = inputUrl.value = inputWpSite.value = inputWpServerId.value = '';
|
||||||
inputName.value = '';
|
|
||||||
inputUrl.value = '';
|
|
||||||
inputWpSite.value = '';
|
|
||||||
inputWpServerId.value = '';
|
|
||||||
|
|
||||||
renderServerList();
|
renderServerList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,43 +303,26 @@ async function handleEdit() {
|
|||||||
if (!selectedId) return;
|
if (!selectedId) return;
|
||||||
const srv = servers.find(s => s.id === selectedId);
|
const srv = servers.find(s => s.id === selectedId);
|
||||||
if (!srv) return;
|
if (!srv) return;
|
||||||
|
|
||||||
const newName = prompt('Name:', srv.name) || srv.name;
|
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.name = newName.trim();
|
||||||
srv.url = newUrl.trim();
|
|
||||||
srv.wpSite = newWpSite.trim();
|
|
||||||
srv.wpServerId = newWpServerId.trim();
|
|
||||||
|
|
||||||
await saveServersToStorage();
|
await saveServersToStorage();
|
||||||
renderServerList();
|
renderServerList();
|
||||||
renderDetail(selectedId);
|
renderDetail(selectedId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDelete() {
|
async function handleDelete() {
|
||||||
if (!selectedId) return;
|
if (!selectedId || !confirm('Server wirklich löschen?')) return;
|
||||||
if (!confirm('Server wirklich löschen?')) return;
|
|
||||||
|
|
||||||
servers = servers.filter(s => s.id !== selectedId);
|
servers = servers.filter(s => s.id !== selectedId);
|
||||||
selectedId = null;
|
selectedId = null;
|
||||||
|
|
||||||
await saveServersToStorage();
|
await saveServersToStorage();
|
||||||
renderServerList();
|
renderServerList();
|
||||||
renderDetail(null);
|
renderDetail(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function manualRefresh() {
|
async function manualRefresh() {
|
||||||
try {
|
try { chrome.runtime.sendMessage({ cmd: 'refreshNow' }); } catch(e) {}
|
||||||
chrome.runtime.sendMessage({ cmd: 'refreshNow' });
|
|
||||||
} catch(e) {
|
|
||||||
console.error('Refresh fehlgeschlagen:', e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Storage Listener ---
|
|
||||||
chrome.storage.onChanged.addListener((changes, area) => {
|
chrome.storage.onChanged.addListener((changes, area) => {
|
||||||
if (area === 'local' && changes.serverStatuses) {
|
if (area === 'local' && changes.serverStatuses) {
|
||||||
statuses = changes.serverStatuses.newValue || {};
|
statuses = changes.serverStatuses.newValue || {};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Plugin Name: Minecraft BungeeCord Status – Network Edition
|
* Plugin Name: Minecraft BungeeCord Status – Network Edition
|
||||||
* Description: Der ultimative Live-Status für dein BungeeCord Netzwerk (Border None Fix).
|
* Description: Der ultimative Live-Status für dein BungeeCord Netzwerk (Border None Fix).
|
||||||
* Tags: minecraft, bungeecord, server status, player list
|
* Tags: minecraft, bungeecord, server status, player list
|
||||||
* Version: 3.6.1
|
* Version: 3.6.4
|
||||||
* Author: M_Viper
|
* Author: M_Viper
|
||||||
* Requires at least: 6.0
|
* Requires at least: 6.0
|
||||||
* Requires PHP: 7.4
|
* Requires PHP: 7.4
|
||||||
@@ -59,6 +59,23 @@ function mcss_format_minecraft_colors($text) {
|
|||||||
return $formatted;
|
return $formatted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------------- HELPER: 3D AVATAR URL ---------------- */
|
||||||
|
function mcss_get_3d_avatar($name, $uuid = null) {
|
||||||
|
// Bedrock-Spieler erkennen (Name enthält Punkt oder UUID beginnt mit xuid)
|
||||||
|
$is_bedrock = strpos($name, '.') !== false || ($uuid && strpos($uuid, 'xuid') === 0);
|
||||||
|
|
||||||
|
if ($is_bedrock) {
|
||||||
|
// Bedrock: mc-heads.net mit 3D Head (nutze UUID falls vorhanden)
|
||||||
|
if ($uuid && !empty($uuid)) {
|
||||||
|
return 'https://mc-heads.net/head/' . rawurlencode($uuid) . '/100';
|
||||||
|
}
|
||||||
|
return 'https://mc-heads.net/head/' . rawurlencode($name) . '/100';
|
||||||
|
} else {
|
||||||
|
// Java: mc-heads.net mit 3D Head
|
||||||
|
return 'https://mc-heads.net/head/' . rawurlencode($name) . '/100';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------------- AUTO UPDATE (MCSS) ---------------- */
|
/* ---------------- AUTO UPDATE (MCSS) ---------------- */
|
||||||
if ( ! class_exists( 'MCSS_Auto_Update' ) ) {
|
if ( ! class_exists( 'MCSS_Auto_Update' ) ) {
|
||||||
class MCSS_Auto_Update {
|
class MCSS_Auto_Update {
|
||||||
@@ -547,10 +564,12 @@ function mcss_fetch_server_with_ranks($srv) {
|
|||||||
if (is_array($player_data)) {
|
if (is_array($player_data)) {
|
||||||
$name = $player_data['name'];
|
$name = $player_data['name'];
|
||||||
$prefix = $player_data['prefix'] ?? '';
|
$prefix = $player_data['prefix'] ?? '';
|
||||||
|
$uuid = $player_data['uuid'] ?? null;
|
||||||
} else {
|
} else {
|
||||||
// Fallback für alte API
|
// Fallback für alte API
|
||||||
$name = $player_data;
|
$name = $player_data;
|
||||||
$prefix = '';
|
$prefix = '';
|
||||||
|
$uuid = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Prefix mit Farben konvertieren
|
// 1. Prefix mit Farben konvertieren
|
||||||
@@ -559,12 +578,20 @@ function mcss_fetch_server_with_ranks($srv) {
|
|||||||
// 2. Namen immer Schwarz darstellen (überschreibt eventuelle Farben aus dem Prefix)
|
// 2. Namen immer Schwarz darstellen (überschreibt eventuelle Farben aus dem Prefix)
|
||||||
$name_html = '<span style="color:black;">' . esc_html($name) . '</span>';
|
$name_html = '<span style="color:black;">' . esc_html($name) . '</span>';
|
||||||
|
|
||||||
// 3. Zusammenbauen
|
// 3. Zusammenbauen (Nur Abstand, wenn Prefix existiert)
|
||||||
$display_html = trim($prefix_html) . ' ' . $name_html;
|
if (!empty($prefix_html)) {
|
||||||
|
$display_html = $prefix_html . ' ' . $name_html;
|
||||||
|
} else {
|
||||||
|
$display_html = $name_html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 3D Avatar URL generieren (mit Bedrock-Support)
|
||||||
|
$avatar_url = mcss_get_3d_avatar($name, $uuid);
|
||||||
|
|
||||||
$players_info[] = [
|
$players_info[] = [
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
'avatar' => "https://mc-heads.net/avatar/" . rawurlencode($name) . "/64",
|
'uuid' => $uuid,
|
||||||
|
'avatar' => $avatar_url,
|
||||||
'prefix' => $prefix,
|
'prefix' => $prefix,
|
||||||
'display_html' => $display_html,
|
'display_html' => $display_html,
|
||||||
'rank' => $prefix ?: 'Spieler',
|
'rank' => $prefix ?: 'Spieler',
|
||||||
@@ -666,7 +693,7 @@ function mcss_shortcode($atts) {
|
|||||||
$widget_width = "650px";
|
$widget_width = "650px";
|
||||||
$widget_padding = "15px";
|
$widget_padding = "15px";
|
||||||
$logo_size = "70px";
|
$logo_size = "70px";
|
||||||
$player_head_size = "32px";
|
$player_head_size = "40px"; // Größer für 3D-Köpfe
|
||||||
|
|
||||||
$copy_addr = !empty($srv['copy_address']) ? $srv['copy_address'] : $srv['host'];
|
$copy_addr = !empty($srv['copy_address']) ? $srv['copy_address'] : $srv['host'];
|
||||||
if (empty($srv['hide_port']) && !empty($srv['player_port_copy'])) {
|
if (empty($srv['hide_port']) && !empty($srv['player_port_copy'])) {
|
||||||
@@ -752,13 +779,13 @@ function mcss_shortcode($atts) {
|
|||||||
|
|
||||||
<!-- PLAYER GRID -->
|
<!-- PLAYER GRID -->
|
||||||
<span style="color:#666;margin-bottom:15px;">Spieler:</span>
|
<span style="color:#666;margin-bottom:15px;">Spieler:</span>
|
||||||
<div id="mcss-player-grid-<?php echo esc_attr($uid); ?>" style="display:flex;flex-wrap:wrap;gap:8px;margin-top:8px;color:#666;justify-content:center;">
|
<div id="mcss-player-grid-<?php echo esc_attr($uid); ?>" style="display:flex;flex-wrap:wrap;gap:12px;margin-top:8px;color:#666;justify-content:center;">
|
||||||
|
|
||||||
<?php if (!empty($data['players']) && is_array($data['players'])): ?>
|
<?php if (!empty($data['players']) && is_array($data['players'])): ?>
|
||||||
<?php foreach ($data['players'] as $p): ?>
|
<?php foreach ($data['players'] as $p): ?>
|
||||||
<div style="text-align:center;">
|
<div style="text-align:center;">
|
||||||
<img src="<?php echo esc_url($p['avatar']); ?>" style="width:<?php echo $player_head_size; ?>;height:<?php echo $player_head_size; ?>;border-radius:4px;">
|
<img src="<?php echo esc_url($p['avatar']); ?>" style="width:<?php echo $player_head_size; ?>;height:<?php echo $player_head_size; ?>;border-radius:6px;box-shadow:0 2px 6px rgba(0,0,0,0.15);" loading="lazy">
|
||||||
<div style="font-size:0.75em;"><?php echo $p['display_html']; ?></div>
|
<div style="font-size:0.75em;margin-top:4px;"><?php echo $p['display_html']; ?></div>
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
@@ -843,8 +870,8 @@ function mcss_shortcode($atts) {
|
|||||||
d.players.forEach(function(p){
|
d.players.forEach(function(p){
|
||||||
var content = p.display_html ? p.display_html : p.name;
|
var content = p.display_html ? p.display_html : p.name;
|
||||||
html += '<div style="text-align:center;">' +
|
html += '<div style="text-align:center;">' +
|
||||||
'<img src="' + (p.avatar ? p.avatar : '') + '" style="width:<?php echo $player_head_size; ?>;height:<?php echo $player_head_size; ?>;border-radius:4px;">' +
|
'<img src="' + (p.avatar ? p.avatar : '') + '" style="width:<?php echo $player_head_size; ?>;height:<?php echo $player_head_size; ?>;border-radius:6px;box-shadow:0 2px 6px rgba(0,0,0,0.15);" loading="lazy">' +
|
||||||
'<div style="font-size:0.75em;">' + content + '</div>' +
|
'<div style="font-size:0.75em;margin-top:4px;">' + content + '</div>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -871,3 +898,184 @@ function mcss_shortcode($atts) {
|
|||||||
|
|
||||||
<?php return ob_get_clean();
|
<?php return ob_get_clean();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------------- SIDEBAR WIDGET: MODERN PILL BADGE (ROBUST INLINE STYLES) ---------------- */
|
||||||
|
class MCSS_Sidebar_Status_Widget extends WP_Widget {
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
parent::__construct(
|
||||||
|
'mcss_sidebar_status', // Basis ID
|
||||||
|
'MC Server Status (Simple)', // Name
|
||||||
|
array( 'description' => 'Zeigt einen modernen Online/Offline Status in der Sidebar an.' ) // Args
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backend: Formular im Widget-Bereich
|
||||||
|
public function form( $instance ) {
|
||||||
|
$title = ! empty( $instance['title'] ) ? $instance['title'] : 'Server Status';
|
||||||
|
$server_id = ! empty( $instance['server_id'] ) ? $instance['server_id'] : '';
|
||||||
|
|
||||||
|
// Server-Liste holen
|
||||||
|
$servers = get_option('mcss_servers', []);
|
||||||
|
?>
|
||||||
|
<p>
|
||||||
|
<label for="<?php echo $this->get_field_id( 'title' ); ?>">Titel:</label>
|
||||||
|
<input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>">
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label for="<?php echo $this->get_field_id( 'server_id' ); ?>">Server wählen:</label>
|
||||||
|
<select class="widefat" id="<?php echo $this->get_field_id( 'server_id' ); ?>" name="<?php echo $this->get_field_name( 'server_id' ); ?>">
|
||||||
|
<option value="">-- Bitte wählen --</option>
|
||||||
|
<?php foreach ($servers as $srv): ?>
|
||||||
|
<option value="<?php echo esc_attr($srv['id'] ?? ''); ?>" <?php selected($server_id, $srv['id'] ?? ''); ?>>
|
||||||
|
<?php echo esc_html($srv['name'] ?? 'Unbenannt'); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</p>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backend: Speichern
|
||||||
|
public function update( $new_instance, $old_instance ) {
|
||||||
|
$instance = array();
|
||||||
|
$instance['title'] = ( ! empty( $new_instance['title'] ) ) ? sanitize_text_field( $new_instance['title'] ) : '';
|
||||||
|
$instance['server_id'] = ( ! empty( $new_instance['server_id'] ) ) ? sanitize_text_field( $new_instance['server_id'] ) : '';
|
||||||
|
return $instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Frontend: Ausgabe
|
||||||
|
public function widget( $args, $instance ) {
|
||||||
|
echo $args['before_widget'];
|
||||||
|
|
||||||
|
if ( ! empty( $instance['title'] ) ) {
|
||||||
|
echo $args['before_title'] . apply_filters( 'widget_title', $instance['title'] ) . $args['after_title'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$server_id = $instance['server_id'];
|
||||||
|
$servers = get_option('mcss_servers', []);
|
||||||
|
$target_srv = null;
|
||||||
|
|
||||||
|
// Server suchen
|
||||||
|
foreach ($servers as $srv) {
|
||||||
|
if (($srv['id'] ?? '') === $server_id) { $target_srv = $srv; break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($target_srv) {
|
||||||
|
// Initiale Daten holen
|
||||||
|
$data = mcss_fetch_server_with_ranks($target_srv);
|
||||||
|
$is_online = $data['online'] ?? false;
|
||||||
|
$uid = md5($target_srv['host'] . 'widget'); // Einzigartige ID für JS
|
||||||
|
|
||||||
|
// Farben definieren
|
||||||
|
$bg_online = '#ecfdf5';
|
||||||
|
$bg_offline = '#fef2f2';
|
||||||
|
$text_online = '#059669';
|
||||||
|
$text_offline = '#dc2626';
|
||||||
|
$dot_online = '#10b981';
|
||||||
|
$dot_offline = '#ef4444';
|
||||||
|
$border_online = 'rgba(16, 185, 129, 0.2)';
|
||||||
|
$border_offline = 'rgba(239, 68, 68, 0.2)';
|
||||||
|
|
||||||
|
$anim_name_online = 'mcss-pulse-modern';
|
||||||
|
$anim_name_offline = 'mcss-pulse-modern-red';
|
||||||
|
|
||||||
|
// Werte für den Initialzustand setzen
|
||||||
|
$current_bg = $is_online ? $bg_online : $bg_offline;
|
||||||
|
$current_text = $is_online ? $text_online : $text_offline;
|
||||||
|
$current_dot = $is_online ? $dot_online : $dot_offline;
|
||||||
|
$current_border = $is_online ? $border_online : $border_offline;
|
||||||
|
$current_anim = $is_online ? $anim_name_online : $anim_name_offline;
|
||||||
|
$status_label = $is_online ? 'Online' : 'Offline';
|
||||||
|
|
||||||
|
// Inline Animation Definitionen für Robustheit
|
||||||
|
?>
|
||||||
|
<style>
|
||||||
|
@keyframes mcss-pulse-modern {
|
||||||
|
0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4); }
|
||||||
|
70% { box-shadow: 0 0 0 6px rgba(16, 185, 129, 0); }
|
||||||
|
100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
|
||||||
|
}
|
||||||
|
@keyframes mcss-pulse-modern-red {
|
||||||
|
0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); }
|
||||||
|
70% { box-shadow: 0 0 0 6px rgba(239, 68, 68, 0); }
|
||||||
|
100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- Wrapper mit PUREN Inline Styles -->
|
||||||
|
<div id="mcss-sw-wrapper-<?php echo esc_attr($uid); ?>"
|
||||||
|
style="display: flex; align-items: center; justify-content: center; width: 93%; padding: 8px; border-radius: 50px; background-color: <?php echo $current_bg; ?>; border: 1px solid <?php echo $current_border; ?>; transition: all 0.3s ease; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; cursor: default; margin: 10px 0;">
|
||||||
|
|
||||||
|
<span id="mcss-sw-dot-<?php echo esc_attr($uid); ?>"
|
||||||
|
style="width: 8px; height: 8px; border-radius: 50%; margin-right: 10px; background-color: <?php echo $current_dot; ?>; animation: <?php echo $current_anim; ?> 2s infinite;"></span>
|
||||||
|
|
||||||
|
<span id="mcss-sw-text-<?php echo esc_attr($uid); ?>"
|
||||||
|
style="font-size: 12px; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; color: <?php echo $current_text; ?>;">
|
||||||
|
<?php echo $status_label; ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kleines Skript für Live-Update (alle 3 Sekunden) -->
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
var uid = "<?php echo esc_js($uid); ?>";
|
||||||
|
var serverId = "<?php echo esc_js($target_srv['id']); ?>";
|
||||||
|
|
||||||
|
// Konstanten Farben
|
||||||
|
var cOnlineBg = '<?php echo esc_js($bg_online); ?>';
|
||||||
|
var cOfflineBg = '<?php echo esc_js($bg_offline); ?>';
|
||||||
|
var cOnlineText = '<?php echo esc_js($text_online); ?>';
|
||||||
|
var cOfflineText = '<?php echo esc_js($text_offline); ?>';
|
||||||
|
var cOnlineDot = '<?php echo esc_js($dot_online); ?>';
|
||||||
|
var cOfflineDot = '<?php echo esc_js($dot_offline); ?>';
|
||||||
|
var bOnline = '<?php echo esc_js($border_online); ?>';
|
||||||
|
var bOffline = '<?php echo esc_js($border_offline); ?>';
|
||||||
|
var aOnline = '<?php echo esc_js($anim_name_online); ?>';
|
||||||
|
var aOffline = '<?php echo esc_js($anim_name_offline); ?>';
|
||||||
|
|
||||||
|
setInterval(function(){
|
||||||
|
fetch('<?php echo admin_url('admin-ajax.php'); ?>', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type':'application/x-www-form-urlencoded'},
|
||||||
|
body: 'action=mcss_fetch&server_id=' + serverId
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(d => {
|
||||||
|
var wrapper = document.getElementById('mcss-sw-wrapper-' + uid);
|
||||||
|
var dot = document.getElementById('mcss-sw-dot-' + uid);
|
||||||
|
var txt = document.getElementById('mcss-sw-text-' + uid);
|
||||||
|
if(wrapper && dot && txt && d) {
|
||||||
|
if(d.online) {
|
||||||
|
wrapper.style.backgroundColor = cOnlineBg;
|
||||||
|
wrapper.style.borderColor = bOnline;
|
||||||
|
dot.style.backgroundColor = cOnlineDot;
|
||||||
|
dot.style.animationName = aOnline;
|
||||||
|
txt.style.color = cOnlineText;
|
||||||
|
txt.textContent = 'Online';
|
||||||
|
} else {
|
||||||
|
wrapper.style.backgroundColor = cOfflineBg;
|
||||||
|
wrapper.style.borderColor = bOffline;
|
||||||
|
dot.style.backgroundColor = cOfflineDot;
|
||||||
|
dot.style.animationName = aOffline;
|
||||||
|
txt.style.color = cOfflineText;
|
||||||
|
txt.textContent = 'Offline';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 3000);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<?php
|
||||||
|
} else {
|
||||||
|
echo '<p>Bitte Server in den Einstellungen wählen.</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo $args['after_widget'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Widget registrieren
|
||||||
|
add_action( 'widgets_init', function(){
|
||||||
|
register_widget( 'MCSS_Sidebar_Status_Widget' );
|
||||||
|
});
|
||||||
92
README.md
92
README.md
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
StatusAPI zeigt den aktuellen Status deines Minecraft-Servers direkt auf deiner Webseite an – inklusive Online/Offline, Version, Ping, Spieleranzahl und Spieler-Avatare.
|
StatusAPI zeigt den aktuellen Status deines Minecraft-Servers direkt auf deiner Webseite an – inklusive Online/Offline, Version, Ping, Spieleranzahl und Spieler-Avatare.
|
||||||
|
|
||||||
|
**StatusAPI Repository:** [https://git.viper.ipv64.net/M_Viper/StatusAPI](https://git.viper.ipv64.net/M_Viper/StatusAPI)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Zeigt Serverstatus: **Online / Offline**
|
- Zeigt Serverstatus: **Online / Offline**
|
||||||
@@ -12,26 +16,67 @@ StatusAPI zeigt den aktuellen Status deines Minecraft-Servers direkt auf deiner
|
|||||||
- Hinweise/Banner können vom Nutzer geschlossen werden
|
- Hinweise/Banner können vom Nutzer geschlossen werden
|
||||||
- Anpassbare Darstellung für verschiedene Layouts
|
- Anpassbare Darstellung für verschiedene Layouts
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Wichtige Hinweise
|
## Wichtige Hinweise
|
||||||
|
|
||||||
### BungeeCord Plugin
|
### BungeeCord Plugin (Voraussetzung)
|
||||||
|
|
||||||
|
⚠️ **Die [StatusAPI](https://git.viper.ipv64.net/M_Viper/StatusAPI) MUSS auf deinem BungeeCord-Server installiert sein!**
|
||||||
|
|
||||||
Das Plugin **StatusAPI.jar** muss in den BungeeCord Plugin-Ordner kopiert werden, damit die API korrekt funktioniert.
|
Das Plugin **StatusAPI.jar** muss in den BungeeCord Plugin-Ordner kopiert werden, damit die API korrekt funktioniert.
|
||||||
|
```text
|
||||||
|
BungeeCord/
|
||||||
|
├─ plugins/
|
||||||
|
│ └─ StatusAPI.jar ← PFLICHT
|
||||||
|
└─ config.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
**Download:** [https://git.viper.ipv64.net/M_Viper/StatusAPI](https://git.viper.ipv64.net/M_Viper/StatusAPI)
|
||||||
|
|
||||||
### WordPress Integration
|
### WordPress Integration
|
||||||
|
|
||||||
In WordPress muss der API-Port auf **9191** eingestellt sein, damit die Serverdaten korrekt abgerufen werden.
|
In WordPress muss der API-Port auf **9191** eingestellt sein, damit die Serverdaten korrekt abgerufen werden.
|
||||||
Stelle sicher, dass dein Server und die Webseite auf diesen Port zugreifen können.
|
Stelle sicher, dass dein Server und die Webseite auf diesen Port zugreifen können.
|
||||||
|
|
||||||
|
Die StatusAPI stellt einen HTTP-Endpunkt bereit (Standard: `http://localhost:9191`), den das WordPress-Plugin abfragt.
|
||||||
|
|
||||||
### Shortcode für WordPress
|
### Shortcode für WordPress
|
||||||
|
|
||||||
Um den Serverstatus auf deiner WordPress-Seite anzuzeigen, füge einfach folgenden Shortcode ein:
|
Um den Serverstatus auf deiner WordPress-Seite anzuzeigen, füge einfach folgenden Shortcode ein:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
[bungeecord_status id="Bungeecord"]
|
[bungeecord_status id="Bungeecord"]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
Ersetze `"Bungeecord"` durch die ID deines Servers, falls mehrere Server eingebunden werden.
|
Ersetze `"Bungeecord"` durch die ID deines Servers, falls mehrere Server eingebunden werden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Schritt 1: StatusAPI auf BungeeCord installieren
|
||||||
|
|
||||||
|
1. Lade die **[StatusAPI](https://git.viper.ipv64.net/M_Viper/StatusAPI)** herunter
|
||||||
|
2. Kopiere die **StatusAPI.jar** in den `plugins`-Ordner deines BungeeCord-Servers
|
||||||
|
3. Starte den BungeeCord-Server neu
|
||||||
|
4. Die API läuft nun standardmäßig auf Port **9191**
|
||||||
|
|
||||||
|
### Schritt 2: WordPress-Plugin einrichten
|
||||||
|
|
||||||
|
1. Installiere das WordPress-Plugin für den BungeeCord-Status
|
||||||
|
2. Gehe zu den Plugin-Einstellungen
|
||||||
|
3. Trage die **StatusAPI URL** ein (z.B. `http://localhost:9191`)
|
||||||
|
4. Speichere die Einstellungen
|
||||||
|
|
||||||
|
### Schritt 3: Shortcode einbinden
|
||||||
|
|
||||||
|
Füge den Shortcode auf einer beliebigen WordPress-Seite ein:
|
||||||
|
```html
|
||||||
|
[bungeecord_status id="Bungeecord"]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Beispielanzeige
|
## Beispielanzeige
|
||||||
|
|
||||||
**Status:** Online
|
**Status:** Online
|
||||||
@@ -42,17 +87,52 @@ Ersetze `"Bungeecord"` durch die ID deines Servers, falls mehrere Server eingebu
|
|||||||
**Avatare:**
|
**Avatare:**
|
||||||
 <!-- Platzhalter – wird im Plugin dynamisch geladen -->
|
 <!-- Platzhalter – wird im Plugin dynamisch geladen -->
|
||||||
|
|
||||||
> Der Abstand zwischen dem Text „Spieler:“ und den Avataren ist bewusst etwas größer, um die Übersichtlichkeit zu erhöhen.
|
> Der Abstand zwischen dem Text „Spieler:" und den Avataren ist bewusst etwas größer, um die Übersichtlichkeit zu erhöhen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Nutzung
|
## Nutzung
|
||||||
|
|
||||||
1. Shortcode an der gewünschten Stelle einfügen
|
1. Shortcode an der gewünschten Stelle einfügen
|
||||||
2. Der Serverstatus aktualisiert sich automatisch, sodass die angezeigten Spieler und der Ping immer aktuell sind
|
2. Der Serverstatus aktualisiert sich automatisch, sodass die angezeigten Spieler und der Ping immer aktuell sind
|
||||||
|
3. Alle Daten werden live von der StatusAPI bezogen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technische Details
|
||||||
|
|
||||||
|
- **API-Port:** 9191 (Standard)
|
||||||
|
- **Update-Intervall:** 2 Sekunden (automatisch)
|
||||||
|
- **Protokoll:** HTTP/JSON
|
||||||
|
- **Datenquelle:** StatusAPI auf BungeeCord
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
Bei Problemen überprüfe bitte:
|
Bei Problemen überprüfe bitte:
|
||||||
|
|
||||||
- Ob das Plugin **StatusAPI.jar** korrekt im BungeeCord Plugin-Ordner liegt
|
- Ob das Plugin **[StatusAPI.jar](https://git.viper.ipv64.net/M_Viper/StatusAPI)** korrekt im BungeeCord Plugin-Ordner liegt
|
||||||
- Ob der API-Port in WordPress korrekt auf **9191** eingestellt ist
|
- Ob der API-Port in WordPress korrekt auf **9191** eingestellt ist
|
||||||
- Ob der Server erreichbar ist und Spieler online sind
|
- Ob der BungeeCord-Server läuft und die StatusAPI erreichbar ist
|
||||||
|
- Ob keine Firewall den Port 9191 blockiert
|
||||||
|
- Ob die StatusAPI gültige JSON-Daten zurückliefert
|
||||||
|
|
||||||
|
### Häufige Fehler
|
||||||
|
|
||||||
|
❌ **"Server offline" obwohl Server läuft**
|
||||||
|
- StatusAPI ist nicht installiert oder läuft nicht
|
||||||
|
- Falscher Port in WordPress eingestellt
|
||||||
|
- Firewall blockiert Port 9191
|
||||||
|
|
||||||
|
❌ **Keine Spieler werden angezeigt**
|
||||||
|
- StatusAPI liefert keine Spielerdaten
|
||||||
|
- API-Verbindung fehlgeschlagen
|
||||||
|
- WordPress kann die API-URL nicht erreichen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Weitere Informationen
|
||||||
|
|
||||||
|
Für detaillierte Informationen zur Installation, Konfiguration und Fehlerbehebung der StatusAPI:
|
||||||
|
**[https://git.viper.ipv64.net/M_Viper/StatusAPI](https://git.viper.ipv64.net/M_Viper/StatusAPI)**
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<groupId>net.viper.bungee</groupId>
|
<groupId>net.viper.bungee</groupId>
|
||||||
<artifactId>StatusAPI</artifactId>
|
<artifactId>StatusAPI</artifactId>
|
||||||
<version>1.1</version>
|
<version>3.6.2</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<name>StatusAPI</name>
|
<name>StatusAPI</name>
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<!-- BungeeCord API (lokal installiert) -->
|
<!-- BungeeCord API -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>net.md-5</groupId>
|
<groupId>net.md-5</groupId>
|
||||||
<artifactId>bungeecord-api</artifactId>
|
<artifactId>bungeecord-api</artifactId>
|
||||||
@@ -28,12 +28,13 @@
|
|||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- LuckPerms API für Prefix Support -->
|
<!-- LuckPerms API (Optional) -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>net.luckperms</groupId>
|
<groupId>net.luckperms</groupId>
|
||||||
<artifactId>api</artifactId>
|
<artifactId>api</artifactId>
|
||||||
<version>5.4</version>
|
<version>5.4</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
|||||||
59
StatusAPI/src/main/java/net/viper/status/FileDownloader.java
Normal file
59
StatusAPI/src/main/java/net/viper/status/FileDownloader.java
Normal 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) {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
package net.viper.status;
|
package net.viper.status;
|
||||||
|
|
||||||
import net.luckperms.api.LuckPerms;
|
|
||||||
import net.luckperms.api.LuckPermsProvider;
|
|
||||||
import net.luckperms.api.model.user.User;
|
|
||||||
import net.luckperms.api.query.QueryOptions;
|
|
||||||
import net.md_5.bungee.api.ProxyServer;
|
import net.md_5.bungee.api.ProxyServer;
|
||||||
import net.md_5.bungee.api.config.ListenerInfo;
|
import net.md_5.bungee.api.config.ListenerInfo;
|
||||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||||
import net.md_5.bungee.api.plugin.Plugin;
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.BufferedReader;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.PrintWriter;
|
||||||
import java.net.ServerSocket;
|
import java.net.ServerSocket;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@@ -20,6 +21,7 @@ public class StatusAPI extends Plugin implements Runnable {
|
|||||||
private Thread thread;
|
private Thread thread;
|
||||||
private int port = 9191;
|
private int port = 9191;
|
||||||
private UpdateChecker updateChecker;
|
private UpdateChecker updateChecker;
|
||||||
|
private FileDownloader fileDownloader;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
@@ -30,18 +32,33 @@ public class StatusAPI extends Plugin implements Runnable {
|
|||||||
thread = new Thread(this, "StatusAPI-HTTP-Server");
|
thread = new Thread(this, "StatusAPI-HTTP-Server");
|
||||||
thread.start();
|
thread.start();
|
||||||
|
|
||||||
// Start UpdateChecker: initialer Check (async) + regelmäßiger Schedule
|
|
||||||
String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0";
|
String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0";
|
||||||
updateChecker = new UpdateChecker(this, currentVersion, 6); // 6 Stunden Intervall
|
updateChecker = new UpdateChecker(this, currentVersion, 6); // 6 Stunden Intervall
|
||||||
|
fileDownloader = new FileDownloader(this);
|
||||||
|
|
||||||
// initialer sofortiger Start (async)
|
File pluginFile = getFile();
|
||||||
ProxyServer.getInstance().getScheduler().runAsync(this, () -> updateChecker.checkNow());
|
File backupFile = new File(pluginFile.getParentFile(), "StatusAPI.jar.bak");
|
||||||
|
|
||||||
// planmäßiger Intervall (alle 6 Stunden)
|
// --- AUTOMATISCHES BACKUP-CLEANUP ---
|
||||||
ProxyServer.getInstance().getScheduler().schedule(this, updateChecker, 6, 6, TimeUnit.HOURS);
|
// Falls ein altes Update (.bak) im Ordner liegt, löschen wir es nach 1 Minute Startzeit.
|
||||||
|
// Wenn der Server kurz crashen würde, hast du noch 60 Sekunden Zeit, ihn zu stoppen,
|
||||||
|
// damit das Backup erhalten bleibt. Läuft er stabil, wird der Platz freigegeben.
|
||||||
|
if (backupFile.exists()) {
|
||||||
|
ProxyServer.getInstance().getScheduler().schedule(this, () -> {
|
||||||
|
if (backupFile.exists()) {
|
||||||
|
if (backupFile.delete()) {
|
||||||
|
getLogger().info("Altes Backup (.bak) wurde erfolgreich gelöscht.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 1, TimeUnit.MINUTES);
|
||||||
|
}
|
||||||
|
// ---------------------------------------
|
||||||
|
|
||||||
// Register join listener to notify OPs on login
|
// Sofortiger Start-Check
|
||||||
ProxyServer.getInstance().getPluginManager().registerListener(this, new UpdateListener(this, updateChecker));
|
checkAndMaybeUpdate();
|
||||||
|
|
||||||
|
// Regelmäßiger Check (alle 6 Stunden)
|
||||||
|
ProxyServer.getInstance().getScheduler().schedule(this, this::checkAndMaybeUpdate, 6, 6, TimeUnit.HOURS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -49,9 +66,99 @@ public class StatusAPI extends Plugin implements Runnable {
|
|||||||
getLogger().info("Stoppe Web-Server...");
|
getLogger().info("Stoppe Web-Server...");
|
||||||
if (thread != null) {
|
if (thread != null) {
|
||||||
thread.interrupt();
|
thread.interrupt();
|
||||||
|
try { thread.join(1000); } catch (InterruptedException ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft Update und startet Download falls nötig.
|
||||||
|
*/
|
||||||
|
private void checkAndMaybeUpdate() {
|
||||||
try {
|
try {
|
||||||
thread.join(1000);
|
updateChecker.checkNow();
|
||||||
} catch (InterruptedException ignored) {}
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,189 +166,124 @@ public class StatusAPI extends Plugin implements Runnable {
|
|||||||
public void run() {
|
public void run() {
|
||||||
try (ServerSocket serverSocket = new ServerSocket(port)) {
|
try (ServerSocket serverSocket = new ServerSocket(port)) {
|
||||||
serverSocket.setSoTimeout(1000);
|
serverSocket.setSoTimeout(1000);
|
||||||
|
|
||||||
while (!Thread.interrupted()) {
|
while (!Thread.interrupted()) {
|
||||||
try {
|
try {
|
||||||
Socket clientSocket = serverSocket.accept();
|
Socket clientSocket = serverSocket.accept();
|
||||||
handleConnection(clientSocket);
|
handleConnection(clientSocket);
|
||||||
} catch (java.net.SocketTimeoutException e) {
|
} catch (java.net.SocketTimeoutException e) {}
|
||||||
// Loop Check (timeout) - erlaubt Unterbrechungsschleife
|
catch (IOException e) {
|
||||||
} catch (IOException e) {
|
|
||||||
getLogger().warning("Fehler beim Akzeptieren einer Verbindung: " + e.getMessage());
|
getLogger().warning("Fehler beim Akzeptieren einer Verbindung: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
getLogger().severe("Konnte ServerSocket nicht starten auf Port " + port + ": " + e.getMessage());
|
getLogger().severe("Konnte ServerSocket nicht starten auf Port " + port + ": " + e.getMessage());
|
||||||
e.printStackTrace();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleConnection(Socket clientSocket) {
|
private void handleConnection(Socket clientSocket) {
|
||||||
BufferedReader in = null;
|
try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), "UTF-8"));
|
||||||
OutputStream out = null;
|
OutputStream out = clientSocket.getOutputStream()) {
|
||||||
|
|
||||||
try {
|
|
||||||
in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), "UTF-8"));
|
|
||||||
out = clientSocket.getOutputStream();
|
|
||||||
|
|
||||||
String inputLine = in.readLine();
|
String inputLine = in.readLine();
|
||||||
|
|
||||||
if (inputLine != null && inputLine.startsWith("GET")) {
|
if (inputLine != null && inputLine.startsWith("GET")) {
|
||||||
|
|
||||||
Map<String, Object> data = new LinkedHashMap<>();
|
Map<String, Object> data = new LinkedHashMap<>();
|
||||||
data.put("online", true);
|
data.put("online", true);
|
||||||
|
|
||||||
// --- VERSION CLEANUP START ---
|
// Version
|
||||||
String versionRaw = ProxyServer.getInstance().getVersion();
|
String versionRaw = ProxyServer.getInstance().getVersion();
|
||||||
String versionClean = versionRaw;
|
String versionClean = versionRaw;
|
||||||
|
|
||||||
if (versionRaw != null && versionRaw.contains(":")) {
|
if (versionRaw != null && versionRaw.contains(":")) {
|
||||||
String[] parts = versionRaw.split(":");
|
String[] parts = versionRaw.split(":");
|
||||||
if (parts.length >= 3) {
|
if (parts.length >= 3) versionClean = parts[2].trim();
|
||||||
versionClean = parts[2].trim();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
data.put("version", versionClean);
|
data.put("version", versionClean);
|
||||||
// --- VERSION CLEANUP ENDE ---
|
|
||||||
|
|
||||||
data.put("max_players", String.valueOf(ProxyServer.getInstance().getConfig().getPlayerLimit()));
|
data.put("max_players", String.valueOf(ProxyServer.getInstance().getConfig().getPlayerLimit()));
|
||||||
|
|
||||||
|
// Motd
|
||||||
String motd = "BungeeCord";
|
String motd = "BungeeCord";
|
||||||
try {
|
try {
|
||||||
Iterator<ListenerInfo> it = ProxyServer.getInstance().getConfig().getListeners().iterator();
|
Iterator<ListenerInfo> it = ProxyServer.getInstance().getConfig().getListeners().iterator();
|
||||||
if (it.hasNext()) {
|
if (it.hasNext()) {
|
||||||
ListenerInfo listener = it.next();
|
ListenerInfo listener = it.next();
|
||||||
if (listener != null && listener.getMotd() != null) {
|
if (listener != null && listener.getMotd() != null) motd = listener.getMotd();
|
||||||
motd = listener.getMotd();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Fallback bleibt "BungeeCord"
|
|
||||||
}
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
data.put("motd", motd);
|
data.put("motd", motd);
|
||||||
|
|
||||||
// --- LUCKPERMS INTEGRATION START ---
|
// LuckPerms (Optional)
|
||||||
// LuckPerms API über Provider holen (Verhindert ClassCastException)
|
boolean luckPermsEnabled = ProxyServer.getInstance().getPluginManager().getPlugin("LuckPerms") != null;
|
||||||
LuckPerms luckPermsApi = null;
|
|
||||||
try {
|
|
||||||
luckPermsApi = LuckPermsProvider.get();
|
|
||||||
} catch (IllegalStateException e) {
|
|
||||||
// LuckPerms ist nicht geladen
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Map<String, String>> playersList = new ArrayList<>();
|
List<Map<String, String>> playersList = new ArrayList<>();
|
||||||
|
|
||||||
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
||||||
Map<String, String> playerInfo = new LinkedHashMap<>();
|
Map<String, String> playerInfo = new LinkedHashMap<>();
|
||||||
playerInfo.put("name", p.getName());
|
playerInfo.put("name", p.getName());
|
||||||
|
|
||||||
// Prefix abfragen, falls LP gefunden wurde
|
|
||||||
String prefix = "";
|
String prefix = "";
|
||||||
if (luckPermsApi != null) {
|
if (luckPermsEnabled) {
|
||||||
User user = luckPermsApi.getUserManager().getUser(p.getUniqueId());
|
try {
|
||||||
|
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) {
|
if (user != null) {
|
||||||
// Context bestimmen (Global oder Server-spezifisch)
|
Class<?> queryOptionsClass = Class.forName("net.luckperms.api.query.QueryOptions");
|
||||||
QueryOptions queryOptions = luckPermsApi.getContextManager().getQueryOptions(user)
|
Object queryOptions = queryOptionsClass.getMethod("defaultContextualOptions").invoke(null);
|
||||||
.orElse(QueryOptions.defaultContextualOptions());
|
Object cachedData = user.getClass().getMethod("getCachedData").invoke(user);
|
||||||
|
Object metaData = cachedData.getClass().getMethod("getMetaData", queryOptionsClass).invoke(cachedData, queryOptions);
|
||||||
String lpPrefix = user.getCachedData().getMetaData(queryOptions).getPrefix();
|
Object result = metaData.getClass().getMethod("getPrefix").invoke(metaData);
|
||||||
if (lpPrefix != null) {
|
if (result != null) prefix = (String) result;
|
||||||
prefix = lpPrefix;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
}
|
}
|
||||||
playerInfo.put("prefix", prefix);
|
playerInfo.put("prefix", prefix);
|
||||||
playersList.add(playerInfo);
|
playersList.add(playerInfo);
|
||||||
}
|
}
|
||||||
data.put("players", playersList);
|
data.put("players", playersList);
|
||||||
// --- LUCKPERMS INTEGRATION ENDE ---
|
|
||||||
|
|
||||||
|
// Response
|
||||||
String json = buildJsonString(data);
|
String json = buildJsonString(data);
|
||||||
byte[] jsonBytes = json.getBytes("UTF-8");
|
byte[] jsonBytes = json.getBytes("UTF-8");
|
||||||
|
|
||||||
// HTTP Response mit korrekter Byte-Length
|
|
||||||
StringBuilder response = new StringBuilder();
|
StringBuilder response = new StringBuilder();
|
||||||
response.append("HTTP/1.1 200 OK\r\n");
|
response.append("HTTP/1.1 200 OK\r\n");
|
||||||
response.append("Content-Type: application/json; charset=UTF-8\r\n");
|
response.append("Content-Type: application/json; charset=UTF-8\r\n");
|
||||||
response.append("Access-Control-Allow-Origin: *\r\n");
|
response.append("Access-Control-Allow-Origin: *\r\n");
|
||||||
response.append("Content-Length: ").append(jsonBytes.length).append("\r\n");
|
response.append("Content-Length: ").append(jsonBytes.length).append("\r\n");
|
||||||
response.append("Connection: close\r\n");
|
response.append("Connection: close\r\n\r\n");
|
||||||
response.append("\r\n");
|
|
||||||
|
|
||||||
// Header senden
|
|
||||||
out.write(response.toString().getBytes("UTF-8"));
|
out.write(response.toString().getBytes("UTF-8"));
|
||||||
// Body senden
|
|
||||||
out.write(jsonBytes);
|
out.write(jsonBytes);
|
||||||
out.flush();
|
out.flush();
|
||||||
} else {
|
|
||||||
// Ungültige Anfrage -> 400
|
|
||||||
String resp = "HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n";
|
|
||||||
out.write(resp.getBytes("UTF-8"));
|
|
||||||
out.flush();
|
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
getLogger().severe("Fehler beim Verarbeiten der Anfrage: " + e.getMessage());
|
getLogger().severe("Fehler beim Verarbeiten der Anfrage: " + e.getMessage());
|
||||||
e.printStackTrace();
|
|
||||||
} finally {
|
|
||||||
// Sauber aufräumen
|
|
||||||
try {
|
|
||||||
if (out != null) out.close();
|
|
||||||
if (in != null) in.close();
|
|
||||||
if (clientSocket != null && !clientSocket.isClosed()) {
|
|
||||||
clientSocket.close();
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
// Ignorieren
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Rekursiver JSON Builder, der nun auch Listen von Objekten (Maps) verarbeiten kann.
|
|
||||||
*/
|
|
||||||
private String buildJsonString(Map<String, Object> data) {
|
private String buildJsonString(Map<String, Object> data) {
|
||||||
StringBuilder sb = new StringBuilder("{");
|
StringBuilder sb = new StringBuilder("{");
|
||||||
boolean first = true;
|
boolean first = true;
|
||||||
for (Map.Entry<String, Object> entry : data.entrySet()) {
|
for (Map.Entry<String, Object> entry : data.entrySet()) {
|
||||||
if (!first) sb.append(",");
|
if (!first) sb.append(",");
|
||||||
first = false;
|
first = false;
|
||||||
sb.append("\"").append(escapeJson(entry.getKey())).append("\":");
|
sb.append("\"").append(escapeJson(entry.getKey())).append("\":").append(valueToString(entry.getValue()));
|
||||||
|
|
||||||
// Wert verarbeiten (String, Boolean, List oder Map)
|
|
||||||
sb.append(valueToString(entry.getValue()));
|
|
||||||
}
|
}
|
||||||
sb.append("}");
|
sb.append("}");
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Hilfsmethode, um verschiedene Objekttypen in JSON-Strings umzuwandeln.
|
|
||||||
*/
|
|
||||||
private String valueToString(Object value) {
|
private String valueToString(Object value) {
|
||||||
if (value == null) {
|
if (value == null) return "null";
|
||||||
return "null";
|
else if (value instanceof Boolean) return value.toString();
|
||||||
} else if (value instanceof Boolean) {
|
else if (value instanceof List) {
|
||||||
return value.toString();
|
|
||||||
} else if (value instanceof List) {
|
|
||||||
StringBuilder sb = new StringBuilder("[");
|
StringBuilder sb = new StringBuilder("[");
|
||||||
List<?> list = (List<?>) value;
|
List<?> list = (List<?>) value;
|
||||||
for (int i = 0; i < list.size(); i++) {
|
for (int i = 0; i < list.size(); i++) {
|
||||||
if (i > 0) sb.append(",");
|
if (i > 0) sb.append(",");
|
||||||
Object item = list.get(i);
|
Object item = list.get(i);
|
||||||
// Wenn es eine Map ist (Player-Objekt), rufen wir buildJsonString rekursiv auf
|
if (item instanceof Map) sb.append(buildJsonString((Map<String, Object>) item));
|
||||||
if (item instanceof Map) {
|
else sb.append("\"").append(escapeJson(String.valueOf(item))).append("\"");
|
||||||
sb.append(buildJsonString((Map<String, Object>) item));
|
|
||||||
} else {
|
|
||||||
// Andernfalls String behandeln
|
|
||||||
sb.append("\"").append(escapeJson(String.valueOf(item))).append("\"");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
sb.append("]");
|
sb.append("]");
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
} else {
|
|
||||||
// Standard String Behandlung
|
|
||||||
return "\"" + escapeJson(String.valueOf(value)) + "\"";
|
|
||||||
}
|
}
|
||||||
|
else return "\"" + escapeJson(String.valueOf(value)) + "\"";
|
||||||
}
|
}
|
||||||
|
|
||||||
private String escapeJson(String s) {
|
private String escapeJson(String s) {
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
package net.viper.status;
|
package net.viper.status;
|
||||||
|
|
||||||
import net.md_5.bungee.api.plugin.Plugin;
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
import net.md_5.bungee.api.ProxyServer;
|
|
||||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
|
||||||
import net.md_5.bungee.api.chat.TextComponent;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
@@ -13,31 +10,23 @@ import java.util.logging.Level;
|
|||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
public class UpdateChecker {
|
||||||
* UpdateChecker:
|
|
||||||
* - Fragt die Gitea Releases API ab
|
|
||||||
* - Sucht in den Releases nach dem Asset "StatusAPI.jar"
|
|
||||||
* - Extrahiert die Version (tag_name bevorzugt, name als Fallback)
|
|
||||||
* - Cache die gefundene Version + URL (volatile fields)
|
|
||||||
*/
|
|
||||||
public class UpdateChecker implements Runnable {
|
|
||||||
|
|
||||||
private final Plugin plugin;
|
private final Plugin plugin;
|
||||||
private final String currentVersion;
|
private final String currentVersion;
|
||||||
private final int intervalHours;
|
private final int intervalHours;
|
||||||
|
|
||||||
// Gitea Releases API für dein Repo
|
|
||||||
private final String apiUrl = "https://git.viper.ipv64.net/api/v1/repos/M_Viper/Minecraft-BungeeCord-Status/releases";
|
private final String apiUrl = "https://git.viper.ipv64.net/api/v1/repos/M_Viper/Minecraft-BungeeCord-Status/releases";
|
||||||
|
|
||||||
// cached results (volatile für Thread-Sichtbarkeit)
|
|
||||||
private volatile String latestVersion = "";
|
private volatile String latestVersion = "";
|
||||||
private volatile String latestUrl = "";
|
private volatile String latestUrl = "";
|
||||||
|
|
||||||
// Patterns for quick parse (we parse JSON as text; this is lightweight and robust for our needs)
|
// Pattern für Dateinamen im Assets-Array
|
||||||
private static final Pattern ASSET_NAME_PATTERN = Pattern.compile("\"name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
|
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);
|
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);
|
private static final Pattern TAG_NAME_PATTERN = Pattern.compile("\"tag_name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
|
||||||
private static final Pattern NAME_FIELD_PATTERN = Pattern.compile("\"name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
|
|
||||||
|
|
||||||
public UpdateChecker(Plugin plugin, String currentVersion, int intervalHours) {
|
public UpdateChecker(Plugin plugin, String currentVersion, int intervalHours) {
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
@@ -45,23 +34,12 @@ public class UpdateChecker implements Runnable {
|
|||||||
this.intervalHours = Math.max(1, intervalHours);
|
this.intervalHours = Math.max(1, intervalHours);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// scheduled task calls checkNow
|
|
||||||
checkNow();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Führt einen sofortigen Check aus und updated cache (synchron for the calling thread).
|
|
||||||
*/
|
|
||||||
public void checkNow() {
|
public void checkNow() {
|
||||||
try {
|
try {
|
||||||
plugin.getLogger().info("Prüfe StatusAPI Releases via Gitea API...");
|
|
||||||
|
|
||||||
HttpURLConnection conn = (HttpURLConnection) new URL(apiUrl).openConnection();
|
HttpURLConnection conn = (HttpURLConnection) new URL(apiUrl).openConnection();
|
||||||
conn.setRequestMethod("GET");
|
conn.setRequestMethod("GET");
|
||||||
conn.setRequestProperty("Accept", "application/json");
|
conn.setRequestProperty("Accept", "application/json");
|
||||||
conn.setRequestProperty("User-Agent", "StatusAPI-UpdateChecker/1.0");
|
conn.setRequestProperty("User-Agent", "StatusAPI-UpdateChecker/2.0");
|
||||||
conn.setConnectTimeout(5000);
|
conn.setConnectTimeout(5000);
|
||||||
conn.setReadTimeout(5000);
|
conn.setReadTimeout(5000);
|
||||||
|
|
||||||
@@ -74,25 +52,37 @@ public class UpdateChecker implements Runnable {
|
|||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"))) {
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"))) {
|
||||||
String line;
|
String line;
|
||||||
while ((line = br.readLine()) != null) {
|
while ((line = br.readLine()) != null) sb.append(line).append("\n");
|
||||||
sb.append(line).append("\n");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String body = sb.toString();
|
String body = sb.toString();
|
||||||
|
|
||||||
// Suche Release mit Asset "StatusAPI.jar"
|
// 1. Die LATEST Version (Tag) finden
|
||||||
|
// Wir suchen das erste "tag_name" im gesamten JSON, das ist meistens das neueste Release.
|
||||||
String foundVersion = null;
|
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;
|
String foundUrl = null;
|
||||||
|
|
||||||
// Split releases by top-level objects: we simply find repeating {...} blocks and inspect them
|
|
||||||
// This is robust enough for Gitea JSON responses.
|
|
||||||
Pattern releasePattern = Pattern.compile("(?s)\\{.*?\\}");
|
Pattern releasePattern = Pattern.compile("(?s)\\{.*?\\}");
|
||||||
Matcher releaseMatcher = releasePattern.matcher(body);
|
Matcher releaseMatcher = releasePattern.matcher(body);
|
||||||
while (releaseMatcher.find()) {
|
while (releaseMatcher.find()) {
|
||||||
String block = releaseMatcher.group();
|
String block = releaseMatcher.group();
|
||||||
|
|
||||||
// extract asset names & download urls inside the block
|
|
||||||
java.util.List<String> names = new java.util.ArrayList<>();
|
java.util.List<String> names = new java.util.ArrayList<>();
|
||||||
java.util.List<String> downloads = new java.util.ArrayList<>();
|
java.util.List<String> downloads = new java.util.ArrayList<>();
|
||||||
|
|
||||||
@@ -107,37 +97,20 @@ public class UpdateChecker implements Runnable {
|
|||||||
String name = names.get(i);
|
String name = names.get(i);
|
||||||
String dl = downloads.get(i);
|
String dl = downloads.get(i);
|
||||||
if ("StatusAPI.jar".equalsIgnoreCase(name.trim())) {
|
if ("StatusAPI.jar".equalsIgnoreCase(name.trim())) {
|
||||||
// find tag_name or name field in the same block
|
|
||||||
Matcher tagM = TAG_NAME_PATTERN.matcher(block);
|
|
||||||
if (tagM.find()) {
|
|
||||||
foundVersion = tagM.group(1).trim();
|
|
||||||
} else {
|
|
||||||
Matcher nameFieldM = NAME_FIELD_PATTERN.matcher(block);
|
|
||||||
if (nameFieldM.find()) {
|
|
||||||
foundVersion = nameFieldM.group(1).trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
foundUrl = dl;
|
foundUrl = dl;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (foundUrl != null) break;
|
if (foundUrl != null) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (foundVersion == null || foundUrl == null) {
|
if (foundUrl == null) {
|
||||||
plugin.getLogger().info("Kein Release mit Asset 'StatusAPI.jar' gefunden.");
|
plugin.getLogger().warning("Keine JAR-Datei für dieses Release gefunden.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize version
|
plugin.getLogger().info("Gefundene Version: " + foundVersion + " (Aktuell: " + currentVersion + ")");
|
||||||
if (foundVersion.startsWith("v") || foundVersion.startsWith("V")) {
|
|
||||||
foundVersion = foundVersion.substring(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
plugin.getLogger().info("Gefundene Version: " + foundVersion + " (aktuell: " + currentVersion + ")");
|
|
||||||
|
|
||||||
// set cache
|
|
||||||
latestVersion = foundVersion;
|
latestVersion = foundVersion;
|
||||||
latestUrl = foundUrl;
|
latestUrl = foundUrl;
|
||||||
|
|
||||||
@@ -146,32 +119,20 @@ public class UpdateChecker implements Runnable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gibt die zuletzt gecachte Version (oder empty string).
|
|
||||||
*/
|
|
||||||
public String getLatestVersion() {
|
public String getLatestVersion() {
|
||||||
return latestVersion != null ? latestVersion : "";
|
return latestVersion != null ? latestVersion : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gibt die zuletzt gecachte download-URL (oder empty string).
|
|
||||||
*/
|
|
||||||
public String getLatestUrl() {
|
public String getLatestUrl() {
|
||||||
return latestUrl != null ? latestUrl : "";
|
return latestUrl != null ? latestUrl : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Prüft ob die gecachte latestVersion größer ist als übergebene version.
|
|
||||||
*/
|
|
||||||
public boolean isUpdateAvailable(String currentVer) {
|
public boolean isUpdateAvailable(String currentVer) {
|
||||||
String lv = getLatestVersion();
|
String lv = getLatestVersion();
|
||||||
if (lv.isEmpty()) return false;
|
if (lv.isEmpty()) return false;
|
||||||
return compareVersions(lv, currentVer) > 0;
|
return compareVersions(lv, currentVer) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Einfacher SemVer-Vergleich (1.2.3). Liefert >0 wenn a>b, 0 wenn gleich, <0 wenn a<b.
|
|
||||||
*/
|
|
||||||
private int compareVersions(String a, String b) {
|
private int compareVersions(String a, String b) {
|
||||||
try {
|
try {
|
||||||
String[] aa = a.split("\\.");
|
String[] aa = a.split("\\.");
|
||||||
@@ -187,25 +148,4 @@ public class UpdateChecker implements Runnable {
|
|||||||
return a.compareTo(b);
|
return a.compareTo(b);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Convenience: prüft cached state und falls update vorhanden, notifies online players with given permission(s).
|
|
||||||
*/
|
|
||||||
public void notifyOnlineOpsIfAvailable(String[] perms) {
|
|
||||||
String lv = getLatestVersion();
|
|
||||||
String url = getLatestUrl();
|
|
||||||
if (lv.isEmpty()) return;
|
|
||||||
|
|
||||||
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
|
||||||
try {
|
|
||||||
for (String perm : perms) {
|
|
||||||
if (p.hasPermission(perm)) {
|
|
||||||
p.sendMessage(new TextComponent("§6[StatusAPI] §eNeue Version verfügbar: §a" + lv));
|
|
||||||
p.sendMessage(new TextComponent("§eDownload: §b" + url));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
package net.viper.status;
|
|
||||||
|
|
||||||
import net.md_5.bungee.api.ProxyServer;
|
|
||||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
|
||||||
import net.md_5.bungee.api.event.PostLoginEvent;
|
|
||||||
import net.md_5.bungee.api.plugin.Listener;
|
|
||||||
import net.md_5.bungee.event.EventHandler;
|
|
||||||
import net.md_5.bungee.api.chat.TextComponent;
|
|
||||||
import net.md_5.bungee.api.plugin.Plugin;
|
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listener: Benachrichtigt OP-Spieler beim Login, falls ein Update bekannt ist oder führt
|
|
||||||
* asynchronen Einzelcheck durch und benachrichtigt den Spieler danach.
|
|
||||||
*/
|
|
||||||
public class UpdateListener implements Listener {
|
|
||||||
|
|
||||||
private final Plugin plugin;
|
|
||||||
private final UpdateChecker checker;
|
|
||||||
private final String[] notifyPerms = new String[] { "statusapi.update.notify", "statusapi.notify", "bungeecord.command.alert" };
|
|
||||||
|
|
||||||
public UpdateListener(Plugin plugin, UpdateChecker checker) {
|
|
||||||
this.plugin = plugin;
|
|
||||||
this.checker = checker;
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler
|
|
||||||
public void onPostLogin(PostLoginEvent event) {
|
|
||||||
ProxiedPlayer player = event.getPlayer();
|
|
||||||
// Only notify players with the notify permission(s)
|
|
||||||
boolean hasPerm = false;
|
|
||||||
for (String perm : notifyPerms) {
|
|
||||||
if (player.hasPermission(perm)) { hasPerm = true; break; }
|
|
||||||
}
|
|
||||||
if (!hasPerm) return;
|
|
||||||
|
|
||||||
String currentVersion = plugin.getDescription() != null ? plugin.getDescription().getVersion() : "0.0.0";
|
|
||||||
|
|
||||||
// If we already have a cached update and it's newer -> notify immediately
|
|
||||||
if (checker.isUpdateAvailable(currentVersion)) {
|
|
||||||
String lv = checker.getLatestVersion();
|
|
||||||
String url = checker.getLatestUrl();
|
|
||||||
player.sendMessage(new TextComponent("§6[StatusAPI] §eNeue Version verfügbar: §a" + lv));
|
|
||||||
player.sendMessage(new TextComponent("§eDownload: §b" + url));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No cached update yet -> run an async check for this player and notify if found
|
|
||||||
ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
|
|
||||||
// perform network call
|
|
||||||
checker.checkNow();
|
|
||||||
|
|
||||||
if (checker.isUpdateAvailable(currentVersion)) {
|
|
||||||
String lv = checker.getLatestVersion();
|
|
||||||
String url = checker.getLatestUrl();
|
|
||||||
|
|
||||||
// ensure player is still online before sending message
|
|
||||||
ProxiedPlayer p = ProxyServer.getInstance().getPlayer(player.getUniqueId());
|
|
||||||
if (p != null) {
|
|
||||||
p.sendMessage(new TextComponent("§6[StatusAPI] §eNeue Version verfügbar: §a" + lv));
|
|
||||||
p.sendMessage(new TextComponent("§eDownload: §b" + url));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
name: StatusAPI
|
name: StatusAPI
|
||||||
main: net.viper.status.StatusAPI
|
main: net.viper.status.StatusAPI
|
||||||
version: 1.1
|
version: 3.6.2
|
||||||
author: M_Viper
|
author: M_Viper
|
||||||
description: StatusAPI für BungeeCord inkl. Update-Checker
|
description: StatusAPI für BungeeCord inkl. Update-Checker
|
||||||
|
|
||||||
|
softdepend:
|
||||||
|
- LuckPerms
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
statusapi.update.notify:
|
statusapi.update.notify:
|
||||||
description: 'Erlaubt Update-Benachrichtigungen'
|
description: 'Erlaubt Update-Benachrichtigungen'
|
||||||
|
|||||||
Reference in New Issue
Block a user