7 Commits
5 changed files with 961 additions and 617 deletions
Binary file not shown.
+1 -1
View File
@@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "Minecraft Bungee Status", "name": "Minecraft Bungee Status",
"description": "Zeigt den Live-Status mehrerer Bungee/Proxy-Server an. Fallback auf WordPress AJAX möglich.", "description": "Zeigt den Live-Status mehrerer Bungee/Proxy-Server an. Fallback auf WordPress AJAX möglich.",
"version": "1.1.0", "version": "1.1.1",
"action": { "action": {
"default_popup": "popup.html", "default_popup": "popup.html",
"default_icon": { "default_icon": {
+343 -125
View File
@@ -1,20 +1,21 @@
:root { :root {
--bg: #111827; --bg: #0d1117;
--panel: #0b1220; --panel: #161b27;
--panel2: #111620;
--text: #e6eef8; --text: #e6eef8;
--muted: #9aa7b3; --muted: #5a6a7a;
--muted2: #8899aa;
--accent: #3b82f6; --accent: #3b82f6;
--danger: #ef4444; --radius: 9px;
--radius: 8px;
--online: #22c55e; --online: #22c55e;
--offline: #ef4444; --offline: #ef4444;
--border: rgba(255,255,255,0.07);
} }
* { box-sizing: border-box; } * { box-sizing: border-box; }
html, body { html, body {
margin: 0; margin: 0; padding: 0;
padding: 0;
font-family: Inter, Arial, sans-serif; font-family: Inter, Arial, sans-serif;
background: var(--bg); background: var(--bg);
color: var(--text); color: var(--text);
@@ -22,196 +23,413 @@ html, body {
} }
.root { .root {
width: 520px; width: 560px;
padding: 12px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
/* ── Header ── */
header { header {
background: var(--panel);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin-bottom: 8px; padding: 13px 20px;
border-bottom: 1px solid var(--border);
} }
header h1 { font-size: 16px; margin: 0; } header h1 {
font-size: 14px;
font-weight: 700;
margin: 0;
letter-spacing: 0.03em;
color: #fff;
}
header .actions { display: flex; gap: 7px; }
header .actions button { header .actions button {
background: transparent; background: rgba(255,255,255,0.07);
border: 1px solid var(--muted); border: 1px solid rgba(255,255,255,0.1);
color: var(--muted2);
border-radius: 7px;
width: 32px; height: 32px;
cursor: pointer;
font-size: 16px;
display: flex; align-items: center; justify-content: center;
transition: background 0.15s, color 0.15s;
}
header .actions button:hover {
background: rgba(255,255,255,0.12);
color: #fff;
}
/* ── Main layout ── */
.main {
display: flex;
flex-direction: column;
}
/* ── Settings / add-form panel ── */
.settings-panel {
background: var(--panel2);
border-bottom: 1px solid var(--border);
padding: 14px 20px;
display: flex;
flex-direction: column;
gap: 8px;
}
.settings-panel input {
padding: 8px 12px;
border-radius: 7px;
border: 1px solid rgba(255,255,255,0.09);
background: rgba(0,0,0,0.3);
color: var(--text); color: var(--text);
padding: 4px 8px; font-size: 13px;
border-radius: 6px; outline: none;
}
.settings-panel input:focus {
border-color: var(--accent);
}
.settings-panel button {
padding: 8px 12px;
border-radius: 7px;
border: none;
background: var(--accent);
color: white;
font-size: 13px;
font-weight: 600;
cursor: pointer; cursor: pointer;
} }
.main { /* ── Server list ── */
display: flex; .server-list-panel {
gap: 12px; background: var(--panel2);
position: relative; border-bottom: 1px solid var(--border);
align-items: flex-start; padding: 10px 12px;
} }
.server-list {
width: 45%;
background: var(--panel);
padding: 8px;
border-radius: var(--radius);
flex-shrink: 0;
}
.detail {
flex: 1;
background: var(--panel);
padding: 8px;
border-radius: var(--radius);
min-height: 220px;
transition: all 0.3s ease;
}
.detail.full-width {
width: 100%;
flex: 1 1 100%;
}
.add-form { display: flex; flex-direction: column; gap: 6px; margin-bottom: 8px; }
.add-form input { padding: 6px 8px; border-radius: 6px; border: 1px solid #21303b; background: #071019; color: var(--text); }
.add-form button { padding: 6px 8px; border-radius: 6px; border: none; background: var(--accent); color: white; cursor: pointer; }
#serversContainer { #serversContainer {
list-style: none; list-style: none;
margin: 0; margin: 0; padding: 0;
padding: 0; display: flex;
flex-direction: column;
gap: 4px;
} }
.server-item { .server-item {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 8px; padding: 9px 12px;
margin-bottom: 6px; border-radius: 8px;
border-radius: 6px;
cursor: pointer; cursor: pointer;
background: rgba(255,255,255,0.02); background: transparent;
transition: background 0.2s ease; border: 1px solid transparent;
transition: background 0.15s, border-color 0.15s;
} }
.server-item:hover { background: rgba(255,255,255,0.05); } .server-item:hover { background: rgba(255,255,255,0.04); }
.server-item .meta { display: flex; flex-direction: column; } .server-item.selected { background: rgba(59,130,246,0.08); border-color: rgba(59,130,246,0.25); }
.server-item .meta .name { font-weight: 600; }
.server-item .meta { display: flex; flex-direction: column; gap: 2px; }
.server-item .meta .name { font-size: 13px; font-weight: 600; color: #fff; }
.server-item .meta .url { font-size: 11px; color: var(--muted); } .server-item .meta .url { font-size: 11px; color: var(--muted); }
.server-item .status-bubble { .status-bubble {
font-size: 11px; font-size: 11px;
padding: 2px 8px; font-weight: 600;
padding: 3px 10px;
border-radius: 999px; border-radius: 999px;
color: #fff; color: #fff;
background: transparent;
} }
.placeholder { color: var(--muted); padding: 12px; font-size: 13px; } /* ── Detail panel ── */
.hidden { display: none; } .detail-panel {
padding: 18px 20px;
display: flex;
flex-direction: column;
gap: 16px;
min-height: 200px;
}
.detail-header { .placeholder {
color: var(--muted);
font-size: 13px;
padding: 24px 0;
text-align: center;
}
/* Server identity + online badge */
.srv-identity {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: flex-start;
margin-bottom: 12px;
} }
.server-identity h2 { margin: 0; font-size: 18px; line-height: 1.2; } .srv-name { font-size: 20px; font-weight: 700; color: #fff; margin: 0 0 4px; }
.server-url { display: block; font-size: 12px; color: var(--muted); margin-top: 2px; } .srv-url { font-size: 12px; color: var(--muted); }
.status-wrapper { .online-badge {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 7px;
font-weight: 600; border-radius: 999px;
padding: 5px 14px;
font-size: 13px; font-size: 13px;
font-weight: 600;
}
.online-badge.online {
background: rgba(34,197,94,0.12);
border: 1px solid rgba(34,197,94,0.25);
color: #4ade80;
}
.online-badge.offline {
background: rgba(239,68,68,0.1);
border: 1px solid rgba(239,68,68,0.2);
color: #f87171;
} }
.pulsing-dot { .pulsing-dot {
width: 10px; height: 10px; border-radius: 50%; width: 7px; height: 7px;
background-color: var(--offline); border-radius: 50%;
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); flex-shrink: 0;
animation: pulse-red 2s infinite;
} }
.pulsing-dot.online { .pulsing-dot.online {
background-color: var(--online); background: var(--online);
animation: pulse-green 2s infinite; 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 8px rgba(34, 197, 94, 0);} 100% {box-shadow: 0 0 0 0 rgba(34, 197, 94, 0);} } .pulsing-dot.offline {
@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);} } background: var(--offline);
animation: pulse-red 2s infinite;
}
.detail-stats { @keyframes pulse-green {
display: flex; 0%,100% { box-shadow: 0 0 0 0 rgba(34,197,94,0.5); }
justify-content: space-between; 50% { box-shadow: 0 0 0 5px rgba(34,197,94,0); }
background: rgba(255,255,255,0.05); }
padding: 10px; @keyframes pulse-red {
0%,100% { box-shadow: 0 0 0 0 rgba(239,68,68,0.5); }
50% { box-shadow: 0 0 0 5px rgba(239,68,68,0); }
}
/* MOTD */
.motd-bar {
font-size: 12px;
color: var(--muted2);
background: rgba(255,255,255,0.03);
border-radius: 8px;
padding: 8px 14px;
border-left: 2px solid rgba(59,130,246,0.35);
font-style: italic;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-top: 4px;
}
/* Stats row – 3 gleich breite Karten */
.stats-row {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 10px;
margin-top: 4px;
}
.stat-card {
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius); border-radius: var(--radius);
margin-bottom: 12px; padding: 12px 16px;
font-size: 13px;
} }
.playerList { .stat-label {
list-style: none; font-size: 10px;
padding: 10px 0; color: var(--muted);
margin: 0; text-transform: uppercase;
display: flex; letter-spacing: 0.08em;
flex-wrap: wrap; margin-bottom: 5px;
gap: 12px;
justify-content: center; /* Das hier setzt die Köpfe wieder in die Mitte */
} }
.player-item { .stat-val {
position: relative; font-size: 18px;
font-weight: 700;
color: #fff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.stat-val.small {
font-size: 12px;
padding-top: 4px;
font-weight: 500;
color: var(--muted2);
}
/* Section label */
.section-label {
font-size: 11px;
font-weight: 700;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: 9px;
padding-top: 6px;
border-top: 1px solid var(--border);
}
/* Sub-server chips – immer 3 pro Zeile */
.sub-servers {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.sub-chip {
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
gap: 7px;
border-radius: 8px;
padding: 8px 12px;
font-size: 13px;
font-weight: 600;
border: 1px solid var(--border);
background: var(--panel);
color: var(--muted2);
cursor: default;
transition: background 0.15s;
min-width: 0;
}
.sub-chip.active {
background: rgba(59,130,246,0.1);
border-color: rgba(59,130,246,0.3);
color: #93c5fd;
}
.sub-chip-dot {
width: 7px; height: 7px;
border-radius: 50%;
background: #2a3040;
flex-shrink: 0;
}
.sub-chip.active .sub-chip-dot {
background: var(--accent);
}
.sub-chip-name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
min-width: 0;
}
.sub-chip-count {
font-size: 11px;
font-weight: 500;
opacity: 0.65;
flex-shrink: 0;
margin-left: auto;
padding-left: 4px;
}
/* Players grid – 2 Spalten */
.players-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.player-card {
display: flex;
align-items: center;
gap: 12px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 10px 14px;
transition: background 0.15s;
min-width: 0;
}
.player-card:hover {
background: rgba(255,255,255,0.05);
} }
.player-avatar { .player-avatar {
width: 42px; height: 42px; width: 40px; height: 40px;
border-radius: 6px; border-radius: 7px;
transition: transform 0.2s ease; border: 1px solid rgba(255,255,255,0.1);
box-shadow: 0 2px 6px rgba(0,0,0,0.3); flex-shrink: 0;
border: 2px solid rgba(255,255,255,0.1); image-rendering: pixelated;
} }
.player-avatar:hover { .player-info { flex: 1; min-width: 0; }
transform: scale(1.1);
border-color: var(--accent);
}
.player-hover-info { .player-name {
position: absolute; font-size: 14px;
bottom: -28px; font-weight: 700;
background: rgba(0, 0, 0, 0.95); color: var(--text);
color: white;
padding: 3px 10px;
border-radius: 4px;
font-size: 11px;
white-space: nowrap; white-space: nowrap;
pointer-events: none; overflow: hidden;
opacity: 0; text-overflow: ellipsis;
transform: translateY(5px);
transition: all 0.2s ease;
z-index: 100;
border: 1px solid rgba(255,255,255,0.2);
} }
.player-item:hover .player-hover-info { .player-prefix {
opacity: 1; font-size: 11px;
transform: translateY(0); color: var(--muted2);
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.detailButtons { display: flex; gap: 8px; margin-top: 15px; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 10px; } .player-server-tag {
.detailButtons button { padding: 6px 12px; border-radius: 6px; border: none; cursor: pointer; font-size: 12px; font-weight: 600; } display: inline-block;
#btnEdit { background: #f59e0b; color: #000; } margin-top: 3px;
#btnDelete { background: var(--danger); color: #fff; } font-size: 11px;
font-weight: 600;
background: rgba(59,130,246,0.18);
color: #93c5fd;
border-radius: 5px;
padding: 2px 7px;
}
::-webkit-scrollbar { width: 6px; } /* Edit/Delete buttons */
.detail-buttons {
display: flex;
gap: 10px;
padding-top: 10px;
border-top: 1px solid var(--border);
}
.detail-buttons button {
padding: 7px 16px;
border-radius: 7px;
border: none;
cursor: pointer;
font-size: 13px;
font-weight: 600;
}
#btnEdit { background: rgba(245,158,11,0.15); color: #fbbf24; border: 1px solid rgba(245,158,11,0.25); }
#btnDelete { background: rgba(239,68,68,0.12); color: #f87171; border: 1px solid rgba(239,68,68,0.2); }
.hidden { display: none !important; }
::-webkit-scrollbar { width: 5px; }
::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 10px; } ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: var(--accent); } ::-webkit-scrollbar-thumb:hover { background: var(--accent); }
+58 -27
View File
@@ -8,58 +8,89 @@
</head> </head>
<body> <body>
<div class="root"> <div class="root">
<header> <header>
<h1>Bungee Status</h1> <h1>Bungee Status</h1>
<div class="actions"> <div class="actions">
<button id="btnRefresh" title="Jetzt aktualisieren">↻</button> <button id="btnRefresh" title="Jetzt aktualisieren">↻</button>
<button id="btnToggleSettings">⚙</button> <button id="btnToggleSettings" title="Einstellungen">⚙</button>
</div> </div>
</header> </header>
<section class="main"> <div class="main">
<aside class="server-list">
<div class="add-form" id="settingsForm"> <div id="settingsForm" class="settings-panel hidden">
<h3 id="settingsTitle">Neuen Server hinzufügen</h3>
<input id="inputName" placeholder="Name (z. B. Lobby)" /> <input id="inputName" placeholder="Name (z. B. Lobby)" />
<input id="inputUrl" placeholder="URL (127.0.0.1:9191)" /> <input id="inputUrl" placeholder="URL (z. B. 192.168.1.1:9191)" />
<input id="inputWpSite" placeholder="WP Site" /> <input id="inputWpSite" placeholder="WP Site (z. B. https://meine-seite.de)" />
<input id="inputWpServerId" placeholder="WP Server ID" /> <input id="inputWpServerId" placeholder="WP Server ID" />
<button id="btnAddServer">Hinzufügen</button> <button id="btnAddServer">Hinzufügen</button>
</div> </div>
<div id="serverListPanel" class="server-list-panel hidden">
<ul id="serversContainer"></ul> <ul id="serversContainer"></ul>
</aside>
<div class="detail">
<div id="noSelection" class="placeholder">Wähle einen Server aus der linken Liste.</div>
<div id="detailContent" class="detailContent hidden">
<div class="detail-header">
<div class="server-identity">
<h2 id="detailName"></h2>
<span id="detailUrlText" class="server-url"></span>
</div> </div>
<div class="status-wrapper">
<div id="detailPulse" class="pulsing-dot"></div> <div class="detail-panel">
<span id="detailStatus"></span>
<div id="noSelection" class="placeholder">Noch kein Server ausgewählt.</div>
<div id="detailContent" class="hidden">
<!-- Server Name + Status Badge -->
<div class="srv-identity">
<div>
<div class="srv-name" id="detailName"></div>
<div class="srv-url" id="detailUrlText"></div>
</div>
<div class="online-badge offline" id="statusBadge">
<div class="pulsing-dot offline" id="detailPulse"></div>
<span id="detailStatus">Offline</span>
</div> </div>
</div> </div>
<div class="detail-stats"> <!-- MOTD -->
<div><strong>Spieler:</strong> <span id="detailPlayers"></span></div> <div id="motdBar" class="motd-bar hidden"></div>
<div><strong>Version:</strong> <span id="detailVersion"></span></div>
<div><strong>Ping:</strong> <span id="detailPing">-</span></div> <!-- Stats: Spieler / Version / Ping -->
<div class="stats-row">
<div class="stat-card">
<div class="stat-label">Spieler</div>
<div class="stat-val" id="detailPlayers">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Version</div>
<div class="stat-val small" id="detailVersion">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Ping</div>
<div class="stat-val" id="detailPing">-</div>
</div>
</div> </div>
<h3>Online Spieler</h3> <!-- Sub-Server -->
<ul id="detailPlayerList" class="playerList"></ul> <div id="backendSection" class="hidden">
<div class="section-label">Sub-Server</div>
<div class="sub-servers" id="backendServersList"></div>
</div>
<div class="detailButtons"> <!-- Online Spieler -->
<div>
<div class="section-label">Online Spieler</div>
<div class="players-grid" id="detailPlayerList"></div>
</div>
<!-- Edit / Delete -->
<div class="detail-buttons hidden" id="detailButtons">
<button id="btnEdit">Bearbeiten</button> <button id="btnEdit">Bearbeiten</button>
<button id="btnDelete">Löschen</button> <button id="btnDelete">Löschen</button>
</div> </div>
</div> </div>
</div> </div>
</section>
</div>
</div> </div>
<script src="popup.js"></script> <script src="popup.js"></script>
</body> </body>
+262 -167
View File
@@ -1,103 +1,85 @@
//popup.js // popup.js
const $ = id => document.getElementById(id); const $ = id => document.getElementById(id);
const uid = () => 'srv_' + Math.random().toString(36).slice(2,9); const uid = () => 'srv_' + Math.random().toString(36).slice(2,9);
const inputName = $('inputName');
const inputUrl = $('inputUrl');
const inputWpSite = $('inputWpSite');
const inputWpServerId = $('inputWpServerId');
const btnAdd = $('btnAddServer');
const serversContainer = $('serversContainer');
const serverListPanel = document.querySelector('.server-list');
const btnRefresh = $('btnRefresh');
const btnToggleSettings = $('btnToggleSettings');
const settingsForm = $('settingsForm');
const noSelection = $('noSelection');
const detailContent = $('detailContent');
const detailName = $('detailName');
const detailUrlText = $('detailUrlText');
const detailStatus = $('detailStatus');
const detailPulse = $('detailPulse');
const detailVersion = $('detailVersion');
const detailPlayers = $('detailPlayers');
const detailPing = $('detailPing');
const detailPlayerList = $('detailPlayerList');
const btnEdit = $('btnEdit');
const btnDelete = $('btnDelete');
let servers = []; let servers = [];
let selectedId = null; let selectedId = null;
let statuses = {}; let statuses = {};
let previousStatuses = {}; let previousStatuses = {};
let settingsVisible = false; let settingsVisible = false;
let editingId = null; // Welcher Server gerade bearbeitet wird
document.addEventListener('DOMContentLoaded', init); document.addEventListener('DOMContentLoaded', init);
btnAdd.addEventListener('click', handleAdd); $('btnAdd')?.addEventListener('click', handleAdd);
btnRefresh.addEventListener('click', manualRefresh); $('btnAddServer')?.addEventListener('click', handleAdd);
btnToggleSettings.addEventListener('click', toggleSettings); $('btnRefresh').addEventListener('click', manualRefresh);
btnEdit.addEventListener('click', handleEdit); $('btnToggleSettings').addEventListener('click', toggleSettings);
btnDelete.addEventListener('click', handleDelete); $('btnEdit').addEventListener('click', handleEdit);
$('btnDelete').addEventListener('click', handleDelete);
async function init() { async function init() {
await loadServersFromStorage(); await loadServersFromStorage();
await loadStatusesFromStorage(); await loadStatusesFromStorage();
await loadSettingsVisibility(); await loadSettingsVisibility();
renderServerList(); renderServerList();
if (servers.length === 1) selectedId = servers[0].id; if (servers.length === 1) selectedId = servers[0].id;
renderDetail(selectedId); renderDetail(selectedId);
adjustDetailLayout();
} }
// --- NEU: Minecraft Color Parser (Eingebaut ohne Löschung) --- // ── Minecraft color code parser ──
function parseMinecraftColors(text) { function parseMinecraftColors(text) {
if (!text) return ''; if (!text) return '';
const map = { const map = {
'&0': '#000000', '&1': '#0000AA', '&2': '#00AA00', '&3': '#00AAAA', '§0':'#000000','§1':'#0000AA','§2':'#00AA00','§3':'#00AAAA',
'&4': '#AA0000', '&5': '#AA00AA', '&6': '#FFAA00', '&7': '#AAAAAA', '§4':'#AA0000','§5':'#AA00AA','§6':'#FFAA00','§7':'#AAAAAA',
'&8': '#555555', '&9': '#5555FF', '&a': '#55FF55', '&b': '#55FFFF', '§8':'#555555','§9':'#5555FF','§a':'#55FF55','§b':'#55FFFF',
'&c': '#FF5555', '&d': '#FF55FF', '&e': '#FFFF55', '&f': '#FFFFFF' '§c':'#FF5555','§d':'#FF55FF','§e':'#FFFF55','§f':'#FFFFFF',
'&0':'#000000','&1':'#0000AA','&2':'#00AA00','&3':'#00AAAA',
'&4':'#AA0000','&5':'#AA00AA','&6':'#FFAA00','&7':'#AAAAAA',
'&8':'#555555','&9':'#5555FF','&a':'#55FF55','&b':'#55FFFF',
'&c':'#FF5555','&d':'#FF55FF','&e':'#FFFF55','&f':'#FFFFFF'
}; };
let html = text.replace(/[<>]/g, ''); let html = text.replace(/[<>]/g, '');
Object.keys(map).forEach(code => { let result = '';
const color = map[code]; let i = 0;
const regex = new RegExp(code, 'g'); while (i < html.length) {
html = html.replace(regex, `</span><span style="color: ${color}">`); const ch = html[i];
}); if ((ch === '§' || ch === '&') && i + 1 < html.length) {
return `<span>${html}</span>`.replace(/&l/g, '<b>').replace(/&r/g, '</b>'); const code = ch + html[i+1].toLowerCase();
if (map[code]) {
result += `<span style="color:${map[code]}">`;
i += 2;
continue;
}
}
result += html[i];
i++;
}
return result;
} }
// --- 3D Avatar URL Generator --- function get3DAvatarUrl(name, uuid = null) {
function get3DAvatarUrl(playerName, uuid = null) { return `https://mc-heads.net/head/${encodeURIComponent(name)}/64`;
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`;
}
} }
// ── Storage ──
async function loadSettingsVisibility() { async function loadSettingsVisibility() {
const obj = await chrome.storage.local.get(['settingsVisible']); const obj = await chrome.storage.local.get(['settingsVisible']);
settingsVisible = obj.settingsVisible !== undefined ? obj.settingsVisible : false; settingsVisible = !!obj.settingsVisible;
if (!settingsVisible) settingsForm.classList.add('hidden'); applySettingsVisibility();
else settingsForm.classList.remove('hidden');
} }
async function saveSettingsVisibility() { async function saveSettingsVisibility() {
await chrome.storage.local.set({ settingsVisible }); await chrome.storage.local.set({ settingsVisible });
} }
function applySettingsVisibility() {
$('settingsForm').classList.toggle('hidden', !settingsVisible);
$('serverListPanel').classList.toggle('hidden', !settingsVisible);
$('detailButtons').classList.toggle('hidden', !settingsVisible);
}
async function toggleSettings() { async function toggleSettings() {
settingsVisible = !settingsVisible; settingsVisible = !settingsVisible;
settingsForm.classList.toggle('hidden'); applySettingsVisibility();
await saveSettingsVisibility(); await saveSettingsVisibility();
adjustDetailLayout();
} }
async function loadServersFromStorage() { async function loadServersFromStorage() {
@@ -106,143 +88,189 @@ async function loadServersFromStorage() {
for (const s of servers) if (!s.id) s.id = uid(); for (const s of servers) if (!s.id) s.id = uid();
await chrome.storage.local.set({ servers }); await chrome.storage.local.set({ servers });
} }
async function saveServersToStorage() { async function saveServersToStorage() {
await chrome.storage.local.set({ servers }); await chrome.storage.local.set({ servers });
} }
async function loadStatusesFromStorage() { async function loadStatusesFromStorage() {
const obj = await chrome.storage.local.get(['serverStatuses']); const obj = await chrome.storage.local.get(['serverStatuses']);
statuses = obj.serverStatuses || {}; statuses = obj.serverStatuses || {};
} }
function adjustDetailLayout() { // ── Server list ──
const settingsHidden = settingsForm.classList.contains('hidden');
detailContent.classList.toggle('full-width', settingsHidden);
serverListPanel.classList.toggle('hidden', settingsHidden);
btnEdit.style.display = settingsHidden ? 'none' : 'inline-block';
btnDelete.style.display = settingsHidden ? 'none' : 'inline-block';
}
function renderServerList() { function renderServerList() {
serversContainer.innerHTML = ''; const container = $('serversContainer');
container.innerHTML = '';
if (servers.length === 0) { if (servers.length === 0) {
serversContainer.innerHTML = '<div class="placeholder">Noch keine Server hinzugefügt.</div>'; container.innerHTML = '<div class="placeholder" style="padding:8px 0;">Noch kein Server hinzugefügt.</div>';
return; return;
} }
for (const s of servers) { for (const s of servers) {
const item = document.createElement('li'); const li = document.createElement('li');
item.className = 'server-item'; li.className = 'server-item' + (s.id === selectedId ? ' selected' : '');
item.dataset.id = s.id; li.dataset.id = s.id;
const meta = document.createElement('div'); const meta = document.createElement('div');
meta.className = 'meta'; meta.className = 'meta';
const name = document.createElement('div');
name.className = 'name'; const nameEl = document.createElement('div');
name.textContent = s.name || '(Kein Name)'; nameEl.className = 'name';
let listUrl = s.url || (s.wpSite ? s.wpSite + ' (WP)' : ''); nameEl.textContent = s.name || '(Kein Name)';
listUrl = listUrl.replace(':9191', '');
const url = document.createElement('div'); let urlText = s.url || (s.wpSite || '');
url.className = 'url'; urlText = urlText.replace(':9191', '');
url.textContent = listUrl; const urlEl = document.createElement('div');
meta.append(name, url); urlEl.className = 'url';
const statusBubble = document.createElement('div'); urlEl.textContent = urlText;
statusBubble.className = 'status-bubble';
statusBubble.textContent = '—'; meta.append(nameEl, urlEl);
statusBubble.style.backgroundColor = 'transparent';
item.statusBubble = statusBubble; const bubble = document.createElement('div');
item.append(meta, statusBubble); bubble.className = 'status-bubble';
item.addEventListener('click', () => { bubble.textContent = '—';
li._bubble = bubble;
li.append(meta, bubble);
li.addEventListener('click', () => {
selectedId = s.id; selectedId = s.id;
document.querySelectorAll('.server-item').forEach(el => el.classList.remove('selected'));
li.classList.add('selected');
renderDetail(selectedId); renderDetail(selectedId);
}); });
serversContainer.appendChild(item); container.appendChild(li);
} }
updateServerListStatuses(); updateServerListStatuses();
} }
// --- Fortsetzung popup.js --- // ── Detail ──
function renderDetail(id) { function renderDetail(id) {
if (!id) { if (!id) {
noSelection.classList.remove('hidden'); $('noSelection').classList.remove('hidden');
detailContent.classList.add('hidden'); $('detailContent').classList.add('hidden');
return; return;
} }
const srv = servers.find(x => x.id === id); const srv = servers.find(x => x.id === id);
if (!srv) { if (!srv) {
noSelection.classList.remove('hidden'); $('noSelection').classList.remove('hidden');
detailContent.classList.add('hidden'); $('detailContent').classList.add('hidden');
return; return;
} }
noSelection.classList.add('hidden'); $('noSelection').classList.add('hidden');
detailContent.classList.remove('hidden'); $('detailContent').classList.remove('hidden');
detailName.textContent = srv.name || 'Unbenannter Server';
let urlToShow = srv.url || (srv.wpSite ? srv.wpSite : 'Lokal'); $('detailName').textContent = srv.name || 'Unbenannter Server';
let urlToShow = srv.url || (srv.wpSite || 'Lokal');
urlToShow = urlToShow.replace(':9191', ''); urlToShow = urlToShow.replace(':9191', '');
detailUrlText.textContent = urlToShow; $('detailUrlText').textContent = urlToShow;
updateDetailForServer(srv, true); updateDetailForServer(srv, true);
} }
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 changed = force || JSON.stringify(st) !== JSON.stringify(prevSt);
if (!changed) return;
const badge = $('statusBadge');
const pulse = $('detailPulse');
if (!st || !st.ok || !st.data) { if (!st || !st.ok || !st.data) {
if (statusChanged) { badge.className = 'online-badge offline';
detailStatus.textContent = 'Offline'; pulse.className = 'pulsing-dot offline';
detailPulse.classList.remove('online'); $('detailStatus').textContent = 'Offline';
detailVersion.textContent = '-'; $('detailVersion').textContent = '-';
detailPlayers.textContent = '-'; $('detailPlayers').textContent = '-';
detailPing.textContent = '-'; $('detailPing').textContent = '-';
$('motdBar').classList.add('hidden');
updatePlayerList([]); updatePlayerList([]);
} updateBackendServers(null);
} else { } else {
const d = st.data; const d = st.data;
if (statusChanged) { const isOnline = !!d.online;
detailStatus.textContent = d.online ? 'Online' : 'Offline';
if (d.online) detailPulse.classList.add('online'); badge.className = 'online-badge ' + (isOnline ? 'online' : 'offline');
else detailPulse.classList.remove('online'); pulse.className = 'pulsing-dot ' + (isOnline ? 'online' : 'offline');
$('detailStatus').textContent = isOnline ? 'Online' : 'Offline';
$('detailVersion').textContent = d.version || 'unknown';
const count = Array.isArray(d.players) ? d.players.length
: (typeof d.players === 'number' ? d.players : 0);
const max = d.max_players;
$('detailPlayers').textContent = max && max !== '-1' ? `${count} / ${max}` : String(count);
const ping = d.ping || d.latency;
$('detailPing').textContent = typeof ping === 'number' ? ping + ' ms' : '-';
const motdRaw = d.motd || (d.network && d.network.motd) || '';
if (motdRaw) {
const motdEl = $('motdBar');
motdEl.innerHTML = parseMinecraftColors(motdRaw);
motdEl.classList.remove('hidden');
} else {
$('motdBar').classList.add('hidden');
} }
const newVersion = d.version || 'unknown';
if (force || detailVersion.textContent !== newVersion) detailVersion.textContent = newVersion;
const playersCount = Array.isArray(d.players) ? d.players.length : (typeof d.players === 'number' ? d.players : 0);
const maxPlayers = d.max_players;
let newPlayersText = String(playersCount);
if (maxPlayers && maxPlayers !== '-1') newPlayersText += ` / ${maxPlayers}`;
if (force || detailPlayers.textContent !== newPlayersText) detailPlayers.textContent = newPlayersText;
let pingVal = d.ping || d.latency || '-';
if (pingVal !== '-' && typeof pingVal === 'number') pingVal = pingVal + ' ms';
if (force || (pingVal !== '-' && detailPing.textContent !== pingVal)) detailPing.textContent = pingVal;
const currentPlayers = Array.isArray(d.players) ? d.players : []; const currentPlayers = Array.isArray(d.players) ? d.players : [];
const prevPlayers = (prevSt && Array.isArray(prevSt.data?.players)) ? prevSt.data.players : []; updatePlayerList(currentPlayers);
let playersChanged = force || JSON.stringify(currentPlayers) !== JSON.stringify(prevPlayers); updateBackendServers(d.network?.backend_servers || null);
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 mit Prefix-Hover und Farbsupport --- // ── Sub-Server chips ──
function updateBackendServers(list) {
const section = $('backendSection');
const container = $('backendServersList');
if (!list || list.length === 0) {
section.classList.add('hidden');
container.innerHTML = '';
return;
}
section.classList.remove('hidden');
container.innerHTML = '';
for (const bs of list) {
const count = bs.online_players || 0;
const chip = document.createElement('div');
chip.className = 'sub-chip' + (count > 0 ? ' active' : '');
const dot = document.createElement('span');
dot.className = 'sub-chip-dot';
const nameEl = document.createElement('span');
nameEl.className = 'sub-chip-name';
nameEl.textContent = bs.name;
const countEl = document.createElement('span');
countEl.className = 'sub-chip-count';
countEl.textContent = count > 0 ? count : 'leer';
chip.append(dot, nameEl, countEl);
container.appendChild(chip);
}
}
// ── Player list ──
function updatePlayerList(players) { function updatePlayerList(players) {
detailPlayerList.innerHTML = ''; const grid = $('detailPlayerList');
grid.innerHTML = '';
if (!players || players.length === 0) { if (!players || players.length === 0) {
detailPlayerList.innerHTML = '<li class="placeholder">Keine Spieler online.</li>'; grid.innerHTML = '<div class="placeholder">Keine Spieler online.</div>';
return; return;
} }
for (const p of players) { for (const p of players) {
const li = document.createElement('li'); const card = document.createElement('div');
li.className = 'player-item'; // CSS Klasse für den Hover-Container card.className = 'player-card';
let name = ''; let name = '', uuid = null, prefix = '', serverName = null;
let uuid = null;
let prefix = '';
if (typeof p === 'object') { if (typeof p === 'object') {
name = p.name || p.username || p.player || ''; name = p.name || p.username || p.player || '';
uuid = p.uuid || null; uuid = p.uuid || null;
prefix = p.prefix || ''; prefix = p.prefix || p.group || '';
serverName = p.server || p.current_server || p.connected_server || null;
} else { } else {
name = String(p); name = String(p);
} }
@@ -256,58 +284,123 @@ function updatePlayerList(players) {
this.src = `https://mc-heads.net/avatar/${encodeURIComponent(name)}/64`; this.src = `https://mc-heads.net/avatar/${encodeURIComponent(name)}/64`;
}; };
// Das Hover-Element mit farbigem Prefix + Name
const info = document.createElement('div'); const info = document.createElement('div');
info.className = 'player-hover-info'; info.className = 'player-info';
info.innerHTML = `${parseMinecraftColors(prefix)} ${name}`.trim();
li.appendChild(img); if (prefix) {
li.appendChild(info); const prefixEl = document.createElement('div');
detailPlayerList.appendChild(li); prefixEl.className = 'player-prefix';
prefixEl.innerHTML = parseMinecraftColors(prefix);
info.appendChild(prefixEl);
}
const nameEl = document.createElement('div');
nameEl.className = 'player-name';
nameEl.textContent = name;
info.appendChild(nameEl);
if (serverName) {
const tag = document.createElement('span');
tag.className = 'player-server-tag';
tag.textContent = serverName;
info.appendChild(tag);
}
card.append(img, info);
grid.appendChild(card);
} }
} }
// ── Server list status bubbles ──
function updateServerListStatuses() { 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 || !item._bubble) return;
const st = statuses[s.id]; const st = statuses[s.id];
if (!st || !st.ok || !st.data) { if (!st || !st.ok || !st.data || !st.data.online) {
item.statusBubble.textContent = 'Offline'; item._bubble.textContent = 'Offline';
item.statusBubble.style.backgroundColor = 'var(--offline)'; item._bubble.style.background = 'rgba(239,68,68,0.15)';
} else if (st.data.online) { item._bubble.style.color = '#f87171';
item.statusBubble.textContent = 'Online';
item.statusBubble.style.backgroundColor = 'var(--online)';
} else { } else {
item.statusBubble.textContent = 'Offline'; item._bubble.textContent = 'Online';
item.statusBubble.style.backgroundColor = 'var(--offline)'; item._bubble.style.background = 'rgba(34,197,94,0.15)';
item._bubble.style.color = '#4ade80';
} }
}); });
} }
// ── Edit / Add Mode ──
function enterEditMode(server) {
editingId = server.id;
$('settingsTitle').textContent = 'Server bearbeiten';
$('btnAddServer').textContent = 'Änderungen speichern';
$('inputName').value = server.name || '';
$('inputUrl').value = server.url || '';
$('inputWpSite').value = server.wpSite || '';
$('inputWpServerId').value = server.wpServerId || '';
}
function exitEditMode() {
editingId = null;
$('settingsTitle').textContent = 'Neuen Server hinzufügen';
$('btnAddServer').textContent = 'Hinzufügen';
$('inputName').value = '';
$('inputUrl').value = '';
$('inputWpSite').value = '';
$('inputWpServerId').value = '';
}
// ── Actions ──
async function handleAdd() { 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;
const s = { id: uid(), name: name || url || wpSite, url: url || null, wpSite: wpSite || null, wpServerId: wpServerId || null }; if (!url && !wpSite) {
alert('Bitte eine URL oder WordPress-Einstellungen angeben.');
return;
}
if (editingId) {
// === BEARBEITEN ===
const srv = servers.find(s => s.id === editingId);
if (srv) {
srv.name = name || url || wpSite;
if (url) srv.url = url;
if (wpSite) srv.wpSite = wpSite;
if (wpServerId) srv.wpServerId = wpServerId;
}
exitEditMode();
} else {
// === NEU HINZUFÜGEN ===
const s = {
id: uid(),
name: name || url || wpSite,
url: url || null,
wpSite: wpSite || null,
wpServerId: wpServerId || null
};
servers.push(s); servers.push(s);
}
await saveServersToStorage(); await saveServersToStorage();
inputName.value = inputUrl.value = inputWpSite.value = inputWpServerId.value = '';
renderServerList(); renderServerList();
if (selectedId) renderDetail(selectedId);
} }
async function handleEdit() { async function handleEdit() {
if (!selectedId) return; if (!selectedId) return;
const srv = servers.find(s => s.id === selectedId); const srv = servers.find(s => s.id === selectedId);
if (!srv) return; if (!srv) return;
const newName = prompt('Name:', srv.name) || srv.name;
srv.name = newName.trim(); settingsVisible = true;
await saveServersToStorage(); applySettingsVisibility();
renderServerList(); await saveSettingsVisibility();
renderDetail(selectedId);
enterEditMode(srv);
} }
async function handleDelete() { async function handleDelete() {
@@ -317,12 +410,14 @@ async function handleDelete() {
await saveServersToStorage(); await saveServersToStorage();
renderServerList(); renderServerList();
renderDetail(null); renderDetail(null);
exitEditMode();
} }
async function manualRefresh() { async function manualRefresh() {
try { chrome.runtime.sendMessage({ cmd: 'refreshNow' }); } catch(e) {} try { chrome.runtime.sendMessage({ cmd: 'refreshNow' }); } catch(e) {}
} }
// ── Live updates from background ──
chrome.storage.onChanged.addListener((changes, area) => { chrome.storage.onChanged.addListener((changes, area) => {
if (area === 'local' && changes.serverStatuses) { if (area === 'local' && changes.serverStatuses) {
statuses = changes.serverStatuses.newValue || {}; statuses = changes.serverStatuses.newValue || {};