Upload via GUI (324 Dateien)
@@ -0,0 +1,189 @@
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* streamdeck.js – lokale Schnittstelle für das AeroMC-Plugin des Elgato Stream Deck
|
||||
* -----------------------------------------------------------------------------
|
||||
* Ein kleiner HTTP-Server, der AUSSCHLIESSLICH auf 127.0.0.1 lauscht und nur auf
|
||||
* Wunsch läuft (Einstellung "streamdeckEnabled", standardmäßig aus).
|
||||
*
|
||||
* Absicherung gegen fremde Zugriffe:
|
||||
* - nur Loopback, kein Zugriff aus dem Netzwerk
|
||||
* - jede Anfrage braucht den Zufalls-Schlüssel im Kopf "Authorization: Bearer …";
|
||||
* der wird bei jedem Start neu erzeugt und nur in eine Datei im
|
||||
* Benutzerordner geschrieben, die das Plugin liest (streamdeck.json)
|
||||
* - Anfragen mit "Origin"-Kopf (also aus einer Webseite im Browser) und mit
|
||||
* fremdem "Host"-Kopf (DNS-Rebinding) werden abgewiesen
|
||||
*
|
||||
* Die eigentlichen Aktionen kommen von main.js (siehe init) – dieses Modul kennt
|
||||
* weder Instanzen noch Aufnahmen und reicht nur weiter.
|
||||
*/
|
||||
|
||||
const http = require('http');
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const STANDARD_PORT = 48615;
|
||||
const PORT_VERSUCHE = 10;
|
||||
const MAX_BODY = 4096;
|
||||
|
||||
let deps = null;
|
||||
let server = null;
|
||||
let schluessel = null;
|
||||
let verbindungsDatei = null;
|
||||
|
||||
/*
|
||||
* deps: {
|
||||
* userDataDir,
|
||||
* zustand() -> { instanzen: [{ id, name, laeuft, seit }], aufnahme, clip }
|
||||
* starte(id, server) -> { ok, message? } (server optional: direkt verbinden)
|
||||
* neustart(id) -> { ok, message? }
|
||||
* fenster() -> { ok } Launcher-Fenster zeigen bzw. wegminimieren
|
||||
* clipModus() -> { ok, message? }
|
||||
* ordner(id) -> { ok } Aufnahmen-Ordner öffnen
|
||||
* serverListe(id) -> { ok, server: [{ name, ip }] }
|
||||
* ping(ip) -> { ok, online, max, latencyMs }
|
||||
* screenshot() -> { ok, message? } Bild des Spielfensters in den Screenshot-Ordner
|
||||
* symbol(id) -> { ok, bild, pixel } Instanz-Bild als data:-URI
|
||||
* kopf(uuid) -> { ok, bild } Spielerkopf eines Kontos als data:-URI
|
||||
* absturz(id) -> { ok, bericht, zeit, frisch } neuester Absturzbericht der Instanz
|
||||
* konto(id) -> { ok } aktives Konto wechseln
|
||||
* backup() -> { ok, message? } Sicherung aller Instanzen starten
|
||||
* oeffne(art, id) -> { ok, message? } Fenster öffnen: friends | logs | console | crash
|
||||
* beende(id) -> { ok, message? }
|
||||
* clipSpeichern() -> { ok, message? }
|
||||
* aufnahmeUmschalten() -> { ok, message? }
|
||||
* }
|
||||
*/
|
||||
function init(d) {
|
||||
deps = d;
|
||||
verbindungsDatei = path.join(d.userDataDir, 'streamdeck.json');
|
||||
}
|
||||
|
||||
function istSchluesselGleich(angabe) {
|
||||
if (!schluessel || typeof angabe !== 'string') return false;
|
||||
const a = Buffer.from(angabe);
|
||||
const b = Buffer.from(schluessel);
|
||||
return a.length === b.length && crypto.timingSafeEqual(a, b);
|
||||
}
|
||||
|
||||
function antworte(res, status, daten) {
|
||||
const text = JSON.stringify(daten);
|
||||
res.writeHead(status, {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'Content-Length': Buffer.byteLength(text),
|
||||
'Cache-Control': 'no-store',
|
||||
});
|
||||
res.end(text);
|
||||
}
|
||||
|
||||
function leseBody(req) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let groesse = 0;
|
||||
const teile = [];
|
||||
req.on('data', (c) => {
|
||||
groesse += c.length;
|
||||
if (groesse > MAX_BODY) { reject(new Error('zu groß')); req.destroy(); return; }
|
||||
teile.push(c);
|
||||
});
|
||||
req.on('end', () => {
|
||||
if (!teile.length) { resolve({}); return; }
|
||||
try { resolve(JSON.parse(Buffer.concat(teile).toString('utf8')) || {}); } catch { reject(new Error('kein JSON')); }
|
||||
});
|
||||
req.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function behandle(req, res, port) {
|
||||
// Anfragen aus Webseiten (Origin) und DNS-Rebinding (fremder Host) gar nicht erst bedienen
|
||||
const host = String(req.headers.host || '').toLowerCase();
|
||||
if (req.headers.origin || (host !== `127.0.0.1:${port}` && host !== `localhost:${port}`)) {
|
||||
return antworte(res, 403, { ok: false, message: 'Nicht erlaubt.' });
|
||||
}
|
||||
const auth = String(req.headers.authorization || '');
|
||||
if (!auth.startsWith('Bearer ') || !istSchluesselGleich(auth.slice(7))) {
|
||||
return antworte(res, 401, { ok: false, message: 'Schlüssel fehlt oder ist falsch.' });
|
||||
}
|
||||
|
||||
const url = new URL(req.url || '/', 'http://127.0.0.1');
|
||||
const pfad = url.pathname;
|
||||
if (req.method === 'GET' && pfad === '/state') {
|
||||
return antworte(res, 200, Object.assign({ ok: true }, deps.zustand({ freunde: url.searchParams.get('freunde') === '1' })));
|
||||
}
|
||||
if (req.method === 'GET' && pfad === '/servers') {
|
||||
return antworte(res, 200, await deps.serverListe(url.searchParams.get('id') || ''));
|
||||
}
|
||||
if (req.method === 'GET' && pfad === '/icon') {
|
||||
const konto = url.searchParams.get('konto');
|
||||
return antworte(res, 200, konto ? await deps.kopf(konto) : await deps.symbol(url.searchParams.get('id') || ''));
|
||||
}
|
||||
if (req.method === 'GET' && pfad === '/crash') {
|
||||
return antworte(res, 200, await deps.absturz(url.searchParams.get('id') || ''));
|
||||
}
|
||||
if (req.method === 'GET' && pfad === '/ping') {
|
||||
return antworte(res, 200, await deps.ping(url.searchParams.get('ip') || ''));
|
||||
}
|
||||
if (req.method !== 'POST') return antworte(res, 405, { ok: false, message: 'Nur GET/POST.' });
|
||||
|
||||
let body;
|
||||
try { body = await leseBody(req); } catch (err) { return antworte(res, 400, { ok: false, message: err.message }); }
|
||||
const id = typeof body.id === 'string' ? body.id : '';
|
||||
|
||||
let erg;
|
||||
if (pfad === '/launch' && id) erg = await deps.starte(id, typeof body.server === 'string' ? body.server.trim().slice(0, 253) : '');
|
||||
else if (pfad === '/restart' && id) erg = await deps.neustart(id);
|
||||
else if (pfad === '/window') erg = await deps.fenster();
|
||||
else if (pfad === '/clipmode') erg = await deps.clipModus();
|
||||
else if (pfad === '/folder') erg = await deps.ordner(id);
|
||||
else if (pfad === '/screenshot') erg = await deps.screenshot();
|
||||
else if (pfad === '/account' && id) erg = await deps.konto(id);
|
||||
else if (pfad === '/backup') erg = await deps.backup();
|
||||
else if (pfad === '/open' && typeof body.art === 'string') erg = await deps.oeffne(body.art, id);
|
||||
else if (pfad === '/stop' && id) erg = await deps.beende(id);
|
||||
else if (pfad === '/clip') erg = await deps.clipSpeichern();
|
||||
else if (pfad === '/record') erg = await deps.aufnahmeUmschalten();
|
||||
else return antworte(res, 404, { ok: false, message: 'Unbekannte Aktion.' });
|
||||
return antworte(res, erg && erg.ok ? 200 : 409, erg || { ok: false });
|
||||
}
|
||||
|
||||
function schreibeVerbindung(port) {
|
||||
try {
|
||||
fs.writeFileSync(verbindungsDatei, JSON.stringify({ port, token: schluessel }), 'utf8');
|
||||
} catch { /* ohne Datei findet das Plugin den Launcher nicht – mehr geht nicht */ }
|
||||
}
|
||||
|
||||
function loescheVerbindung() {
|
||||
try { fs.rmSync(verbindungsDatei, { force: true }); } catch { /* egal */ }
|
||||
}
|
||||
|
||||
function lausche(port, versuche) {
|
||||
const s = http.createServer((req, res) => {
|
||||
behandle(req, res, port).catch(() => { try { antworte(res, 500, { ok: false }); } catch { /* egal */ } });
|
||||
});
|
||||
s.on('error', (err) => {
|
||||
if (err.code === 'EADDRINUSE' && versuche > 1 && server === s) { lausche(port + 1, versuche - 1); return; }
|
||||
if (server === s) server = null;
|
||||
});
|
||||
server = s;
|
||||
s.listen(port, '127.0.0.1', () => {
|
||||
if (server === s) schreibeVerbindung(port);
|
||||
});
|
||||
}
|
||||
|
||||
function start() {
|
||||
if (server || !deps) return;
|
||||
schluessel = crypto.randomBytes(32).toString('hex');
|
||||
lausche(STANDARD_PORT, PORT_VERSUCHE);
|
||||
}
|
||||
|
||||
function stop() {
|
||||
if (server) { try { server.close(); } catch { /* egal */ } server = null; }
|
||||
schluessel = null;
|
||||
if (verbindungsDatei) loescheVerbindung();
|
||||
}
|
||||
|
||||
function configure(settings) {
|
||||
if (settings && settings.streamdeckEnabled === true) start(); else stop();
|
||||
}
|
||||
|
||||
module.exports = { init, configure, shutdown: stop };
|
||||
@@ -0,0 +1,82 @@
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* streamdeckplugin.js – das AeroMC-Plugin in die Stream-Deck-Software kopieren
|
||||
* -----------------------------------------------------------------------------
|
||||
* Das Plugin (Ordner streamdeck-plugin/com.aeromc.launcher.sdPlugin) liegt im
|
||||
* Launcher mit dabei. Schaltet jemand "Stream Deck erlauben" ein – und bei jedem
|
||||
* Start, solange es an ist – legt AeroMC es in den Plugin-Ordner der Stream-Deck-
|
||||
* Software, falls es dort fehlt oder von einer anderen Fassung stammt. Nach dem
|
||||
* Kopieren muss die Stream-Deck-Software einmal neu gestartet werden, damit sie
|
||||
* das Plugin lädt.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const PLUGIN_NAME = 'com.aeromc.launcher.sdPlugin';
|
||||
|
||||
// Ordner mit dem mitgelieferten Plugin; null, wenn er in dieser Fassung fehlt
|
||||
function quelle(istPaketiert, ressourcenOrdner) {
|
||||
const kandidat = istPaketiert
|
||||
? path.join(ressourcenOrdner, 'streamdeck-plugin', PLUGIN_NAME)
|
||||
: path.join(__dirname, '..', 'streamdeck-plugin', PLUGIN_NAME);
|
||||
return fs.existsSync(path.join(kandidat, 'manifest.json')) ? kandidat : null;
|
||||
}
|
||||
|
||||
// Plugin-Ordner der Stream-Deck-Software; null, wenn sie nie installiert war
|
||||
function pluginOrdner() {
|
||||
const appdata = process.env.APPDATA;
|
||||
if (!appdata) return null;
|
||||
const ordner = path.join(appdata, 'Elgato', 'StreamDeck', 'Plugins');
|
||||
return fs.existsSync(ordner) ? ordner : null;
|
||||
}
|
||||
|
||||
// Was die Stream-Deck-Software selbst im Plugin-Ordner anlegt (Protokolle), zählt nicht mit
|
||||
const IGNORIERT = new Set(['logs']);
|
||||
|
||||
function dateien(basis, rel = '') {
|
||||
const ergebnis = [];
|
||||
for (const e of fs.readdirSync(path.join(basis, rel), { withFileTypes: true })) {
|
||||
if (!rel && IGNORIERT.has(e.name)) continue;
|
||||
const r = rel ? rel + '/' + e.name : e.name;
|
||||
if (e.isDirectory()) ergebnis.push(...dateien(basis, r));
|
||||
else ergebnis.push(r);
|
||||
}
|
||||
return ergebnis.sort();
|
||||
}
|
||||
|
||||
// Prüfsumme über Dateinamen und -inhalt - gleiche Prüfsumme = gleicher Stand
|
||||
function fingerabdruck(ordner) {
|
||||
const h = crypto.createHash('sha1');
|
||||
for (const rel of dateien(ordner)) {
|
||||
h.update(rel + '\0');
|
||||
h.update(fs.readFileSync(path.join(ordner, ...rel.split('/'))));
|
||||
}
|
||||
return h.digest('hex');
|
||||
}
|
||||
|
||||
/*
|
||||
* -> { ok: true, status: 'installiert' | 'aktualisiert' | 'aktuell', neustart }
|
||||
* { ok: false, grund: 'quelle-fehlt' | 'kein-streamdeck' | 'kopieren', message }
|
||||
*/
|
||||
function installiere(opt) {
|
||||
const o = opt || {};
|
||||
if (!o.quelle) return { ok: false, grund: 'quelle-fehlt', message: 'Das Plugin ist in dieser Fassung nicht enthalten.' };
|
||||
const plugins = o.pluginOrdner !== undefined ? o.pluginOrdner : pluginOrdner();
|
||||
if (!plugins || !fs.existsSync(plugins)) {
|
||||
return { ok: false, grund: 'kein-streamdeck', message: 'Die Stream-Deck-Software wurde nicht gefunden.' };
|
||||
}
|
||||
const ziel = path.join(plugins, PLUGIN_NAME);
|
||||
try {
|
||||
const vorhanden = fs.existsSync(ziel);
|
||||
if (vorhanden && fingerabdruck(ziel) === fingerabdruck(o.quelle)) return { ok: true, status: 'aktuell', neustart: false };
|
||||
fs.cpSync(o.quelle, ziel, { recursive: true, force: true, filter: (src) => !IGNORIERT.has(path.basename(src)) || src === o.quelle });
|
||||
return { ok: true, status: vorhanden ? 'aktualisiert' : 'installiert', neustart: true };
|
||||
} catch (err) {
|
||||
return { ok: false, grund: 'kopieren', message: 'Das Plugin konnte nicht kopiert werden (' + err.message + '). Bitte die Stream-Deck-Software beenden und es erneut versuchen.' };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { PLUGIN_NAME, quelle, pluginOrdner, installiere, fingerabdruck };
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "bin",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"ws": "8.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
|
||||
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{ "type": "module", "dependencies": { "ws": "8.18.0" } }
|
||||
@@ -0,0 +1,536 @@
|
||||
// AeroMC-Plugin für das Stream Deck.
|
||||
// Spricht direkt das Stream-Deck-Protokoll (WebSocket) und mit dem Launcher über
|
||||
// dessen lokale Schnittstelle (Einstellung "Stream Deck erlauben" in AeroMC).
|
||||
// Port und Schlüssel stehen in %APPDATA%\AeroMC\streamdeck.json.
|
||||
|
||||
import WebSocket from 'ws';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const A = (n) => 'com.aeromc.launcher.' + n;
|
||||
const AKTION_INSTANZ = A('instanz');
|
||||
const AKTION_CLIP = A('clip');
|
||||
const AKTION_AUFNAHME = A('aufnahme');
|
||||
const AKTION_SERVER = A('server');
|
||||
const AKTION_ZULETZT = A('zuletzt');
|
||||
const AKTION_FENSTER = A('fenster');
|
||||
const AKTION_CLIPMODUS = A('clipmodus');
|
||||
const AKTION_NEUSTART = A('neustart');
|
||||
const AKTION_ORDNER = A('ordner');
|
||||
const AKTION_SPIELZEIT = A('spielzeit');
|
||||
const AKTION_SERVERSTATUS = A('serverstatus');
|
||||
const AKTION_SCREENSHOT = A('screenshot');
|
||||
const AKTION_DREH = A('dreh');
|
||||
const AKTION_FREUNDE = A('freunde');
|
||||
const AKTION_KONTO = A('konto');
|
||||
const AKTION_SCHNELL = A('schnellstart');
|
||||
const AKTION_BACKUP = A('backup');
|
||||
const AKTION_LOGS = A('logs');
|
||||
const AKTION_KONSOLE = A('konsole');
|
||||
const AKTION_ABSTURZ = A('absturz');
|
||||
const CRASH_MS = 30000;
|
||||
const ABFRAGE_MS = 2000;
|
||||
const PING_MS = 30000;
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const arg = (name) => { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : null; };
|
||||
const port = arg('-port');
|
||||
const pluginUUID = arg('-pluginUUID');
|
||||
const registerEvent = arg('-registerEvent');
|
||||
|
||||
const tasten = new Map(); // context -> { action, settings }
|
||||
let stand = null; // letzte Antwort von /state, null = Launcher nicht erreichbar
|
||||
let meldung = null; // kurze Rückmeldung nach einem Tastendruck { context, text, bis }
|
||||
|
||||
const ws = new WebSocket(`ws://127.0.0.1:${port}`);
|
||||
const sende = (obj) => { if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(obj)); };
|
||||
|
||||
// ---------- Launcher ----------
|
||||
|
||||
function verbindung() {
|
||||
const datei = path.join(process.env.APPDATA || '', 'AeroMC', 'streamdeck.json');
|
||||
try { return JSON.parse(fs.readFileSync(datei, 'utf8')); } catch { return null; }
|
||||
}
|
||||
|
||||
async function anfrage(methode, pfad, body) {
|
||||
const v = verbindung();
|
||||
if (!v) return null;
|
||||
try {
|
||||
const res = await fetch(`http://127.0.0.1:${v.port}${pfad}`, {
|
||||
method: methode,
|
||||
headers: { Authorization: `Bearer ${v.token}`, 'Content-Type': 'application/json' },
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
signal: AbortSignal.timeout(3000),
|
||||
});
|
||||
return await res.json();
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
async function holeStand() {
|
||||
// Freunde nur anfordern, wenn eine Freunde-Taste existiert (der Launcher fragt dafür den Server)
|
||||
const mitFreunden = [...tasten.values()].some((t) => t.action === AKTION_FREUNDE);
|
||||
const s = await anfrage('GET', '/state' + (mitFreunden ? '?freunde=1' : ''));
|
||||
stand = s && s.ok ? s : null;
|
||||
}
|
||||
|
||||
// ---------- Tastenbild ----------
|
||||
|
||||
const esc = (t) => String(t).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
||||
|
||||
function zeilen(text, breite = 9) {
|
||||
const woerter = String(text || '').split(/\s+/).filter(Boolean);
|
||||
const out = [];
|
||||
let zeile = '';
|
||||
for (const w of woerter) {
|
||||
if ((zeile + ' ' + w).trim().length <= breite) zeile = (zeile + ' ' + w).trim();
|
||||
else { if (zeile) out.push(zeile); zeile = w; }
|
||||
}
|
||||
if (zeile) out.push(zeile);
|
||||
return out.slice(0, 2).map((z) => (z.length > breite ? z.slice(0, breite - 1) + '…' : z));
|
||||
}
|
||||
|
||||
function dauer(seit) {
|
||||
const s = Math.max(0, Math.floor((Date.now() - seit) / 1000));
|
||||
const h = Math.floor(s / 3600);
|
||||
const m = Math.floor((s % 3600) / 60);
|
||||
return h ? `${h}:${String(m).padStart(2, '0')} h` : `${m} min`;
|
||||
}
|
||||
|
||||
// AeroMC-Logo einmal einlesen und in jedes Tastenbild einbetten
|
||||
let logo = '';
|
||||
try {
|
||||
const datei = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'imgs', 'actions', 'key@2x.png');
|
||||
logo = 'data:image/png;base64,' + fs.readFileSync(datei).toString('base64');
|
||||
} catch { /* ohne Logo geht es auch */ }
|
||||
|
||||
function bild({ farbe, kopf, titel, fuss, symbol }) {
|
||||
const sym = symbol && symbol.bild ? symbol : (logo ? { bild: logo, pixel: false } : null);
|
||||
const t = zeilen(titel);
|
||||
const texte = t.map((z, i) => `<text x="72" y="${92 + i * 20}" font-size="18" font-weight="700" fill="#fff" text-anchor="middle">${esc(z)}</text>`).join('');
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="144" height="144" viewBox="0 0 144 144" font-family="Segoe UI, Arial, sans-serif">`
|
||||
+ `<rect width="144" height="144" rx="14" fill="#14161f"/>`
|
||||
+ (sym ? `<image href="${sym.bild}" x="48" y="4" width="48" height="48" preserveAspectRatio="xMidYMid meet"${sym.pixel ? ' style="image-rendering:pixelated"' : ''}/>` : '')
|
||||
+ `<text x="72" y="68" font-size="14" font-weight="700" fill="${farbe}" text-anchor="middle">${esc(kopf)}</text>`
|
||||
+ texte
|
||||
+ `<text x="72" y="134" font-size="14" fill="#aab" text-anchor="middle">${esc(fuss)}</text>`
|
||||
+ `<rect x="0" y="138" width="144" height="6" fill="${farbe}"/></svg>`;
|
||||
return 'data:image/svg+xml;charset=utf8,' + encodeURIComponent(svg);
|
||||
}
|
||||
|
||||
const GRAU = '#6b7280', GRUEN = '#34d399', ROT = '#f87171', GELB = '#fbbf24', BLAU = '#60a5fa';
|
||||
|
||||
const symbole = new Map(); // Instanz-ID -> { bildId, bild, pixel }
|
||||
|
||||
const koepfe = new Map(); // Konto-UUID -> { bild }
|
||||
const abstuerze = new Map(); // Instanz-ID -> { zeit(Abfrage), bericht, frisch, berichtZeit }
|
||||
|
||||
function symbolKonto(uuid) {
|
||||
const x = uuid && koepfe.get(uuid);
|
||||
return x && x.bild ? x : null;
|
||||
}
|
||||
|
||||
// Instanz, die gerade läuft, sonst die zuletzt gespielte - für Tasten ohne feste Auswahl
|
||||
function standardInstanz() {
|
||||
const l = (stand && stand.instanzen) || [];
|
||||
const lauf = l.find((x) => x.laeuft);
|
||||
return lauf ? lauf.id : (stand && stand.zuletzt) || null;
|
||||
}
|
||||
|
||||
function vor(ms) {
|
||||
const min = Math.max(0, Math.floor((Date.now() - ms) / 60000));
|
||||
if (min < 1) return 'gerade eben';
|
||||
if (min < 60) return 'vor ' + min + ' min';
|
||||
if (min < 1440) return 'vor ' + Math.floor(min / 60) + ' h';
|
||||
return 'vor ' + Math.floor(min / 1440) + ' Tg.';
|
||||
}
|
||||
|
||||
async function pollAbstuerze(erzwingen) {
|
||||
const ids = new Set();
|
||||
for (const t of tasten.values()) if (t.action === AKTION_ABSTURZ) { const id = instanzId(t); if (id) ids.add(id); }
|
||||
for (const id of ids) {
|
||||
const alt = abstuerze.get(id);
|
||||
if (!erzwingen && alt && Date.now() - alt.zeit < CRASH_MS) continue;
|
||||
const r = await anfrage('GET', '/crash?id=' + encodeURIComponent(id));
|
||||
abstuerze.set(id, { zeit: Date.now(), bericht: !!(r && r.bericht), frisch: !!(r && r.frisch), berichtZeit: r && r.zeit });
|
||||
}
|
||||
}
|
||||
|
||||
function symbolFuer(id) {
|
||||
const x = id && symbole.get(id);
|
||||
return x && x.bild ? x : null;
|
||||
}
|
||||
|
||||
// Instanz-Bilder (eigenes Bild oder Minecraft-Icon) nachladen, sobald sie gebraucht werden oder sich ändern
|
||||
async function holeSymbole() {
|
||||
if (!stand) return;
|
||||
const ids = new Set();
|
||||
for (const t of tasten.values()) { const id = instanzId(t); if (id) ids.add(id); }
|
||||
for (const t of tasten.values()) {
|
||||
const u = t.action === AKTION_KONTO && t.settings.kontoId;
|
||||
if (u && !koepfe.has(u)) {
|
||||
const r = await anfrage('GET', '/icon?konto=' + encodeURIComponent(u));
|
||||
koepfe.set(u, { bild: r && r.ok ? r.bild : null });
|
||||
}
|
||||
}
|
||||
for (const id of ids) {
|
||||
const inst = (stand.instanzen || []).find((x) => x.id === id);
|
||||
if (!inst) continue;
|
||||
const alt = symbole.get(id);
|
||||
if (alt && alt.bildId === inst.bildId) continue;
|
||||
const r = await anfrage('GET', '/icon?id=' + encodeURIComponent(id));
|
||||
symbole.set(id, { bildId: inst.bildId, bild: r && r.ok ? r.bild : null, pixel: !!(r && r.pixel) });
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Drehregler (Stream Deck+) ----------
|
||||
|
||||
function dialListe() { return (stand && stand.instanzen) || []; }
|
||||
|
||||
function dialIndex(t) {
|
||||
const l = dialListe();
|
||||
let i = l.findIndex((x) => x.id === t.settings.instanceId);
|
||||
if (i < 0) i = l.findIndex((x) => x.id === (stand && stand.zuletzt));
|
||||
return i < 0 ? 0 : i;
|
||||
}
|
||||
|
||||
function zeichneDial(context) {
|
||||
const t = tasten.get(context);
|
||||
if (!t) return;
|
||||
const l = dialListe();
|
||||
let fb;
|
||||
if (meldung && meldung.context === context && meldung.bis > Date.now()) {
|
||||
fb = { title: 'HINWEIS', value: meldung.text, icon: logo || '', indicator: { value: 0 } };
|
||||
} else if (!stand) {
|
||||
fb = { title: 'AeroMC', value: 'Launcher aus', icon: logo || '', indicator: { value: 0 } };
|
||||
} else if (!l.length) {
|
||||
fb = { title: 'AeroMC', value: 'Keine Instanz', icon: logo || '', indicator: { value: 0 } };
|
||||
} else {
|
||||
const i = dialIndex(t);
|
||||
const x = l[i];
|
||||
const sym = symbolFuer(x.id);
|
||||
fb = {
|
||||
title: x.name,
|
||||
value: x.laeuft ? '● ' + dauer(x.seit || Date.now()) : '▶ bereit',
|
||||
icon: (sym && sym.bild) || logo || '',
|
||||
indicator: { value: Math.round(((i + 1) / l.length) * 100) },
|
||||
};
|
||||
}
|
||||
sende({ event: 'setFeedback', context, payload: fb });
|
||||
}
|
||||
|
||||
const pings = new Map(); // Adresse -> { ok, online, max, latencyMs, zeit }
|
||||
|
||||
function stunden(sek) {
|
||||
if (sek < 3600) return Math.floor(sek / 60) + ' min';
|
||||
const h = sek / 3600;
|
||||
return (h < 100 ? h.toFixed(1).replace('.', ',') : Math.round(h)) + ' h';
|
||||
}
|
||||
|
||||
// Welche Instanz gehört zu dieser Taste?
|
||||
function instanzId(t) {
|
||||
if (t.action === AKTION_ZULETZT) return stand && stand.zuletzt;
|
||||
if (t.action === AKTION_SCHNELL) return (stand && stand.schnellstart && stand.schnellstart[t.settings.slot || '1']) || null;
|
||||
if (t.action === AKTION_LOGS || t.action === AKTION_ABSTURZ) return t.settings.instanceId || standardInstanz();
|
||||
if (t.action === AKTION_DREH) return stand && dialListe().length ? dialListe()[dialIndex(t)].id : null;
|
||||
return t.settings.instanceId || null;
|
||||
}
|
||||
|
||||
function adresse(t) {
|
||||
return String(t.settings.adresse || t.settings.server || '').trim();
|
||||
}
|
||||
|
||||
function zeichne(context) {
|
||||
const t = tasten.get(context);
|
||||
if (!t) return;
|
||||
if (t.action === AKTION_DREH) { zeichneDial(context); return; }
|
||||
const id = instanzId(t);
|
||||
const sym = t.action === AKTION_KONTO ? symbolKonto(t.settings.kontoId) : symbolFuer(id);
|
||||
const i = stand && id ? (stand.instanzen || []).find((x) => x.id === id) : null;
|
||||
if (i) t.name = i.name; // merken, damit die Taste auch bei ausgeschaltetem Launcher ihren Namen zeigt
|
||||
const aus = !stand;
|
||||
const name = (i && i.name) || t.name || 'Instanz';
|
||||
let b;
|
||||
if (meldung && meldung.context === context && meldung.bis > Date.now()) {
|
||||
b = bild({ symbol: sym, farbe: GELB, kopf: 'HINWEIS', titel: meldung.text, fuss: '' });
|
||||
} else switch (t.action) {
|
||||
case AKTION_INSTANZ:
|
||||
case AKTION_ZULETZT: {
|
||||
const kopfLeer = t.action === AKTION_ZULETZT ? 'ZULETZT' : 'INSTANZ';
|
||||
if (t.action === AKTION_INSTANZ && !id) b = bild({ symbol: sym, farbe: GRAU, kopf: kopfLeer, titel: 'Bitte wählen', fuss: '' });
|
||||
else if (aus) b = bild({ symbol: sym, farbe: GRAU, kopf: 'AUS', titel: name, fuss: 'Launcher aus' });
|
||||
else if (!i) b = bild({ symbol: sym, farbe: GRAU, kopf: kopfLeer, titel: t.action === AKTION_ZULETZT ? 'noch nichts gespielt' : 'nicht gefunden', fuss: '' });
|
||||
else if (i.laeuft) b = bild({ symbol: sym, farbe: GRUEN, kopf: '● LÄUFT', titel: i.name, fuss: i.seit ? dauer(i.seit) : '■ beenden' });
|
||||
else b = bild({ symbol: sym, farbe: BLAU, kopf: t.action === AKTION_ZULETZT ? '▶ ZULETZT' : '▶ START', titel: i.name, fuss: '' });
|
||||
break;
|
||||
}
|
||||
case AKTION_SERVER: {
|
||||
const adr = adresse(t);
|
||||
if (!id || !adr) b = bild({ symbol: sym, farbe: GRAU, kopf: 'SERVER', titel: 'Bitte wählen', fuss: '' });
|
||||
else if (aus) b = bild({ symbol: sym, farbe: GRAU, kopf: 'AUS', titel: adr, fuss: 'Launcher aus' });
|
||||
else if (i && i.laeuft) b = bild({ symbol: sym, farbe: GRUEN, kopf: '● LÄUFT', titel: adr, fuss: name });
|
||||
else b = bild({ symbol: sym, farbe: BLAU, kopf: '▶ BEITRETEN', titel: t.settings.serverName || adr, fuss: name });
|
||||
break;
|
||||
}
|
||||
case AKTION_NEUSTART:
|
||||
if (!id) b = bild({ symbol: sym, farbe: GRAU, kopf: 'NEUSTART', titel: 'Bitte wählen', fuss: '' });
|
||||
else if (aus) b = bild({ symbol: sym, farbe: GRAU, kopf: 'NEUSTART', titel: name, fuss: 'Launcher aus' });
|
||||
else if (i && i.laeuft) b = bild({ symbol: sym, farbe: GELB, kopf: '↻ NEUSTART', titel: name, fuss: dauer(i.seit || Date.now()) });
|
||||
else b = bild({ symbol: sym, farbe: GRAU, kopf: 'NEUSTART', titel: name, fuss: 'läuft nicht' });
|
||||
break;
|
||||
case AKTION_FENSTER:
|
||||
b = bild({ symbol: sym, farbe: aus ? GRAU : BLAU, kopf: 'LAUNCHER', titel: 'Fenster', fuss: aus ? 'Launcher aus' : 'zeigen / weg' });
|
||||
break;
|
||||
case AKTION_CLIPMODUS:
|
||||
if (aus) b = bild({ symbol: sym, farbe: GRAU, kopf: 'CLIP-MODUS', titel: 'Puffer', fuss: 'Launcher aus' });
|
||||
else if (stand.clipStartet) b = bild({ symbol: sym, farbe: GELB, kopf: 'CLIP-MODUS', titel: 'Puffer', fuss: 'startet …' });
|
||||
else b = stand.clip ? bild({ symbol: sym, farbe: GRUEN, kopf: 'CLIP-MODUS', titel: 'Puffer', fuss: '● an' }) : bild({ symbol: sym, farbe: GRAU, kopf: 'CLIP-MODUS', titel: 'Puffer', fuss: 'aus' });
|
||||
break;
|
||||
case AKTION_ORDNER:
|
||||
b = bild({ symbol: sym, farbe: aus ? GRAU : BLAU, kopf: 'ORDNER', titel: 'Aufnahmen', fuss: aus ? 'Launcher aus' : (id ? name : '') });
|
||||
break;
|
||||
case AKTION_SPIELZEIT: {
|
||||
if (aus) { b = bild({ symbol: sym, farbe: GRAU, kopf: 'SPIELZEIT', titel: '–', fuss: 'Launcher aus' }); break; }
|
||||
const liste = id ? (i ? [i] : []) : stand.instanzen || [];
|
||||
const summe = liste.reduce((n, x) => n + (x.gesamt || 0), 0);
|
||||
const lauf = liste.find((x) => x.laeuft && x.seit);
|
||||
b = lauf
|
||||
? bild({ symbol: sym, farbe: GRUEN, kopf: '● SITZUNG', titel: dauer(lauf.seit), fuss: 'gesamt ' + stunden(summe + Math.floor((Date.now() - lauf.seit) / 1000)) })
|
||||
: bild({ symbol: sym, farbe: BLAU, kopf: 'SPIELZEIT', titel: stunden(summe), fuss: id ? name : 'alle Instanzen' });
|
||||
break;
|
||||
}
|
||||
case AKTION_SERVERSTATUS: {
|
||||
const adr = adresse(t);
|
||||
const p = adr && pings.get(adr);
|
||||
if (!adr) b = bild({ symbol: sym, farbe: GRAU, kopf: 'SERVER', titel: 'Bitte wählen', fuss: '' });
|
||||
else if (aus) b = bild({ symbol: sym, farbe: GRAU, kopf: 'AUS', titel: adr, fuss: 'Launcher aus' });
|
||||
else if (!p) b = bild({ symbol: sym, farbe: GRAU, kopf: 'SERVER', titel: adr, fuss: 'prüfe …' });
|
||||
else if (!p.ok) b = bild({ symbol: sym, farbe: ROT, kopf: '● OFFLINE', titel: t.settings.serverName || adr, fuss: '' });
|
||||
else b = bild({ symbol: sym, farbe: GRUEN, kopf: '● ONLINE', titel: p.online != null ? p.online + ' Spieler' : 'online', fuss: (p.max != null ? 'max ' + p.max + ' · ' : '') + p.latencyMs + ' ms' });
|
||||
break;
|
||||
}
|
||||
case AKTION_SCHNELL: {
|
||||
const slot = t.settings.slot || '1';
|
||||
if (aus) b = bild({ symbol: sym, farbe: GRAU, kopf: 'AUS', titel: name, fuss: 'Launcher aus' });
|
||||
else if (!id || !i) b = bild({ symbol: sym, farbe: GRAU, kopf: 'STRG+' + slot, titel: 'nicht belegt', fuss: '' });
|
||||
else if (i.laeuft) b = bild({ symbol: sym, farbe: GRUEN, kopf: '● LÄUFT', titel: i.name, fuss: i.seit ? dauer(i.seit) : '■ beenden' });
|
||||
else b = bild({ symbol: sym, farbe: BLAU, kopf: '▶ STRG+' + slot, titel: i.name, fuss: '' });
|
||||
break;
|
||||
}
|
||||
case AKTION_KONTO: {
|
||||
const k = stand && (stand.konten || []).find((x) => x.id === t.settings.kontoId);
|
||||
if (!t.settings.kontoId) b = bild({ symbol: sym, farbe: GRAU, kopf: 'KONTO', titel: 'Bitte wählen', fuss: '' });
|
||||
else if (aus) b = bild({ symbol: sym, farbe: GRAU, kopf: 'AUS', titel: t.name || 'Konto', fuss: 'Launcher aus' });
|
||||
else if (!k) b = bild({ symbol: sym, farbe: ROT, kopf: 'KONTO', titel: 'nicht gefunden', fuss: '' });
|
||||
else { t.name = k.name; b = bild({ symbol: sym, farbe: k.aktiv ? GRUEN : BLAU, kopf: k.aktiv ? '● AKTIV' : 'KONTO', titel: k.name, fuss: k.spielt ? 'spielt gerade' : (k.aktiv ? '' : 'wechseln') }); }
|
||||
break;
|
||||
}
|
||||
case AKTION_FREUNDE: {
|
||||
const f = stand && stand.freunde;
|
||||
if (aus) b = bild({ symbol: sym, farbe: GRAU, kopf: 'FREUNDE', titel: '–', fuss: 'Launcher aus' });
|
||||
else if (!f) b = bild({ symbol: sym, farbe: GRAU, kopf: 'FREUNDE', titel: 'Keine Daten', fuss: 'aus / lädt' });
|
||||
else b = bild({ symbol: sym, farbe: f.online ? GRUEN : GRAU, kopf: f.online ? '● ONLINE' : 'FREUNDE', titel: f.online + ' online', fuss: 'von ' + f.gesamt });
|
||||
break;
|
||||
}
|
||||
case AKTION_BACKUP: {
|
||||
const bk = stand && stand.backup;
|
||||
const frisch = bk && bk.zeit && Date.now() - bk.zeit < 600000;
|
||||
if (aus) b = bild({ symbol: sym, farbe: GRAU, kopf: 'BACKUP', titel: 'Jetzt sichern', fuss: 'Launcher aus' });
|
||||
else if (bk && bk.laeuft) b = bild({ symbol: sym, farbe: GELB, kopf: '● LÄUFT', titel: 'Backup', fuss: 'bitte warten' });
|
||||
else if (frisch && bk.ok) b = bild({ symbol: sym, farbe: GRUEN, kopf: '✔ FERTIG', titel: 'Backup', fuss: vor(bk.zeit) });
|
||||
else if (frisch) b = bild({ symbol: sym, farbe: ROT, kopf: 'FEHLER', titel: 'Backup', fuss: vor(bk.zeit) });
|
||||
else b = bild({ symbol: sym, farbe: BLAU, kopf: 'BACKUP', titel: 'Jetzt sichern', fuss: '' });
|
||||
break;
|
||||
}
|
||||
case AKTION_LOGS:
|
||||
b = bild({ symbol: sym, farbe: aus ? GRAU : BLAU, kopf: 'LOGS', titel: 'Logs öffnen', fuss: aus ? 'Launcher aus' : (i ? i.name : '') });
|
||||
break;
|
||||
case AKTION_KONSOLE:
|
||||
b = bild({ symbol: sym, farbe: aus ? GRAU : BLAU, kopf: 'KONSOLE', titel: 'Konsole', fuss: aus ? 'Launcher aus' : '' });
|
||||
break;
|
||||
case AKTION_ABSTURZ: {
|
||||
const a = id && abstuerze.get(id);
|
||||
if (aus) b = bild({ symbol: sym, farbe: GRAU, kopf: 'ABSTURZ', titel: 'Bericht', fuss: 'Launcher aus' });
|
||||
else if (!a) b = bild({ symbol: sym, farbe: GRAU, kopf: 'ABSTURZ', titel: 'Bericht', fuss: id ? 'prüfe …' : 'keine Instanz' });
|
||||
else if (!a.bericht) b = bild({ symbol: sym, farbe: GRAU, kopf: 'ABSTURZ', titel: 'keiner', fuss: i ? i.name : '' });
|
||||
else if (a.frisch) b = bild({ symbol: sym, farbe: ROT, kopf: '⚠ ABSTURZ', titel: 'Bericht zeigen', fuss: a.berichtZeit ? vor(a.berichtZeit) : '' });
|
||||
else b = bild({ symbol: sym, farbe: GRAU, kopf: 'ABSTURZ', titel: 'kein neuer', fuss: a.berichtZeit ? 'letzter ' + vor(a.berichtZeit) : '' });
|
||||
break;
|
||||
}
|
||||
case AKTION_SCREENSHOT:
|
||||
b = bild({ symbol: sym, farbe: aus ? GRAU : BLAU, kopf: 'SCREENSHOT', titel: 'Spielfenster', fuss: aus ? 'Launcher aus' : '' });
|
||||
break;
|
||||
case AKTION_CLIP:
|
||||
if (aus) b = bild({ symbol: sym, farbe: GRAU, kopf: 'CLIP', titel: 'Clip speichern', fuss: 'Launcher aus' });
|
||||
else if (stand.clipStartet) b = bild({ symbol: sym, farbe: GELB, kopf: 'CLIP', titel: 'Clip speichern', fuss: 'startet …' });
|
||||
else b = stand.clip
|
||||
? bild({ symbol: sym, farbe: GRUEN, kopf: 'CLIP', titel: 'Clip speichern', fuss: 'bereit' })
|
||||
: bild({ symbol: sym, farbe: GRAU, kopf: 'CLIP', titel: 'Clip speichern', fuss: 'Modus aus' });
|
||||
break;
|
||||
default:
|
||||
if (aus) b = bild({ symbol: sym, farbe: GRAU, kopf: 'AUFNAHME', titel: 'Aufnahme starten', fuss: 'Launcher aus' });
|
||||
else b = stand.aufnahme
|
||||
? bild({ symbol: sym, farbe: ROT, kopf: '● REC', titel: 'Aufnahme stoppen', fuss: '' })
|
||||
: bild({ symbol: sym, farbe: BLAU, kopf: 'AUFNAHME', titel: 'Aufnahme starten', fuss: '' });
|
||||
}
|
||||
sende({ event: 'setImage', context, payload: { image: b, target: 0 } });
|
||||
}
|
||||
|
||||
const zeichneAlle = () => { for (const c of tasten.keys()) zeichne(c); };
|
||||
|
||||
async function pingeFaellige(erzwingen) {
|
||||
const adressen = new Set();
|
||||
for (const t of tasten.values()) if (t.action === AKTION_SERVERSTATUS && adresse(t)) adressen.add(adresse(t));
|
||||
for (const adr of adressen) {
|
||||
const alt = pings.get(adr);
|
||||
if (!erzwingen && alt && Date.now() - alt.zeit < PING_MS) continue;
|
||||
const r = await anfrage('GET', '/ping?ip=' + encodeURIComponent(adr));
|
||||
pings.set(adr, Object.assign({ ok: false }, r || {}, { zeit: Date.now() }));
|
||||
}
|
||||
}
|
||||
|
||||
async function abfrage() {
|
||||
if (!tasten.size) return;
|
||||
await holeStand();
|
||||
if (stand) { await holeSymbole(); await pingeFaellige(false); await pollAbstuerze(false); }
|
||||
zeichneAlle();
|
||||
}
|
||||
|
||||
// ---------- Stream-Deck-Ereignisse ----------
|
||||
|
||||
function zeigeMeldung(context, text) {
|
||||
meldung = { context, text, bis: Date.now() + 2500 };
|
||||
zeichne(context);
|
||||
setTimeout(() => zeichne(context), 2600);
|
||||
}
|
||||
|
||||
const FEHLERTEXT = {
|
||||
[AKTION_CLIP]: 'Clip-Modus aus',
|
||||
[AKTION_AUFNAHME]: 'Spiel läuft nicht',
|
||||
[AKTION_CLIPMODUS]: 'Spiel läuft nicht',
|
||||
[AKTION_NEUSTART]: 'Läuft nicht',
|
||||
[AKTION_SERVER]: 'Läuft schon',
|
||||
[AKTION_SCREENSHOT]: 'Kein Spiel',
|
||||
[AKTION_ABSTURZ]: 'Kein Bericht',
|
||||
[AKTION_BACKUP]: 'Läuft schon',
|
||||
[AKTION_SCHNELL]: 'Nicht belegt',
|
||||
};
|
||||
|
||||
async function tastendruck(context) {
|
||||
const t = tasten.get(context);
|
||||
if (!t) return;
|
||||
const id = instanzId(t);
|
||||
const i = stand && id ? (stand.instanzen || []).find((x) => x.id === id) : null;
|
||||
let erg;
|
||||
switch (t.action) {
|
||||
case AKTION_INSTANZ:
|
||||
case AKTION_ZULETZT:
|
||||
case AKTION_SCHNELL:
|
||||
case AKTION_DREH:
|
||||
if (!id) { sende({ event: 'showAlert', context }); return; }
|
||||
erg = await anfrage('POST', i && i.laeuft ? '/stop' : '/launch', { id });
|
||||
break;
|
||||
case AKTION_SCREENSHOT: erg = await anfrage('POST', '/screenshot'); break;
|
||||
case AKTION_FREUNDE: erg = await anfrage('POST', '/open', { art: 'friends' }); break;
|
||||
case AKTION_KONSOLE: erg = await anfrage('POST', '/open', { art: 'console' }); break;
|
||||
case AKTION_LOGS: erg = await anfrage('POST', '/open', { art: 'logs', id: id || '' }); break;
|
||||
case AKTION_ABSTURZ: erg = await anfrage('POST', '/open', { art: 'crash', id: id || '' }); break;
|
||||
case AKTION_BACKUP: erg = await anfrage('POST', '/backup'); break;
|
||||
case AKTION_KONTO:
|
||||
if (!t.settings.kontoId) { sende({ event: 'showAlert', context }); return; }
|
||||
erg = await anfrage('POST', '/account', { id: t.settings.kontoId });
|
||||
break;
|
||||
case AKTION_SERVER:
|
||||
if (!id || !adresse(t)) { sende({ event: 'showAlert', context }); return; }
|
||||
erg = await anfrage('POST', '/launch', { id, server: adresse(t) });
|
||||
break;
|
||||
case AKTION_NEUSTART:
|
||||
if (!id) { sende({ event: 'showAlert', context }); return; }
|
||||
erg = await anfrage('POST', '/restart', { id });
|
||||
break;
|
||||
case AKTION_FENSTER: erg = await anfrage('POST', '/window'); break;
|
||||
case AKTION_CLIPMODUS: erg = await anfrage('POST', '/clipmode'); break;
|
||||
case AKTION_ORDNER: erg = await anfrage('POST', '/folder', id ? { id } : {}); break;
|
||||
case AKTION_SPIELZEIT: zeichne(context); return;
|
||||
case AKTION_SERVERSTATUS:
|
||||
pings.delete(adresse(t));
|
||||
await pingeFaellige(true);
|
||||
zeichne(context);
|
||||
return;
|
||||
case AKTION_CLIP:
|
||||
if (stand && stand.clipStartet) { zeigeMeldung(context, 'Startet noch …'); return; }
|
||||
erg = await anfrage('POST', '/clip');
|
||||
break;
|
||||
default: erg = await anfrage('POST', '/record');
|
||||
}
|
||||
if (erg && erg.ok) sende({ event: 'showOk', context });
|
||||
else {
|
||||
sende({ event: 'showAlert', context });
|
||||
zeigeMeldung(context, !erg ? 'Launcher aus' : FEHLERTEXT[t.action] || 'Nicht möglich');
|
||||
}
|
||||
setTimeout(abfrage, 600);
|
||||
}
|
||||
|
||||
ws.on('open', () => sende({ event: registerEvent, uuid: pluginUUID }));
|
||||
|
||||
ws.on('message', async (roh) => {
|
||||
let m;
|
||||
try { m = JSON.parse(roh.toString()); } catch { return; }
|
||||
switch (m.event) {
|
||||
case 'willAppear':
|
||||
tasten.set(m.context, { action: m.action, settings: (m.payload && m.payload.settings) || {} });
|
||||
await holeStand();
|
||||
await holeSymbole();
|
||||
zeichneAlle();
|
||||
break;
|
||||
case 'willDisappear':
|
||||
tasten.delete(m.context);
|
||||
break;
|
||||
case 'didReceiveSettings': {
|
||||
const t = tasten.get(m.context);
|
||||
if (t) { t.settings = (m.payload && m.payload.settings) || {}; zeichne(m.context); holeSymbole().then(() => pollAbstuerze(true)).then(() => zeichne(m.context)); }
|
||||
break;
|
||||
}
|
||||
case 'keyDown':
|
||||
case 'dialDown':
|
||||
tastendruck(m.context);
|
||||
break;
|
||||
case 'dialRotate': {
|
||||
const t = tasten.get(m.context);
|
||||
const l = dialListe();
|
||||
if (!t || !l.length) break;
|
||||
const n = l.length;
|
||||
const neu = (((dialIndex(t) + (m.payload.ticks || 0)) % n) + n) % n;
|
||||
t.settings.instanceId = l[neu].id;
|
||||
sende({ event: 'setSettings', context: m.context, payload: t.settings });
|
||||
zeichneDial(m.context);
|
||||
holeSymbole().then(() => zeichneDial(m.context));
|
||||
break;
|
||||
}
|
||||
case 'touchTap':
|
||||
// Tippen auf den Touch-Streifen: Launcher-Fenster zeigen bzw. wegminimieren
|
||||
anfrage('POST', '/window');
|
||||
break;
|
||||
case 'sendToPlugin': {
|
||||
// Eigenschaften-Fenster fragt nach der Instanzliste bzw. den Servern einer Instanz
|
||||
await holeStand();
|
||||
const payload = {
|
||||
erreichbar: !!stand,
|
||||
instanzen: stand ? stand.instanzen.map((i) => ({ id: i.id, name: i.name })) : [],
|
||||
konten: stand ? (stand.konten || []) : [],
|
||||
schnellstart: stand ? (stand.schnellstart || {}) : {},
|
||||
};
|
||||
const frage = m.payload || {};
|
||||
if (frage.frage === 'server' && frage.id && stand) {
|
||||
const r = await anfrage('GET', '/servers?id=' + encodeURIComponent(frage.id));
|
||||
payload.server = (r && r.server) || [];
|
||||
}
|
||||
sende({ event: 'sendToPropertyInspector', action: m.action, context: m.context, payload });
|
||||
break;
|
||||
}
|
||||
default:
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('close', () => process.exit(0));
|
||||
setInterval(abfrage, ABFRAGE_MS);
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 57 KiB |
@@ -0,0 +1,365 @@
|
||||
{
|
||||
"Name": "AeroMC",
|
||||
"Version": "1.1.0.0",
|
||||
"Author": "M_Viper",
|
||||
"Description": "Steuert den AeroMC-Launcher: Instanzen starten und beenden, Clips speichern und die Aufnahme umschalten - mit Live-Status auf den Tasten.",
|
||||
"Category": "AeroMC",
|
||||
"CategoryIcon": "imgs/plugin/category-icon",
|
||||
"Icon": "imgs/plugin/marketplace",
|
||||
"URL": "https://aeromc.viper.ipv64.net",
|
||||
"SDKVersion": 2,
|
||||
"UUID": "com.aeromc.launcher",
|
||||
"Nodejs": {
|
||||
"Version": "20"
|
||||
},
|
||||
"Software": {
|
||||
"MinimumVersion": "6.5"
|
||||
},
|
||||
"OS": [
|
||||
{
|
||||
"Platform": "windows",
|
||||
"MinimumVersion": "10"
|
||||
}
|
||||
],
|
||||
"CodePath": "bin/plugin.js",
|
||||
"Actions": [
|
||||
{
|
||||
"Name": "Instanz starten/beenden",
|
||||
"UUID": "com.aeromc.launcher.instanz",
|
||||
"Icon": "imgs/actions/icon",
|
||||
"Tooltip": "Startet die gewählte Instanz; läuft sie schon, beendet die Taste sie wieder. Zeigt den Zustand live an.",
|
||||
"PropertyInspectorPath": "ui/instanz.html",
|
||||
"Controllers": [
|
||||
"Keypad"
|
||||
],
|
||||
"States": [
|
||||
{
|
||||
"Image": "imgs/actions/key",
|
||||
"TitleAlignment": "bottom",
|
||||
"ShowTitle": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Clip speichern",
|
||||
"UUID": "com.aeromc.launcher.clip",
|
||||
"Icon": "imgs/actions/icon",
|
||||
"Tooltip": "Speichert die letzten Sekunden als Clip. Der Clip-Modus muss laufen (Taste „Clip-Modus ein/aus“).",
|
||||
"Controllers": [
|
||||
"Keypad"
|
||||
],
|
||||
"States": [
|
||||
{
|
||||
"Image": "imgs/actions/key",
|
||||
"TitleAlignment": "bottom",
|
||||
"ShowTitle": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Aufnahme starten/stoppen",
|
||||
"UUID": "com.aeromc.launcher.aufnahme",
|
||||
"Icon": "imgs/actions/icon",
|
||||
"Tooltip": "Startet bzw. stoppt die Aufnahme des laufenden Spiels. Das Aufnahme-Fenster öffnet sich dabei unsichtbar von selbst.",
|
||||
"Controllers": [
|
||||
"Keypad"
|
||||
],
|
||||
"States": [
|
||||
{
|
||||
"Image": "imgs/actions/key",
|
||||
"TitleAlignment": "bottom",
|
||||
"ShowTitle": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Server beitreten",
|
||||
"UUID": "com.aeromc.launcher.server",
|
||||
"Icon": "imgs/actions/icon",
|
||||
"Tooltip": "Startet die gewählte Instanz und verbindet direkt mit dem gewählten Server.",
|
||||
"PropertyInspectorPath": "ui/server.html",
|
||||
"Controllers": [
|
||||
"Keypad"
|
||||
],
|
||||
"States": [
|
||||
{
|
||||
"Image": "imgs/actions/key",
|
||||
"TitleAlignment": "bottom",
|
||||
"ShowTitle": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Zuletzt gespielte Instanz",
|
||||
"UUID": "com.aeromc.launcher.zuletzt",
|
||||
"Icon": "imgs/actions/icon",
|
||||
"Tooltip": "Startet (bzw. beendet) die Instanz, die du zuletzt gespielt hast – ohne Auswahl.",
|
||||
"Controllers": [
|
||||
"Keypad"
|
||||
],
|
||||
"States": [
|
||||
{
|
||||
"Image": "imgs/actions/key",
|
||||
"TitleAlignment": "bottom",
|
||||
"ShowTitle": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Launcher-Fenster",
|
||||
"UUID": "com.aeromc.launcher.fenster",
|
||||
"Icon": "imgs/actions/icon",
|
||||
"Tooltip": "Holt das AeroMC-Fenster nach vorn; ist es schon vorn, wird es minimiert.",
|
||||
"Controllers": [
|
||||
"Keypad"
|
||||
],
|
||||
"States": [
|
||||
{
|
||||
"Image": "imgs/actions/key",
|
||||
"TitleAlignment": "bottom",
|
||||
"ShowTitle": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Clip-Modus ein/aus",
|
||||
"UUID": "com.aeromc.launcher.clipmodus",
|
||||
"Icon": "imgs/actions/icon",
|
||||
"Tooltip": "Schaltet den Clip-Puffer des laufenden Spiels ein oder aus. Das Aufnahme-Fenster öffnet sich dabei unsichtbar von selbst.",
|
||||
"Controllers": [
|
||||
"Keypad"
|
||||
],
|
||||
"States": [
|
||||
{
|
||||
"Image": "imgs/actions/key",
|
||||
"TitleAlignment": "bottom",
|
||||
"ShowTitle": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Instanz neu starten",
|
||||
"UUID": "com.aeromc.launcher.neustart",
|
||||
"Icon": "imgs/actions/icon",
|
||||
"Tooltip": "Beendet die laufende Instanz sauber und startet sie wieder (mit demselben Server).",
|
||||
"PropertyInspectorPath": "ui/instanz.html",
|
||||
"Controllers": [
|
||||
"Keypad"
|
||||
],
|
||||
"States": [
|
||||
{
|
||||
"Image": "imgs/actions/key",
|
||||
"TitleAlignment": "bottom",
|
||||
"ShowTitle": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Aufnahmen-Ordner",
|
||||
"UUID": "com.aeromc.launcher.ordner",
|
||||
"Icon": "imgs/actions/icon",
|
||||
"Tooltip": "Öffnet den Ordner mit Aufnahmen und Clips im Explorer.",
|
||||
"PropertyInspectorPath": "ui/instanz.html",
|
||||
"Controllers": [
|
||||
"Keypad"
|
||||
],
|
||||
"States": [
|
||||
{
|
||||
"Image": "imgs/actions/key",
|
||||
"TitleAlignment": "bottom",
|
||||
"ShowTitle": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Spielzeit",
|
||||
"UUID": "com.aeromc.launcher.spielzeit",
|
||||
"Icon": "imgs/actions/icon",
|
||||
"Tooltip": "Zeigt die Gesamtspielzeit (einer Instanz oder aller) und während des Spielens die aktuelle Sitzung.",
|
||||
"PropertyInspectorPath": "ui/instanz.html",
|
||||
"Controllers": [
|
||||
"Keypad"
|
||||
],
|
||||
"States": [
|
||||
{
|
||||
"Image": "imgs/actions/key",
|
||||
"TitleAlignment": "bottom",
|
||||
"ShowTitle": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Server-Status",
|
||||
"UUID": "com.aeromc.launcher.serverstatus",
|
||||
"Icon": "imgs/actions/icon",
|
||||
"Tooltip": "Zeigt, ob ein Server online ist, wie viele Spieler drauf sind und den Ping. Aktualisiert alle 30 Sekunden.",
|
||||
"PropertyInspectorPath": "ui/server.html",
|
||||
"Controllers": [
|
||||
"Keypad"
|
||||
],
|
||||
"States": [
|
||||
{
|
||||
"Image": "imgs/actions/key",
|
||||
"TitleAlignment": "bottom",
|
||||
"ShowTitle": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Screenshot",
|
||||
"UUID": "com.aeromc.launcher.screenshot",
|
||||
"Icon": "imgs/actions/icon",
|
||||
"Tooltip": "Speichert ein Bild des Spielfensters im Screenshot-Ordner der laufenden Instanz. Überlagernde Fenster (z. B. das Stream Deck) erscheinen mit auf dem Bild.",
|
||||
"Controllers": [
|
||||
"Keypad"
|
||||
],
|
||||
"States": [
|
||||
{
|
||||
"Image": "imgs/actions/key",
|
||||
"TitleAlignment": "bottom",
|
||||
"ShowTitle": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Instanz-Drehregler",
|
||||
"UUID": "com.aeromc.launcher.dreh",
|
||||
"Icon": "imgs/actions/icon",
|
||||
"Tooltip": "Für das Stream Deck+: Drehen wählt die Instanz, Drücken startet bzw. beendet sie, Tippen auf den Streifen zeigt oder minimiert den Launcher.",
|
||||
"Controllers": [
|
||||
"Encoder"
|
||||
],
|
||||
"Encoder": {
|
||||
"layout": "$B1",
|
||||
"TriggerDescription": {
|
||||
"Rotate": "Instanz wählen",
|
||||
"Push": "Starten / Beenden",
|
||||
"Touch": "Launcher-Fenster"
|
||||
}
|
||||
},
|
||||
"States": [
|
||||
{
|
||||
"Image": "imgs/actions/key",
|
||||
"TitleAlignment": "bottom",
|
||||
"ShowTitle": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Freunde online",
|
||||
"UUID": "com.aeromc.launcher.freunde",
|
||||
"Icon": "imgs/actions/icon",
|
||||
"Tooltip": "Zeigt, wie viele deiner AeroMC-Freunde gerade online sind. Ein Druck öffnet das Freunde-Fenster.",
|
||||
"Controllers": [
|
||||
"Keypad"
|
||||
],
|
||||
"States": [
|
||||
{
|
||||
"Image": "imgs/actions/key",
|
||||
"TitleAlignment": "bottom",
|
||||
"ShowTitle": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Konto wechseln",
|
||||
"UUID": "com.aeromc.launcher.konto",
|
||||
"Icon": "imgs/actions/icon",
|
||||
"Tooltip": "Setzt das gewählte Konto als aktives Konto in AeroMC. Zeigt den Spielerkopf; grün = gerade aktiv.",
|
||||
"PropertyInspectorPath": "ui/konto.html",
|
||||
"Controllers": [
|
||||
"Keypad"
|
||||
],
|
||||
"States": [
|
||||
{
|
||||
"Image": "imgs/actions/key",
|
||||
"TitleAlignment": "bottom",
|
||||
"ShowTitle": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Schnellstart 1–5",
|
||||
"UUID": "com.aeromc.launcher.schnellstart",
|
||||
"Icon": "imgs/actions/icon",
|
||||
"Tooltip": "Startet die Instanz, die du in AeroMC auf Strg+1 bis Strg+5 gelegt hast (bzw. beendet sie, wenn sie läuft).",
|
||||
"PropertyInspectorPath": "ui/schnellstart.html",
|
||||
"Controllers": [
|
||||
"Keypad"
|
||||
],
|
||||
"States": [
|
||||
{
|
||||
"Image": "imgs/actions/key",
|
||||
"TitleAlignment": "bottom",
|
||||
"ShowTitle": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Backup jetzt",
|
||||
"UUID": "com.aeromc.launcher.backup",
|
||||
"Icon": "imgs/actions/icon",
|
||||
"Tooltip": "Sichert alle Instanzen. Zeigt „läuft“ und nach dem Ende „fertig“ bzw. „Fehler“.",
|
||||
"Controllers": [
|
||||
"Keypad"
|
||||
],
|
||||
"States": [
|
||||
{
|
||||
"Image": "imgs/actions/key",
|
||||
"TitleAlignment": "bottom",
|
||||
"ShowTitle": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Logdateien",
|
||||
"UUID": "com.aeromc.launcher.logs",
|
||||
"Icon": "imgs/actions/icon",
|
||||
"Tooltip": "Öffnet die Logdateien der gewählten (sonst der laufenden oder zuletzt gespielten) Instanz.",
|
||||
"PropertyInspectorPath": "ui/instanz.html",
|
||||
"Controllers": [
|
||||
"Keypad"
|
||||
],
|
||||
"States": [
|
||||
{
|
||||
"Image": "imgs/actions/key",
|
||||
"TitleAlignment": "bottom",
|
||||
"ShowTitle": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Konsole",
|
||||
"UUID": "com.aeromc.launcher.konsole",
|
||||
"Icon": "imgs/actions/icon",
|
||||
"Tooltip": "Öffnet die Konsole von AeroMC.",
|
||||
"Controllers": [
|
||||
"Keypad"
|
||||
],
|
||||
"States": [
|
||||
{
|
||||
"Image": "imgs/actions/key",
|
||||
"TitleAlignment": "bottom",
|
||||
"ShowTitle": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Letzter Absturz",
|
||||
"UUID": "com.aeromc.launcher.absturz",
|
||||
"Icon": "imgs/actions/icon",
|
||||
"Tooltip": "Öffnet den Absturzbericht der gewählten (sonst laufenden/zuletzt gespielten) Instanz. Leuchtet rot, wenn der letzte Start abgestürzt ist.",
|
||||
"PropertyInspectorPath": "ui/instanz.html",
|
||||
"Controllers": [
|
||||
"Keypad"
|
||||
],
|
||||
"States": [
|
||||
{
|
||||
"Image": "imgs/actions/key",
|
||||
"TitleAlignment": "bottom",
|
||||
"ShowTitle": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>AeroMC-Instanz</title>
|
||||
<style>
|
||||
body { font-family: "Segoe UI", sans-serif; font-size: 13px; color: #d8d8d8; background: #2d2d2d; margin: 0; padding: 12px; }
|
||||
label { display: block; margin-bottom: 4px; color: #a8a8a8; }
|
||||
select { width: 100%; padding: 6px; background: #3d3d3d; color: #fff; border: 1px solid #555; border-radius: 4px; }
|
||||
.hint { margin-top: 10px; color: #8a8a8a; font-size: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<label for="instanz">Instanz</label>
|
||||
<select id="instanz"><option value="">Lade …</option></select>
|
||||
<p class="hint" id="hinweis"></p>
|
||||
<script>
|
||||
// bei diesen Tasten darf die Instanz leer bleiben (= alle Instanzen)
|
||||
const OPTIONAL = ['com.aeromc.launcher.spielzeit', 'com.aeromc.launcher.ordner', 'com.aeromc.launcher.logs', 'com.aeromc.launcher.absturz'];
|
||||
const OPTIONAL_TEXT = { 'com.aeromc.launcher.logs': 'Laufende / zuletzt gespielte', 'com.aeromc.launcher.absturz': 'Laufende / zuletzt gespielte' };
|
||||
let ws, uuid, aktion, einstellungen = {}, wiederholung = null;
|
||||
|
||||
function frageInstanzen() {
|
||||
if (ws.readyState === 1) ws.send(JSON.stringify({ event: 'sendToPlugin', action: aktion, context: uuid, payload: { frage: 'instanzen' } }));
|
||||
}
|
||||
|
||||
function fuelle(p) {
|
||||
// solange AeroMC nicht erreichbar ist, alle 2 s neu fragen - ein Neuauswählen der Taste ist dann nicht nötig
|
||||
if (p.erreichbar) { clearInterval(wiederholung); wiederholung = null; }
|
||||
else if (!wiederholung) wiederholung = setInterval(frageInstanzen, 2000);
|
||||
const sel = document.getElementById('instanz');
|
||||
const hinweis = document.getElementById('hinweis');
|
||||
sel.innerHTML = '';
|
||||
if (!p.erreichbar) {
|
||||
sel.innerHTML = '<option value="">AeroMC nicht erreichbar</option>';
|
||||
hinweis.textContent = 'AeroMC starten und unter Optionen → Allgemein „Stream Deck erlauben“ einschalten. Dieses Fenster aktualisiert sich dann selbst.';
|
||||
return;
|
||||
}
|
||||
hinweis.textContent = '';
|
||||
const leer = document.createElement('option');
|
||||
leer.value = ''; leer.textContent = OPTIONAL.includes(aktion) ? (OPTIONAL_TEXT[aktion] || 'Alle Instanzen') : '– bitte wählen –';
|
||||
sel.appendChild(leer);
|
||||
for (const i of p.instanzen) {
|
||||
const o = document.createElement('option');
|
||||
o.value = i.id; o.textContent = i.name;
|
||||
sel.appendChild(o);
|
||||
}
|
||||
sel.value = einstellungen.instanceId || '';
|
||||
}
|
||||
|
||||
function connectElgatoStreamDeckSocket(port, inUUID, registerEvent, info, actionInfo) {
|
||||
uuid = inUUID;
|
||||
const a = JSON.parse(actionInfo);
|
||||
aktion = a.action;
|
||||
einstellungen = (a.payload && a.payload.settings) || {};
|
||||
ws = new WebSocket('ws://127.0.0.1:' + port);
|
||||
ws.onopen = () => {
|
||||
ws.send(JSON.stringify({ event: registerEvent, uuid }));
|
||||
frageInstanzen();
|
||||
};
|
||||
ws.onmessage = (e) => {
|
||||
const m = JSON.parse(e.data);
|
||||
if (m.event === 'sendToPropertyInspector') fuelle(m.payload || {});
|
||||
};
|
||||
document.getElementById('instanz').addEventListener('change', (ev) => {
|
||||
einstellungen.instanceId = ev.target.value;
|
||||
ws.send(JSON.stringify({ event: 'setSettings', context: uuid, payload: einstellungen }));
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,65 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>AeroMC-Konto</title>
|
||||
<style>
|
||||
body { font-family: "Segoe UI", sans-serif; font-size: 13px; color: #d8d8d8; background: #2d2d2d; margin: 0; padding: 12px; }
|
||||
label { display: block; margin-bottom: 4px; color: #a8a8a8; }
|
||||
select { width: 100%; padding: 6px; background: #3d3d3d; color: #fff; border: 1px solid #555; border-radius: 4px; }
|
||||
.hint { margin-top: 10px; color: #8a8a8a; font-size: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<label for="wahl">Konto</label>
|
||||
<select id="wahl"><option value="">Lade …</option></select>
|
||||
<p class="hint" id="hinweis"></p>
|
||||
<script>
|
||||
let ws, uuid, aktion, einstellungen = {}, wiederholung = null;
|
||||
|
||||
function frage() {
|
||||
if (ws.readyState === 1) ws.send(JSON.stringify({ event: 'sendToPlugin', action: aktion, context: uuid, payload: { frage: 'liste' } }));
|
||||
}
|
||||
|
||||
function fuelle(p) {
|
||||
if (p.erreichbar) { clearInterval(wiederholung); wiederholung = null; }
|
||||
else if (!wiederholung) wiederholung = setInterval(frage, 2000);
|
||||
const sel = document.getElementById('wahl');
|
||||
const hinweis = document.getElementById('hinweis');
|
||||
sel.innerHTML = '';
|
||||
if (!p.erreichbar) {
|
||||
sel.innerHTML = '<option value="">AeroMC nicht erreichbar</option>';
|
||||
hinweis.textContent = 'AeroMC starten und unter Optionen → Allgemein „Stream Deck erlauben“ einschalten. Dieses Fenster aktualisiert sich dann selbst.';
|
||||
return;
|
||||
}
|
||||
hinweis.textContent = '';
|
||||
const leer = document.createElement('option');
|
||||
leer.value = ''; leer.textContent = '– bitte wählen –';
|
||||
sel.appendChild(leer);
|
||||
for (const k of p.konten) {
|
||||
const o = document.createElement('option');
|
||||
o.value = k.id; o.textContent = k.name + (k.aktiv ? ' (aktiv)' : '');
|
||||
sel.appendChild(o);
|
||||
}
|
||||
sel.value = einstellungen.kontoId || '';
|
||||
}
|
||||
|
||||
function connectElgatoStreamDeckSocket(port, inUUID, registerEvent, info, actionInfo) {
|
||||
uuid = inUUID;
|
||||
const a = JSON.parse(actionInfo);
|
||||
aktion = a.action;
|
||||
einstellungen = (a.payload && a.payload.settings) || {};
|
||||
ws = new WebSocket('ws://127.0.0.1:' + port);
|
||||
ws.onopen = () => { ws.send(JSON.stringify({ event: registerEvent, uuid })); frage(); };
|
||||
ws.onmessage = (e) => {
|
||||
const m = JSON.parse(e.data);
|
||||
if (m.event === 'sendToPropertyInspector') fuelle(m.payload || {});
|
||||
};
|
||||
document.getElementById('wahl').addEventListener('change', (ev) => {
|
||||
einstellungen.kontoId = ev.target.value;
|
||||
ws.send(JSON.stringify({ event: 'setSettings', context: uuid, payload: einstellungen }));
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,65 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>AeroMC-Schnellstart</title>
|
||||
<style>
|
||||
body { font-family: "Segoe UI", sans-serif; font-size: 13px; color: #d8d8d8; background: #2d2d2d; margin: 0; padding: 12px; }
|
||||
label { display: block; margin-bottom: 4px; color: #a8a8a8; }
|
||||
select { width: 100%; padding: 6px; background: #3d3d3d; color: #fff; border: 1px solid #555; border-radius: 4px; }
|
||||
.hint { margin-top: 10px; color: #8a8a8a; font-size: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<label for="wahl">Schnellstart-Platz (Strg+Zahl in AeroMC)</label>
|
||||
<select id="wahl"><option value="">Lade …</option></select>
|
||||
<p class="hint" id="hinweis"></p>
|
||||
<script>
|
||||
let ws, uuid, aktion, einstellungen = {}, wiederholung = null;
|
||||
|
||||
function frage() {
|
||||
if (ws.readyState === 1) ws.send(JSON.stringify({ event: 'sendToPlugin', action: aktion, context: uuid, payload: { frage: 'liste' } }));
|
||||
}
|
||||
|
||||
function fuelle(p) {
|
||||
if (p.erreichbar) { clearInterval(wiederholung); wiederholung = null; }
|
||||
else if (!wiederholung) wiederholung = setInterval(frage, 2000);
|
||||
const sel = document.getElementById('wahl');
|
||||
const hinweis = document.getElementById('hinweis');
|
||||
sel.innerHTML = '';
|
||||
if (!p.erreichbar) {
|
||||
sel.innerHTML = '<option value="">AeroMC nicht erreichbar</option>';
|
||||
hinweis.textContent = 'AeroMC starten und unter Optionen → Allgemein „Stream Deck erlauben“ einschalten. Dieses Fenster aktualisiert sich dann selbst.';
|
||||
return;
|
||||
}
|
||||
hinweis.textContent = '';
|
||||
const namen = Object.fromEntries(p.instanzen.map((i) => [i.id, i.name]));
|
||||
for (const n of ['1', '2', '3', '4', '5']) {
|
||||
const o = document.createElement('option');
|
||||
o.value = n;
|
||||
o.textContent = 'Strg+' + n + ' – ' + (namen[p.schnellstart[n]] || 'nicht belegt');
|
||||
sel.appendChild(o);
|
||||
}
|
||||
if (!einstellungen.slot) einstellungen.slot = '1';
|
||||
sel.value = einstellungen.slot || '';
|
||||
}
|
||||
|
||||
function connectElgatoStreamDeckSocket(port, inUUID, registerEvent, info, actionInfo) {
|
||||
uuid = inUUID;
|
||||
const a = JSON.parse(actionInfo);
|
||||
aktion = a.action;
|
||||
einstellungen = (a.payload && a.payload.settings) || {};
|
||||
ws = new WebSocket('ws://127.0.0.1:' + port);
|
||||
ws.onopen = () => { ws.send(JSON.stringify({ event: registerEvent, uuid })); frage(); };
|
||||
ws.onmessage = (e) => {
|
||||
const m = JSON.parse(e.data);
|
||||
if (m.event === 'sendToPropertyInspector') fuelle(m.payload || {});
|
||||
};
|
||||
document.getElementById('wahl').addEventListener('change', (ev) => {
|
||||
einstellungen.slot = ev.target.value;
|
||||
ws.send(JSON.stringify({ event: 'setSettings', context: uuid, payload: einstellungen }));
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,110 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>AeroMC-Server</title>
|
||||
<style>
|
||||
body { font-family: "Segoe UI", sans-serif; font-size: 13px; color: #d8d8d8; background: #2d2d2d; margin: 0; padding: 12px; }
|
||||
label { display: block; margin-bottom: 4px; color: #a8a8a8; }
|
||||
select { width: 100%; padding: 6px; background: #3d3d3d; color: #fff; border: 1px solid #555; border-radius: 4px; }
|
||||
input { width: 100%; box-sizing: border-box; padding: 6px; background: #3d3d3d; color: #fff; border: 1px solid #555; border-radius: 4px; }
|
||||
.hint { margin-top: 10px; color: #8a8a8a; font-size: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<label for="instanz">Instanz</label>
|
||||
<select id="instanz"><option value="">Lade …</option></select>
|
||||
<label for="server" style="margin-top:12px">Server dieser Instanz</label>
|
||||
<select id="server"><option value="">– Instanz wählen –</option></select>
|
||||
<label for="adresse" style="margin-top:12px">oder Adresse eingeben</label>
|
||||
<input id="adresse" type="text" placeholder="z. B. mc.example.net:25565" />
|
||||
<p class="hint" id="hinweis"></p>
|
||||
<script>
|
||||
// bei diesen Tasten darf die Instanz leer bleiben (= alle Instanzen)
|
||||
const OPTIONAL = [];
|
||||
let ws, uuid, aktion, einstellungen = {}, wiederholung = null;
|
||||
|
||||
function frageInstanzen() {
|
||||
if (ws.readyState !== 1) return;
|
||||
const id = einstellungen.instanceId;
|
||||
ws.send(JSON.stringify({ event: 'sendToPlugin', action: aktion, context: uuid, payload: id ? { frage: 'server', id } : { frage: 'instanzen' } }));
|
||||
}
|
||||
|
||||
function speichere() {
|
||||
ws.send(JSON.stringify({ event: 'setSettings', context: uuid, payload: einstellungen }));
|
||||
}
|
||||
|
||||
function fuelleServer(liste) {
|
||||
const sel = document.getElementById('server');
|
||||
sel.innerHTML = '';
|
||||
const leer = document.createElement('option');
|
||||
leer.value = ''; leer.textContent = liste.length ? '– bitte wählen –' : 'Keine Server in dieser Instanz';
|
||||
sel.appendChild(leer);
|
||||
for (const x of liste) {
|
||||
const o = document.createElement('option');
|
||||
o.value = x.ip; o.textContent = (x.name ? x.name + ' – ' : '') + x.ip; o.dataset.name = x.name || '';
|
||||
sel.appendChild(o);
|
||||
}
|
||||
sel.value = liste.some((x) => x.ip === einstellungen.server) ? einstellungen.server : '';
|
||||
}
|
||||
|
||||
function fuelle(p) {
|
||||
// solange AeroMC nicht erreichbar ist, alle 2 s neu fragen - ein Neuauswählen der Taste ist dann nicht nötig
|
||||
if (p.erreichbar) { clearInterval(wiederholung); wiederholung = null; }
|
||||
else if (!wiederholung) wiederholung = setInterval(frageInstanzen, 2000);
|
||||
const sel = document.getElementById('instanz');
|
||||
const hinweis = document.getElementById('hinweis');
|
||||
sel.innerHTML = '';
|
||||
if (!p.erreichbar) {
|
||||
sel.innerHTML = '<option value="">AeroMC nicht erreichbar</option>';
|
||||
hinweis.textContent = 'AeroMC starten und unter Optionen → Allgemein „Stream Deck erlauben“ einschalten. Dieses Fenster aktualisiert sich dann selbst.';
|
||||
return;
|
||||
}
|
||||
hinweis.textContent = '';
|
||||
const leer = document.createElement('option');
|
||||
leer.value = ''; leer.textContent = OPTIONAL.includes(aktion) ? 'Alle Instanzen' : '– bitte wählen –';
|
||||
sel.appendChild(leer);
|
||||
for (const i of p.instanzen) {
|
||||
const o = document.createElement('option');
|
||||
o.value = i.id; o.textContent = i.name;
|
||||
sel.appendChild(o);
|
||||
}
|
||||
sel.value = einstellungen.instanceId || '';
|
||||
if (p.server) fuelleServer(p.server);
|
||||
}
|
||||
|
||||
function connectElgatoStreamDeckSocket(port, inUUID, registerEvent, info, actionInfo) {
|
||||
uuid = inUUID;
|
||||
const a = JSON.parse(actionInfo);
|
||||
aktion = a.action;
|
||||
einstellungen = (a.payload && a.payload.settings) || {};
|
||||
ws = new WebSocket('ws://127.0.0.1:' + port);
|
||||
ws.onopen = () => {
|
||||
ws.send(JSON.stringify({ event: registerEvent, uuid }));
|
||||
frageInstanzen();
|
||||
};
|
||||
ws.onmessage = (e) => {
|
||||
const m = JSON.parse(e.data);
|
||||
if (m.event === 'sendToPropertyInspector') fuelle(m.payload || {});
|
||||
};
|
||||
document.getElementById('adresse').value = einstellungen.adresse || '';
|
||||
document.getElementById('instanz').addEventListener('change', (ev) => {
|
||||
einstellungen.instanceId = ev.target.value;
|
||||
einstellungen.server = ''; einstellungen.serverName = '';
|
||||
speichere();
|
||||
fuelleServer([]);
|
||||
frageInstanzen();
|
||||
});
|
||||
document.getElementById('server').addEventListener('change', (ev) => {
|
||||
einstellungen.server = ev.target.value;
|
||||
einstellungen.serverName = ev.target.selectedOptions[0] ? ev.target.selectedOptions[0].dataset.name || '' : '';
|
||||
speichere();
|
||||
});
|
||||
document.getElementById('adresse').addEventListener('input', (ev) => {
|
||||
einstellungen.adresse = ev.target.value.trim();
|
||||
speichere();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||