diff --git a/BungeeCord-Chrome/background.js b/BungeeCord-Chrome/background.js new file mode 100644 index 0000000..f4dde14 --- /dev/null +++ b/BungeeCord-Chrome/background.js @@ -0,0 +1,111 @@ +// background.js +const POLL_INTERVAL_SEC = 2; + +// Alarm erstellen +chrome.runtime.onInstalled.addListener(() => { + chrome.alarms.create('periodicRefresh', { periodInMinutes: POLL_INTERVAL_SEC / 60 }); +}); + +chrome.alarms.onAlarm.addListener((alarm) => { + if (alarm.name === 'periodicRefresh') { + refreshAllServers().catch(console.error); + } +}); + +// Message Listener +chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { + if (msg && msg.cmd === 'refreshNow') { + refreshAllServers() + .then(statuses => sendResponse({ ok: true, statuses })) + .catch(err => sendResponse({ ok: false, error: String(err) })); + return true; + } +}); + +async function refreshAllServers() { + const obj = await chrome.storage.local.get('servers'); + const servers = obj.servers || []; + if (servers.length === 0) { + await chrome.storage.local.set({ serverStatuses: {} }); + chrome.action.setBadgeText({ text: '' }); + return {}; + } + + const results = {}; + let totalOnlinePlayers = 0; + + const tasks = servers.map(async (srv) => { + try { + const data = await fetchServerStatus(srv, 2000); + results[srv.id] = { ok: true, server: srv, fetched: Date.now(), data }; + + if (data) { + if (Array.isArray(data.players)) totalOnlinePlayers += data.players.length; + else if (typeof data.players === 'number') totalOnlinePlayers += data.players; + } + } catch (e) { + results[srv.id] = { ok: false, server: srv, fetched: Date.now(), error: String(e) }; + } + }); + + await Promise.all(tasks); + await chrome.storage.local.set({ serverStatuses: results }); + + const badgeText = totalOnlinePlayers > 0 ? String(totalOnlinePlayers) : ''; + chrome.action.setBadgeText({ text: badgeText }); + chrome.action.setBadgeBackgroundColor({ color: '#3b82f6' }); + + return results; +} + +async function fetchServerStatus(server, timeoutMs = 2000) { + let serverObj = typeof server === 'string' ? { id: 'legacy', url: server } : server; + let fetchUrl = serverObj.url || ''; + + if (fetchUrl) { + if (!/^https?:\/\//i.test(fetchUrl)) fetchUrl = 'http://' + fetchUrl; + try { + const controller = new AbortController(); + const id = setTimeout(() => controller.abort(), timeoutMs); + const resp = await fetch(fetchUrl, { method: 'GET', signal: controller.signal }); + clearTimeout(id); + + if (resp.ok) { + let text = await resp.text(); + const lastBrace = text.lastIndexOf('}'); + if (lastBrace !== -1) text = text.substring(0, lastBrace + 1); + text = text.trim(); + try { + const parsed = JSON.parse(text); + if (parsed && (typeof parsed.online !== 'undefined' || Array.isArray(parsed.players) || parsed.version)) return parsed; + } catch {} + } + } catch {} + } + + const wpSite = serverObj.wpSite ? String(serverObj.wpSite).replace(/\/$/, '') : null; + const wpServerId = serverObj.wpServerId ? String(serverObj.wpServerId) : null; + if (wpSite && wpServerId) { + try { + const ajaxUrl = wpSite + '/wp-admin/admin-ajax.php'; + const body = 'action=mcss_fetch&server_id=' + encodeURIComponent(wpServerId); + + const controller2 = new AbortController(); + const id2 = setTimeout(() => controller2.abort(), timeoutMs); + + const resp2 = await fetch(ajaxUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body, + signal: controller2.signal + }); + clearTimeout(id2); + + if (!resp2.ok) throw new Error('WP AJAX HTTP ' + resp2.status); + const json = await resp2.json(); + if (json && (typeof json.online !== 'undefined' || Array.isArray(json.players) || json.version)) return json; + } catch {} + } + + return { online: false, players: [], max_players: 0, version: 'unknown', motd: '' }; +} \ No newline at end of file diff --git a/BungeeCord-Chrome/manifest.json b/BungeeCord-Chrome/manifest.json new file mode 100644 index 0000000..1e2972d --- /dev/null +++ b/BungeeCord-Chrome/manifest.json @@ -0,0 +1,25 @@ +{ + "manifest_version": 3, + "name": "Minecraft Bungee Status", + "description": "Zeigt den Live-Status mehrerer Bungee/Proxy-Server an. Fallback auf WordPress AJAX möglich.", + "version": "1.1.0", + "action": { + "default_popup": "popup.html", + "default_icon": { + "16": "icons/icon16.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + } + }, + "background": { + "service_worker": "background.js" + }, + "permissions": [ + "storage", + "alarms" + ], + "host_permissions": [ + "http://*/*", + "https://*/*" + ] +} diff --git a/BungeeCord-Chrome/popup.css b/BungeeCord-Chrome/popup.css new file mode 100644 index 0000000..b2dffac --- /dev/null +++ b/BungeeCord-Chrome/popup.css @@ -0,0 +1,220 @@ +:root { + --bg: #111827; + --panel: #0b1220; + --text: #e6eef8; + --muted: #9aa7b3; + --accent: #3b82f6; + --danger: #ef4444; + --radius: 8px; + --online: #22c55e; + --offline: #ef4444; +} + +* { box-sizing:border-box; } +html,body { margin:0; padding:0; font-family:Inter, Arial, sans-serif; background:var(--bg); color:var(--text);} +.root { width:520px; max-width:520px; padding:12px; } + +header { display:flex; align-items:center; justify-content:space-between; margin-bottom:8px; } +header h1 { font-size:16px; margin:0; } +header .actions button { background:transparent; border:1px solid var(--muted); color:var(--text); padding:4px 8px; border-radius:6px; cursor:pointer; } + +.main { + display:flex; + gap:12px; + position: relative; +} + +.server-list { + width:45%; + background:var(--panel); + padding:8px; + border-radius:var(--radius); + flex-shrink: 0; +} + +.detail { + flex:1; + background:var(--panel); + padding:8px; + border-radius:var(--radius); + min-height:220px; + transition: all 0.3s ease; +} + +.detail.full-width { + width: 100%; + flex: 1 1 100%; +} + +.add-form { display:flex; flex-direction:column; gap:6px; margin-bottom:8px; } +.add-form input { padding:6px 8px; border-radius:6px; border:1px solid #21303b; background:#071019; color:var(--text); } +.add-form button { padding:6px 8px; border-radius:6px; border:none; background:var(--accent); color:white; cursor:pointer; } + +#serversContainer { list-style:none; margin:0; padding:0; max-height:260px; overflow:auto; } +.server-item { + display:flex; + align-items:center; + justify-content:space-between; + padding:6px; + margin-bottom:6px; + border-radius:6px; + cursor:pointer; + background: rgba(255,255,255,0.02); + transition: background 0.2s ease; +} + +.server-item:hover { background: rgba(255,255,255,0.05); } + +.server-item .meta { display:flex; flex-direction:column; } +.server-item .meta .name { font-weight:600; } +.server-item .meta .url { font-size:12px; color:var(--muted); } +.server-item .status-bubble { + font-size:12px; + padding:3px 6px; + border-radius:999px; + color:#fff; + transition: background-color 0.3s ease; +} + +.placeholder { color:var(--muted); padding:12px; } +.hidden { display:none; } + +/* --- Detail Header (Zeile 1) --- */ +.detail-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.server-identity h2 { + margin: 0; + font-size: 18px; + line-height: 1.2; +} + +.server-url { + display: block; + font-size: 12px; + color: var(--muted); + margin-top: 2px; +} + +.status-wrapper { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; +} + +/* --- Pulsierender Punkt Animation --- */ +.pulsing-dot { + width: 10px; + height: 10px; + border-radius: 50%; + background-color: var(--offline); /* Standard Offline Rot */ + box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); + animation: pulse-red 2s infinite; +} + +.pulsing-dot.online { + background-color: var(--online); /* Online Grün */ + animation: pulse-green 2s infinite; +} + +@keyframes pulse-green { + 0% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.7); } + 70% { box-shadow: 0 0 0 10px rgba(34, 197, 94, 0); } + 100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); } +} + +@keyframes pulse-red { + 0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); } + 70% { box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); } + 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); } +} + +/* --- Detail Stats (Zeile 2) --- */ +.detail-stats { + display: flex; + justify-content: space-between; + align-items: center; + background: rgba(255,255,255,0.05); + padding: 8px; + border-radius: var(--radius); + margin-bottom: 16px; + font-size: 13px; +} + +/* ---------------------------------- */ + +.playerList { + list-style:none; + padding-left:16px; + max-height:140px; + overflow:auto; + margin:6px 0; + display:flex; + flex-wrap:wrap; + gap:8px; +} + +.playerList li { + display:flex; + align-items:center; + gap:4px; + font-size:13px; + color:var(--text); +} + +.player-avatar { + width:24px; + height:24px; + border-radius:4px; + transition: transform 0.2s ease; +} + +.player-avatar:hover { transform: scale(1.1); } + +.detailButtons { + display:flex; + gap:8px; + margin-top:8px; + transition: opacity 0.3s ease; +} + +.detailButtons button { + padding:6px 8px; + border-radius:6px; + border:none; + cursor:pointer; + transition: opacity 0.2s ease, transform 0.1s ease; +} + +.detailButtons button:hover { opacity: 0.9; transform: translateY(-1px); } +.detailButtons button:active { transform: translateY(0); } + +#btnEdit { background: #f59e0b; color:#000; } +#btnDelete { background: var(--danger); color:#fff; } + +#serversContainer::-webkit-scrollbar, +.playerList::-webkit-scrollbar { + width: 6px; +} + +#serversContainer::-webkit-scrollbar-track, +.playerList::-webkit-scrollbar-track { + background: rgba(255,255,255,0.05); + border-radius: 3px; +} + +#serversContainer::-webkit-scrollbar-thumb, +.playerList::-webkit-scrollbar-thumb { + background: var(--muted); + border-radius: 3px; +} + +#serversContainer::-webkit-scrollbar-thumb:hover, +.playerList::-webkit-scrollbar-thumb:hover { + background: var(--accent); +} \ No newline at end of file diff --git a/BungeeCord-Chrome/popup.html b/BungeeCord-Chrome/popup.html new file mode 100644 index 0000000..4cb393a --- /dev/null +++ b/BungeeCord-Chrome/popup.html @@ -0,0 +1,68 @@ + + + + + +Bungee Status + + + +
+
+

Bungee Status

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