Upload via GUI (90 Dateien)
This commit is contained in:
+109
@@ -0,0 +1,109 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
Alle nennenswerten Änderungen an AeroMC werden hier festgehalten.
|
||||||
|
|
||||||
|
## [1.1.4] - 2026-10-02
|
||||||
|
|
||||||
|
### Neu
|
||||||
|
- Neuer Installer im modernen Look: Schritt-für-Schritt-Assistent mit Lizenz, Ordnerwahl und Verknüpfungen, eigene Deinstallation (Instanzen und Welten bleiben auf Wunsch erhalten) – eine vorhandene Installation wird automatisch übernommen
|
||||||
|
- Optionaler Import im Installer: erkennt Minecraft Launcher, CurseForge, MultiMC, Prism Launcher, PolyMC und Technic und bietet deren Instanzen beim ersten Start zum Übernehmen an
|
||||||
|
- Updates aus dem Launcher heraus laufen jetzt ohne Rückfragen durch und starten AeroMC danach von selbst neu
|
||||||
|
- Freunde: „Beitreten“ startet eine passende Instanz und verbindet direkt mit dem Server, auf dem ein Freund spielt – gibt es keine passende Instanz, wird eine angelegt
|
||||||
|
- Freunde: „Einladen“ schickt einem Freund deinen aktuellen Server – er bekommt die Einladung im Launcher und als Meldung im Spiel (Strg+Alt+J kopiert dort die Adresse)
|
||||||
|
- Freunde: Der eigene Server lässt sich in den Einstellungen vor Freunden verbergen
|
||||||
|
- Clip-Modus in der Spielaufzeichnung: läuft im Hintergrund mit, Alt+F10 speichert die letzten 30, 60 oder 90 Sekunden als MP4
|
||||||
|
- Performance-Paket beim Anlegen einer Fabric-, Quilt- oder NeoForge-Instanz: Sodium, Lithium, FerriteCore, ImmediatelyFast, Entity Culling und ModernFix per Häkchen mitinstallieren
|
||||||
|
- Automatische Mod-Updates vor dem Spielstart (Optionen → Spiel → Mods)
|
||||||
|
- Spielzeit-Statistik: Gesamtspielzeit, Spielstarts, meistgespielte Instanz und Spielzeit pro Instanz auf einen Blick
|
||||||
|
|
||||||
|
### Geändert
|
||||||
|
- Die Suche nach Updates startet jetzt sofort beim Öffnen des Launchers, die Info-Seite zeigt das Ergebnis direkt an
|
||||||
|
- Direkt einem Server beitreten funktioniert jetzt auch bei Minecraft-Versionen vor 1.20
|
||||||
|
|
||||||
|
### Behoben
|
||||||
|
- Die Info-Seite zeigte „Kein Update-Check ausgeführt“, obwohl der Launcher beim Start bereits gesucht hatte
|
||||||
|
|
||||||
|
## [1.1.3] - 2026-09-29
|
||||||
|
|
||||||
|
### Neu
|
||||||
|
- Freundesystem: Freunde per Minecraft-Name hinzufügen, Anfragen annehmen/ablehnen, gesendete Anfragen zurückziehen, Spitznamen vergeben – mit 3D-Spielerkopf und Online-Status
|
||||||
|
- In-Game-Meldung mit 3D-Spielerkopf, sobald ein Freund online kommt – bei mehreren gleichzeitig laufenden Konten erscheint sie im jeweils richtigen Spielfenster
|
||||||
|
- Tastenkombination Strg+Alt+F zeigt im Spiel an, welche Freunde gerade online sind (inklusive Server)
|
||||||
|
- Zähler für offene Freundschaftsanfragen am "Freunde"-Knopf
|
||||||
|
- Freundesystem in den Einstellungen abschaltbar – man ist dann weder sichtbar noch als Freund hinzufügbar, der "Freunde"-Knopf wird ausgeblendet
|
||||||
|
- Neuer Reiter "Aufzeichnungen" im Screenshot-Fenster mit Vorschaubildern, Abspielen und Löschen
|
||||||
|
|
||||||
|
### Geändert
|
||||||
|
- Aufzeichnungen werden jetzt als MP4 gespeichert und laufen damit in jedem gängigen Player
|
||||||
|
- Der Mauszeiger ist in Aufzeichnungen nicht mehr zu sehen
|
||||||
|
|
||||||
|
### Behoben
|
||||||
|
- Aufzeichnungen ruckelten bzw. hatten eine falsche Bildrate
|
||||||
|
- Wechsel des aktiven Microsoft-Kontos wurde nicht in allen Fenstern übernommen
|
||||||
|
|
||||||
|
## [1.1.2] - 2026-09-27
|
||||||
|
|
||||||
|
### Neu
|
||||||
|
- Auflösung, Qualität und Bildrate der Spielaufzeichnung einstellbar (Nativ/1080p/720p/480p, Niedrig/Mittel/Hoch, 30/60 fps) – inklusive Anzeige der tatsächlich aufgenommenen Pixelgröße
|
||||||
|
- Lautstärke jeder einzelnen Tonspur der Aufzeichnung (Systemton, Nur Minecraft, jedes Mikrofon einzeln) regelbar, auch während eine Aufnahme bereits läuft
|
||||||
|
- Video- und Audioeinstellungen der Spielaufzeichnung bleiben jetzt über einen Neustart des Launchers hinweg erhalten
|
||||||
|
|
||||||
|
### Geändert
|
||||||
|
- Anzeige "Aufnahme läuft" samt Timer sitzt jetzt über dem Start/Stopp-Knopf statt auf dem Videobild
|
||||||
|
- Mikrofonnamen in der Audioquellen-Auswahl zeigen keine technische USB-Kennung mehr (z. B. "(9886:0038)")
|
||||||
|
|
||||||
|
### Behoben
|
||||||
|
- Servername in der Spielaufzeichnung fehlte, wenn man sich schon vor dem Öffnen des Aufzeichnungsfensters mit dem Server verbunden hatte – wird jetzt zentral erkannt und trägt sich auch nachträglich in den Titel ein
|
||||||
|
- Vorschaubild der Aufzeichnung blieb beim ersten Öffnen manchmal schwarz – wird jetzt automatisch erkannt und neu erfasst
|
||||||
|
|
||||||
|
## [1.0.9] - 2026-08-09
|
||||||
|
|
||||||
|
### Behoben
|
||||||
|
- Update-Installation brach mit "EPERM: operation not permitted, lstat …" ab, wenn vom vorherigen Versuch noch eine gesperrte Installer-Datei im Temp-Ordner lag (z. B. durch einen Virenscanner) – der Download weicht jetzt automatisch auf einen neuen Dateinamen aus
|
||||||
|
- Autostart mit Windows öffnete im Entwicklungsmodus ein leeres Electron-Fenster statt AeroMC (gleiche Ursache wie zuvor bei den Desktop-Verknüpfungen), zusätzlich zeigte der Autostart-Eintrag bei der Portable-Fassung potenziell auf einen flüchtigen Temp-Pfad
|
||||||
|
- Wiederherstellen aus einem ungültigen oder beschädigten Backup-Archiv löschte die aktuellen, unveränderten Instanzen ersatzlos, obwohl der Vorgang mit einer Fehlermeldung abgebrochen wurde – der Rollback greift jetzt nur noch, wenn am Originalbestand überhaupt schon etwas verändert wurde
|
||||||
|
|
||||||
|
## [1.0.8] - 2026-08-08
|
||||||
|
|
||||||
|
### Neu
|
||||||
|
- Datenpakete lassen sich jetzt wie Mods, Ressourcenpakete und Shader direkt aus dem Modrinth-Katalog durchsuchen, installieren, aktivieren/deaktivieren und aktualisieren
|
||||||
|
- Modpacks lassen sich jetzt auch aus dem CurseForge-Katalog durchsuchen und mit einem Klick installieren (bisher nur Modrinth und Technic; CurseForge ging nur per manuellem ZIP-Import) – braucht wie die bestehende CurseForge-Suche einen eigenen API-Key
|
||||||
|
- OptiFine als eigenständige Loader-Option: Installer-Datei auswählen, AeroMC bereitet die passende Minecraft-Version vor und startet den offiziellen OptiFine-Installer sichtbar; nach Abschluss wird die neue Version automatisch übernommen
|
||||||
|
- Von Hand hinzugefügte OptiFine-Dateien im Mods-Dialog werden jetzt erkannt und eigens gekennzeichnet (funktioniert bei jedem Loader, u. a. für Fabric + OptiFabric)
|
||||||
|
- Lizenztext direkt in den Einstellungen unter Info einsehbar
|
||||||
|
|
||||||
|
### Geändert
|
||||||
|
- Lizenzangabe von MIT auf die tatsächlich geltenden Nutzungsbedingungen umgestellt (nur privater, nicht-kommerzieller Gebrauch – siehe LICENSE)
|
||||||
|
|
||||||
|
## [1.0.7] - 2026-08-06
|
||||||
|
|
||||||
|
### Neu
|
||||||
|
- Desktop-Verknüpfung pro Server: startet die Instanz und verbindet direkt mit dem hinterlegten Server (Quick Play, ab MC 1.20)
|
||||||
|
- Name der Desktop-Verknüpfung (Instanz oder Server) lässt sich beim Erstellen frei selbst vergeben
|
||||||
|
|
||||||
|
### Behoben
|
||||||
|
- Desktop-Verknüpfungen zeigten im Entwicklungsmodus einen leeren Electron-Startbildschirm statt AeroMC zu öffnen
|
||||||
|
- Desktop-Verknüpfungen zeigten im Entwicklungsmodus das Electron-Symbol statt des AeroMC-Logos
|
||||||
|
- Desktop-Verknüpfungen (Instanz und Server) starteten bei kaltem Start unzuverlässig gar keine Instanz – der Startwunsch traf manchmal ein, bevor die Oberfläche bereit war, ihn zu empfangen, und ging dann verloren
|
||||||
|
|
||||||
|
## [1.0.6] - 2026-08-05
|
||||||
|
|
||||||
|
### Neu
|
||||||
|
- Technic-Modpacks direkt durchsuchen und installieren (Katalog), oder eine vorhandene Technic-Installation importieren
|
||||||
|
- Mods lassen sich jetzt auch als eigene Datei hinzufügen, nicht nur über die Suche
|
||||||
|
- Dateien (Mods, Modpacks, Instanzen) per Drag & Drop aufs Fenster ziehen
|
||||||
|
- Tastenkürzel: Strg+F (Suche), Strg+N (neue Instanz), Entf (Instanz löschen)
|
||||||
|
- Windows-Benachrichtigung, wenn ein großer Download oder ein Backup im Hintergrund fertig wird
|
||||||
|
- Serverliste zeigt jetzt Live-Status (Spieleranzahl, MOTD, Version)
|
||||||
|
- Ein-Klick-Knopf für empfohlene GC-Flags bei den JVM-Argumenten
|
||||||
|
- Einzelne Welten lassen sich separat schnellsichern
|
||||||
|
- Warnung, wenn ein Mod entfernt wird, den ein anderer installierter Mod noch braucht
|
||||||
|
- Skin-Hintergrund kann automatisch zwischen mehreren angemeldeten Konten durchwechseln
|
||||||
|
- Optionaler Autostart mit Windows (im Infobereich statt als Fenster)
|
||||||
|
|
||||||
|
### Geändert
|
||||||
|
- Update-Suche läuft automatisch beim Start, Anzeige wird grün wenn alles aktuell ist
|
||||||
|
|
||||||
|
### Behoben
|
||||||
|
- Bedrock-Start repariert (ging nach einem Windows-Update nicht mehr) und startete zwischenzeitlich doppelt
|
||||||
|
- Absturz beim Schließen des Spiels durch hängende Mod-Threads (z. B. JourneyMap-Webmap) behoben
|
||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "aeromc",
|
"name": "aeromc",
|
||||||
"version": "1.1.3",
|
"version": "1.1.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "aeromc",
|
"name": "aeromc",
|
||||||
"version": "1.1.3",
|
"version": "1.1.4",
|
||||||
"license": "SEE LICENSE IN LICENSE",
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@xhayper/discord-rpc": "^1.3.4",
|
"@xhayper/discord-rpc": "^1.3.4",
|
||||||
|
|||||||
+3
-7
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "aeromc",
|
"name": "aeromc",
|
||||||
"productName": "AeroMC",
|
"productName": "AeroMC",
|
||||||
"version": "1.1.3",
|
"version": "1.1.4",
|
||||||
"description": "AeroMC – Instanz-basierter Minecraft-Launcher (à la MultiMC/Prism)",
|
"description": "AeroMC – Instanz-basierter Minecraft-Launcher (à la MultiMC/Prism)",
|
||||||
"homepage": "https://m-viper.de",
|
"homepage": "https://m-viper.de",
|
||||||
"launcherWebsite": "https://aeromc.viper.ipv64.net",
|
"launcherWebsite": "https://aeromc.viper.ipv64.net",
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"start": "electron .",
|
"start": "electron .",
|
||||||
"prebuild": "node scripts/write-build-config.js",
|
"prebuild": "node scripts/write-build-config.js",
|
||||||
"prebuild:portable": "node scripts/write-build-config.js",
|
"prebuild:portable": "node scripts/write-build-config.js",
|
||||||
"build": "electron-builder --win nsis --x64 && node -e \"const fs=require('fs');const path=require('path');['dist/builder-debug.yml','dist/builder-effective-config.yaml','dist/win-unpacked'].forEach((target)=>fs.rmSync(target,{recursive:true,force:true}));if(fs.existsSync('dist')){const source=path.join('dist','AeroMC.exe');const target=path.join('dist','AeroMC-Setup.exe');if(fs.existsSync(target))fs.rmSync(target,{force:true});if(fs.existsSync(source))fs.renameSync(source,target);for(const name of fs.readdirSync('dist')){if((name.endsWith('.exe')&&name!=='AeroMC-Setup.exe'&&name!=='AeroMC-Portable.exe')||name.endsWith('.blockmap')){fs.rmSync(path.join('dist',name),{force:true})}}}\"",
|
"build": "electron-builder --win dir && node scripts/build-setup.js",
|
||||||
"build:portable": "electron-builder --win portable --x64 && node -e \"const fs=require('fs');const path=require('path');['dist/builder-debug.yml','dist/builder-effective-config.yaml','dist/win-unpacked'].forEach((target)=>fs.rmSync(target,{recursive:true,force:true}));if(fs.existsSync('dist')){const source=path.join('dist','AeroMC.exe');const target=path.join('dist','AeroMC-Portable.exe');if(fs.existsSync(target))fs.rmSync(target,{force:true});if(fs.existsSync(source))fs.renameSync(source,target);for(const name of fs.readdirSync('dist')){if((name.endsWith('.exe')&&name!=='AeroMC-Setup.exe'&&name!=='AeroMC-Portable.exe')||name.endsWith('.blockmap')){fs.rmSync(path.join('dist',name),{force:true})}}}\""
|
"build:portable": "electron-builder --win portable --x64 && node -e \"const fs=require('fs');const path=require('path');['dist/builder-debug.yml','dist/builder-effective-config.yaml','dist/win-unpacked'].forEach((target)=>fs.rmSync(target,{recursive:true,force:true}));if(fs.existsSync('dist')){const source=path.join('dist','AeroMC.exe');const target=path.join('dist','AeroMC-Portable.exe');if(fs.existsSync(target))fs.rmSync(target,{force:true});if(fs.existsSync(source))fs.renameSync(source,target);for(const name of fs.readdirSync('dist')){if((name.endsWith('.exe')&&name!=='AeroMC-Setup.exe'&&name!=='AeroMC-Portable.exe')||name.endsWith('.blockmap')){fs.rmSync(path.join('dist',name),{force:true})}}}\""
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
@@ -32,12 +32,8 @@
|
|||||||
"node_modules/ffmpeg-static/**/*"
|
"node_modules/ffmpeg-static/**/*"
|
||||||
],
|
],
|
||||||
"win": {
|
"win": {
|
||||||
"target": "nsis",
|
"target": "dir",
|
||||||
"icon": "src/assets/icon.ico"
|
"icon": "src/assets/icon.ico"
|
||||||
},
|
|
||||||
"nsis": {
|
|
||||||
"oneClick": false,
|
|
||||||
"allowToChangeInstallationDirectory": true
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
+85
-70
@@ -1,70 +1,85 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* friends.js – Anbindung an das AeroMC-Freundesystem (WordPress-Plugin)
|
* friends.js – Anbindung an das AeroMC-Freundesystem (WordPress-Plugin)
|
||||||
* -----------------------------------------------------------------------------
|
* -----------------------------------------------------------------------------
|
||||||
* Schlanke Fetch-Wrapper gegen die REST-Routen des "AeroMC Freunde"-Plugins
|
* Schlanke Fetch-Wrapper gegen die REST-Routen des "AeroMC Freunde"-Plugins
|
||||||
* (siehe wordpress-plugin/aeromc-friends/). Keine eigene Anmeldung - jeder
|
* (siehe wordpress-plugin/aeromc-friends/). Keine eigene Anmeldung - jeder
|
||||||
* Aufruf trägt einfach die Minecraft-UUID des aktiven Kontos.
|
* Aufruf trägt einfach die Minecraft-UUID des aktiven Kontos.
|
||||||
*
|
*
|
||||||
* Genau wie bei discordpresence.js: jeder Fehler (Plugin nicht installiert,
|
* Genau wie bei discordpresence.js: jeder Fehler (Plugin nicht installiert,
|
||||||
* kein Internet, Seite down) wird abgefangen und nach oben als { ok:false,
|
* kein Internet, Seite down) wird abgefangen und nach oben als { ok:false,
|
||||||
* message } gereicht statt den Launcher zu stören - ein soziales Extra darf
|
* message } gereicht statt den Launcher zu stören - ein soziales Extra darf
|
||||||
* nie den Spielstart oder die Bedienung blockieren.
|
* nie den Spielstart oder die Bedienung blockieren.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const BASIS_URL = 'https://viper-network.de/wp-json/aeromc/v1';
|
const BASIS_URL = 'https://viper-network.de/wp-json/aeromc/v1';
|
||||||
|
|
||||||
async function anfrage(pfad, { method = 'GET', body } = {}) {
|
async function anfrage(pfad, { method = 'GET', body } = {}) {
|
||||||
try {
|
try {
|
||||||
const url = method === 'GET' && body
|
const url = method === 'GET' && body
|
||||||
? `${BASIS_URL}${pfad}?${new URLSearchParams(body).toString()}`
|
? `${BASIS_URL}${pfad}?${new URLSearchParams(body).toString()}`
|
||||||
: `${BASIS_URL}${pfad}`;
|
: `${BASIS_URL}${pfad}`;
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
method,
|
method,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: method === 'GET' ? undefined : JSON.stringify(body || {}),
|
body: method === 'GET' ? undefined : JSON.stringify(body || {}),
|
||||||
});
|
});
|
||||||
const daten = await res.json().catch(() => null);
|
const daten = await res.json().catch(() => null);
|
||||||
if (!res.ok) return { ok: false, message: (daten && daten.message) || `Fehler ${res.status}` };
|
if (!res.ok) return { ok: false, message: (daten && daten.message) || `Fehler ${res.status}` };
|
||||||
return { ok: true, data: daten };
|
return { ok: true, data: daten };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return { ok: false, message: 'Freundesystem nicht erreichbar: ' + err.message };
|
return { ok: false, message: 'Freundesystem nicht erreichbar: ' + err.message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function heartbeat({ uuid, username, instance, server }) {
|
function heartbeat({ uuid, username, instance, server, version }) {
|
||||||
return anfrage('/presence', { method: 'POST', body: { uuid, username, instance, server } });
|
return anfrage('/presence', { method: 'POST', body: { uuid, username, instance, server, version } });
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFriends(uuid) {
|
function getFriends(uuid) {
|
||||||
return anfrage('/friends', { method: 'GET', body: { uuid } });
|
return anfrage('/friends', { method: 'GET', body: { uuid } });
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRequests(uuid) {
|
function getRequests(uuid) {
|
||||||
return anfrage('/friends/requests', { method: 'GET', body: { uuid } });
|
return anfrage('/friends/requests', { method: 'GET', body: { uuid } });
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSentRequests(uuid) {
|
function getSentRequests(uuid) {
|
||||||
return anfrage('/friends/sent', { method: 'GET', body: { uuid } });
|
return anfrage('/friends/sent', { method: 'GET', body: { uuid } });
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendRequest(uuid, username, targetUsername) {
|
function sendRequest(uuid, username, targetUsername) {
|
||||||
return anfrage('/friends/request', { method: 'POST', body: { uuid, username, targetUsername } });
|
return anfrage('/friends/request', { method: 'POST', body: { uuid, username, targetUsername } });
|
||||||
}
|
}
|
||||||
|
|
||||||
function respond(uuid, otherUuid, accept) {
|
function respond(uuid, otherUuid, accept) {
|
||||||
return anfrage('/friends/respond', { method: 'POST', body: { uuid, otherUuid, accept } });
|
return anfrage('/friends/respond', { method: 'POST', body: { uuid, otherUuid, accept } });
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelRequest(uuid, otherUuid) {
|
function cancelRequest(uuid, otherUuid) {
|
||||||
return anfrage('/friends/cancel', { method: 'POST', body: { uuid, otherUuid } });
|
return anfrage('/friends/cancel', { method: 'POST', body: { uuid, otherUuid } });
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeFriend(uuid, otherUuid) {
|
function removeFriend(uuid, otherUuid) {
|
||||||
return anfrage('/friends/remove', { method: 'POST', body: { uuid, otherUuid } });
|
return anfrage('/friends/remove', { method: 'POST', body: { uuid, otherUuid } });
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
// Einladung "Komm auf meinen Server" (Adresse + Minecraft-Version des Einladenden)
|
||||||
heartbeat, getFriends, getRequests, getSentRequests, sendRequest, respond, cancelRequest, removeFriend,
|
function sendInvite(uuid, targetUuid, server, version) {
|
||||||
};
|
return anfrage('/friends/invite', { method: 'POST', body: { uuid, targetUuid, server, version } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// an mich gerichtete, noch gültige Einladungen
|
||||||
|
function getInvites(uuid) {
|
||||||
|
return anfrage('/friends/invites', { method: 'GET', body: { uuid } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismissInvite(uuid, id) {
|
||||||
|
return anfrage('/friends/invites/dismiss', { method: 'POST', body: { uuid, id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
heartbeat, getFriends, getRequests, getSentRequests, sendRequest, respond, cancelRequest, removeFriend,
|
||||||
|
sendInvite, getInvites, dismissInvite,
|
||||||
|
};
|
||||||
|
|||||||
+1531
-1473
File diff suppressed because it is too large
Load Diff
+17
-2
@@ -357,6 +357,13 @@ function buildArgs(vj, ctx) {
|
|||||||
* auth: null (kein Login) ODER { name, uuid, accessToken, userType }
|
* auth: null (kein Login) ODER { name, uuid, accessToken, userType }
|
||||||
* onProgress: (evt) => void
|
* onProgress: (evt) => void
|
||||||
*/
|
*/
|
||||||
|
// true für Release-Versionen vor 1.20 (kein Quick Play). Snapshots und
|
||||||
|
// Unbekanntes gelten als neu – die meisten heutigen Snapshots sind > 1.20.
|
||||||
|
function vorQuickPlay(versionId) {
|
||||||
|
const m = /^1\.(\d+)(?:\.\d+)?(?:-(?:pre|rc)\d+)?$/.exec(String(versionId || '').trim());
|
||||||
|
return !!m && Number(m[1]) < 20;
|
||||||
|
}
|
||||||
|
|
||||||
async function prepareAndLaunch(opts, onProgress) {
|
async function prepareAndLaunch(opts, onProgress) {
|
||||||
const emit = (phase, extra) => onProgress && onProgress(Object.assign({ phase }, extra));
|
const emit = (phase, extra) => onProgress && onProgress(Object.assign({ phase }, extra));
|
||||||
|
|
||||||
@@ -526,9 +533,17 @@ async function prepareAndLaunch(opts, onProgress) {
|
|||||||
// Fenstergröße bzw. maximiert starten (Minecraft-Argumente)
|
// Fenstergröße bzw. maximiert starten (Minecraft-Argumente)
|
||||||
if (opts.mcMaximized) args.push('--fullscreen');
|
if (opts.mcMaximized) args.push('--fullscreen');
|
||||||
else if (opts.mcWidth && opts.mcHeight) args.push('--width', String(opts.mcWidth), '--height', String(opts.mcHeight));
|
else if (opts.mcWidth && opts.mcHeight) args.push('--width', String(opts.mcWidth), '--height', String(opts.mcHeight));
|
||||||
// Ab 1.20: direkt einem Server beitreten (Quick Play)
|
// Direkt einem Server beitreten: ab 1.20 per Quick Play, davor über die
|
||||||
|
// älteren Startparameter --server/--port (z. B. für "Beitreten" bei Freunden)
|
||||||
if (opts.quickPlayServer) {
|
if (opts.quickPlayServer) {
|
||||||
args.push('--quickPlayMultiplayer', String(opts.quickPlayServer));
|
const adresse = String(opts.quickPlayServer).trim();
|
||||||
|
if (vorQuickPlay(opts.versionId)) {
|
||||||
|
const m = /^(.*?)(?::(\d+))?$/.exec(adresse);
|
||||||
|
args.push('--server', m[1]);
|
||||||
|
if (m[2]) args.push('--port', m[2]);
|
||||||
|
} else {
|
||||||
|
args.push('--quickPlayMultiplayer', adresse);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureDir(opts.gameDir);
|
ensureDir(opts.gameDir);
|
||||||
|
|||||||
+3351
-3015
File diff suppressed because it is too large
Load Diff
+352
-327
@@ -1,327 +1,352 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* preload.js – stellt dem Renderer eine schmale, sichere API bereit.
|
* preload.js – stellt dem Renderer eine schmale, sichere API bereit.
|
||||||
* contextIsolation ist aktiv; der Renderer hat KEINEN direkten Node-Zugriff.
|
* contextIsolation ist aktiv; der Renderer hat KEINEN direkten Node-Zugriff.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { contextBridge, ipcRenderer } = require('electron');
|
const { contextBridge, ipcRenderer } = require('electron');
|
||||||
|
|
||||||
// Debug-Hilfe: JS-Fehler im Renderer landen im Terminal des Hauptprozesses,
|
// Debug-Hilfe: JS-Fehler im Renderer landen im Terminal des Hauptprozesses,
|
||||||
// damit man sie auch ohne offene DevTools sieht
|
// damit man sie auch ohne offene DevTools sieht
|
||||||
window.addEventListener('error', (e) => {
|
window.addEventListener('error', (e) => {
|
||||||
ipcRenderer.send('debug:rendererError', (e.error && e.error.stack) || e.message);
|
ipcRenderer.send('debug:rendererError', (e.error && e.error.stack) || e.message);
|
||||||
});
|
});
|
||||||
window.addEventListener('unhandledrejection', (e) => {
|
window.addEventListener('unhandledrejection', (e) => {
|
||||||
ipcRenderer.send('debug:rendererError', 'unhandledrejection: ' + ((e.reason && e.reason.stack) || e.reason));
|
ipcRenderer.send('debug:rendererError', 'unhandledrejection: ' + ((e.reason && e.reason.stack) || e.reason));
|
||||||
});
|
});
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('api', {
|
contextBridge.exposeInMainWorld('api', {
|
||||||
// Einstellungen
|
// Einstellungen
|
||||||
getSettings: () => ipcRenderer.invoke('settings:get'),
|
getSettings: () => ipcRenderer.invoke('settings:get'),
|
||||||
saveSettings: (patch) => ipcRenderer.invoke('settings:save', patch),
|
saveSettings: (patch) => ipcRenderer.invoke('settings:save', patch),
|
||||||
getInstancesDir: () => ipcRenderer.invoke('settings:instancesDir'),
|
getInstancesDir: () => ipcRenderer.invoke('settings:instancesDir'),
|
||||||
detectJava: (javaPath) => ipcRenderer.invoke('java:detect', javaPath),
|
detectJava: (javaPath) => ipcRenderer.invoke('java:detect', javaPath),
|
||||||
scanJava: () => ipcRenderer.invoke('java:scan'),
|
scanJava: () => ipcRenderer.invoke('java:scan'),
|
||||||
listJava: () => ipcRenderer.invoke('java:list'),
|
listJava: () => ipcRenderer.invoke('java:list'),
|
||||||
|
|
||||||
// Minecraft-Versionen (Mojang)
|
// Minecraft-Versionen (Mojang)
|
||||||
mcVersions: () => ipcRenderer.invoke('mc:versions'),
|
mcVersions: () => ipcRenderer.invoke('mc:versions'),
|
||||||
loaderVersions: (loader, mcVersion) => ipcRenderer.invoke('loaders:versions', loader, mcVersion),
|
loaderVersions: (loader, mcVersion) => ipcRenderer.invoke('loaders:versions', loader, mcVersion),
|
||||||
optifineFind: (mcVersion) => ipcRenderer.invoke('optifine:find', mcVersion),
|
optifineFind: (mcVersion) => ipcRenderer.invoke('optifine:find', mcVersion),
|
||||||
optifineSetup: (id) => ipcRenderer.invoke('optifine:setup', id),
|
optifineSetup: (id) => ipcRenderer.invoke('optifine:setup', id),
|
||||||
optifineConfirm: (id) => ipcRenderer.invoke('optifine:confirm', id),
|
optifineConfirm: (id) => ipcRenderer.invoke('optifine:confirm', id),
|
||||||
|
|
||||||
// Mods (Modrinth)
|
// Mods (Modrinth)
|
||||||
modSearch: (opts) => ipcRenderer.invoke('mods:search', opts),
|
modSearch: (opts) => ipcRenderer.invoke('mods:search', opts),
|
||||||
curseforgeTestKey: (apiKey) => ipcRenderer.invoke('curseforge:testKey', apiKey),
|
curseforgeTestKey: (apiKey) => ipcRenderer.invoke('curseforge:testKey', apiKey),
|
||||||
// art: 'mod' | 'resourcepack' | 'shader'
|
// art: 'mod' | 'resourcepack' | 'shader'
|
||||||
modList: (id, art) => ipcRenderer.invoke('mods:list', id, art),
|
modList: (id, art) => ipcRenderer.invoke('mods:list', id, art),
|
||||||
modInstall: (id, project, art) => ipcRenderer.invoke('mods:install', id, project, art),
|
modInstall: (id, project, art) => ipcRenderer.invoke('mods:install', id, project, art),
|
||||||
modRemove: (id, filename, art) => ipcRenderer.invoke('mods:remove', id, filename, art),
|
modRemove: (id, filename, art) => ipcRenderer.invoke('mods:remove', id, filename, art),
|
||||||
modDependents: (id, filename, art) => ipcRenderer.invoke('mods:dependents', id, filename, art),
|
modDependents: (id, filename, art) => ipcRenderer.invoke('mods:dependents', id, filename, art),
|
||||||
modAddFile: (id, art) => ipcRenderer.invoke('mods:addFile', id, art),
|
modAddFile: (id, art) => ipcRenderer.invoke('mods:addFile', id, art),
|
||||||
modSetEnabled: (id, filename, enabled, art) => ipcRenderer.invoke('mods:setEnabled', id, filename, enabled, art),
|
modSetEnabled: (id, filename, enabled, art) => ipcRenderer.invoke('mods:setEnabled', id, filename, enabled, art),
|
||||||
systemMemory: () => ipcRenderer.invoke('system:memory'),
|
systemMemory: () => ipcRenderer.invoke('system:memory'),
|
||||||
checkMemory: (minMb, maxMb) => ipcRenderer.invoke('system:checkMemory', minMb, maxMb),
|
checkMemory: (minMb, maxMb) => ipcRenderer.invoke('system:checkMemory', minMb, maxMb),
|
||||||
crashLatest: (id, seit) => ipcRenderer.invoke('crash:latest', id, seit),
|
crashLatest: (id, seit) => ipcRenderer.invoke('crash:latest', id, seit),
|
||||||
onAutoBackup: (cb) => {
|
onAutoBackup: (cb) => {
|
||||||
const l = (_e, d) => cb(d);
|
const l = (_e, d) => cb(d);
|
||||||
ipcRenderer.on('backup:auto', l);
|
ipcRenderer.on('backup:auto', l);
|
||||||
return () => ipcRenderer.removeListener('backup:auto', l);
|
return () => ipcRenderer.removeListener('backup:auto', l);
|
||||||
},
|
},
|
||||||
crashOpenFolder: (id) => ipcRenderer.invoke('crash:openFolder', id),
|
crashOpenFolder: (id) => ipcRenderer.invoke('crash:openFolder', id),
|
||||||
serversList: (id) => ipcRenderer.invoke('servers:list', id),
|
serversList: (id) => ipcRenderer.invoke('servers:list', id),
|
||||||
serversAdd: (id, eintrag) => ipcRenderer.invoke('servers:add', id, eintrag),
|
serversAdd: (id, eintrag) => ipcRenderer.invoke('servers:add', id, eintrag),
|
||||||
serversRemove: (id, ip) => ipcRenderer.invoke('servers:remove', id, ip),
|
serversRemove: (id, ip) => ipcRenderer.invoke('servers:remove', id, ip),
|
||||||
serversRename: (id, ip, neuerName) => ipcRenderer.invoke('servers:rename', id, ip, neuerName),
|
serversRename: (id, ip, neuerName) => ipcRenderer.invoke('servers:rename', id, ip, neuerName),
|
||||||
serversCopyTo: (zielIds, eintrag) => ipcRenderer.invoke('servers:copyTo', zielIds, eintrag),
|
serversCopyTo: (zielIds, eintrag) => ipcRenderer.invoke('servers:copyTo', zielIds, eintrag),
|
||||||
serversPing: (ip) => ipcRenderer.invoke('servers:ping', ip),
|
serversPing: (ip) => ipcRenderer.invoke('servers:ping', ip),
|
||||||
logsList: (id) => ipcRenderer.invoke('logs:list', id),
|
logsList: (id) => ipcRenderer.invoke('logs:list', id),
|
||||||
logsRead: (id, name) => ipcRenderer.invoke('logs:read', id, name),
|
logsRead: (id, name) => ipcRenderer.invoke('logs:read', id, name),
|
||||||
logsOpenFolder: (id) => ipcRenderer.invoke('logs:openFolder', id),
|
logsOpenFolder: (id) => ipcRenderer.invoke('logs:openFolder', id),
|
||||||
modCheckUpdates: (id, art) => ipcRenderer.invoke('mods:checkUpdates', id, art),
|
modCheckUpdates: (id, art) => ipcRenderer.invoke('mods:checkUpdates', id, art),
|
||||||
modApplyUpdates: (id, projectIds, art) => ipcRenderer.invoke('mods:applyUpdates', id, projectIds, art),
|
modApplyUpdates: (id, projectIds, art) => ipcRenderer.invoke('mods:applyUpdates', id, projectIds, art),
|
||||||
onModUpdateProgress: (cb) => {
|
onModUpdateProgress: (cb) => {
|
||||||
const l = (_e, d) => cb(d);
|
const l = (_e, d) => cb(d);
|
||||||
ipcRenderer.on('mods:updateProgress', l);
|
ipcRenderer.on('mods:updateProgress', l);
|
||||||
return () => ipcRenderer.removeListener('mods:updateProgress', l);
|
return () => ipcRenderer.removeListener('mods:updateProgress', l);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Instanzen
|
// Instanzen
|
||||||
listInstances: () => ipcRenderer.invoke('instances:list'),
|
listInstances: () => ipcRenderer.invoke('instances:list'),
|
||||||
getInstance: (id) => ipcRenderer.invoke('instances:get', id),
|
getInstance: (id) => ipcRenderer.invoke('instances:get', id),
|
||||||
createInstance: (data) => ipcRenderer.invoke('instances:create', data),
|
createInstance: (data) => ipcRenderer.invoke('instances:create', data),
|
||||||
updateInstance: (id, patch) => ipcRenderer.invoke('instances:update', id, patch),
|
installPerformancePack: (id) => ipcRenderer.invoke('instances:installPerformancePack', id),
|
||||||
deleteInstance: (id) => ipcRenderer.invoke('instances:delete', id),
|
updateInstance: (id, patch) => ipcRenderer.invoke('instances:update', id, patch),
|
||||||
duplicateInstance: (id) => ipcRenderer.invoke('instances:duplicate', id),
|
deleteInstance: (id) => ipcRenderer.invoke('instances:delete', id),
|
||||||
openInstanceFolder: (id) => ipcRenderer.invoke('instances:openFolder', id),
|
duplicateInstance: (id) => ipcRenderer.invoke('instances:duplicate', id),
|
||||||
openGameDir: (id) => ipcRenderer.invoke('instances:openGameDir', id),
|
openInstanceFolder: (id) => ipcRenderer.invoke('instances:openFolder', id),
|
||||||
|
openGameDir: (id) => ipcRenderer.invoke('instances:openGameDir', id),
|
||||||
// Welten & Ressourcenpakete
|
|
||||||
worlds: (id) => ipcRenderer.invoke('instance:worlds', id),
|
// Welten & Ressourcenpakete
|
||||||
resourcepacks: (id) => ipcRenderer.invoke('instance:resourcepacks', id),
|
worlds: (id) => ipcRenderer.invoke('instance:worlds', id),
|
||||||
datapacks: (id) => ipcRenderer.invoke('instance:datapacks', id),
|
resourcepacks: (id) => ipcRenderer.invoke('instance:resourcepacks', id),
|
||||||
deleteContent: (id, sub, name) => ipcRenderer.invoke('instance:deleteContent', id, sub, name),
|
datapacks: (id) => ipcRenderer.invoke('instance:datapacks', id),
|
||||||
backupWorld: (id, worldName) => ipcRenderer.invoke('world:backup', id, worldName),
|
deleteContent: (id, sub, name) => ipcRenderer.invoke('instance:deleteContent', id, sub, name),
|
||||||
openSubdir: (id, sub) => ipcRenderer.invoke('instance:openSubdir', id, sub),
|
backupWorld: (id, worldName) => ipcRenderer.invoke('world:backup', id, worldName),
|
||||||
screenshots: (id) => ipcRenderer.invoke('instance:screenshots', id),
|
openSubdir: (id, sub) => ipcRenderer.invoke('instance:openSubdir', id, sub),
|
||||||
screenshotData: (id, name) => ipcRenderer.invoke('instance:screenshotData', id, name),
|
screenshots: (id) => ipcRenderer.invoke('instance:screenshots', id),
|
||||||
openScreenshot: (id, name) => ipcRenderer.invoke('instance:openScreenshot', id, name),
|
screenshotData: (id, name) => ipcRenderer.invoke('instance:screenshotData', id, name),
|
||||||
launchInstance: (id, accountId, opts) => ipcRenderer.invoke('instances:launch', id, accountId, opts),
|
openScreenshot: (id, name) => ipcRenderer.invoke('instance:openScreenshot', id, name),
|
||||||
exportInstance: (id) => ipcRenderer.invoke('instances:export', id),
|
launchInstance: (id, accountId, opts) => ipcRenderer.invoke('instances:launch', id, accountId, opts),
|
||||||
createGlobalBackup: () => ipcRenderer.invoke('backup:global'),
|
exportInstance: (id) => ipcRenderer.invoke('instances:export', id),
|
||||||
listGlobalBackups: () => ipcRenderer.invoke('backup:list'),
|
createGlobalBackup: () => ipcRenderer.invoke('backup:global'),
|
||||||
restoreGlobalBackup: (sourcePath) => ipcRenderer.invoke('backup:restore', sourcePath),
|
listGlobalBackups: () => ipcRenderer.invoke('backup:list'),
|
||||||
onBackupProgress: (cb) => {
|
restoreGlobalBackup: (sourcePath) => ipcRenderer.invoke('backup:restore', sourcePath),
|
||||||
const l = (_e, d) => cb(d);
|
onBackupProgress: (cb) => {
|
||||||
ipcRenderer.on('backup:progress', l);
|
const l = (_e, d) => cb(d);
|
||||||
return () => ipcRenderer.removeListener('backup:progress', l);
|
ipcRenderer.on('backup:progress', l);
|
||||||
},
|
return () => ipcRenderer.removeListener('backup:progress', l);
|
||||||
importInstance: () => ipcRenderer.invoke('instances:import'),
|
},
|
||||||
createShortcut: (id, customName) => ipcRenderer.invoke('instances:shortcut', id, customName),
|
importInstance: () => ipcRenderer.invoke('instances:import'),
|
||||||
serversShortcut: (id, ip, name, customName) => ipcRenderer.invoke('servers:shortcut', id, ip, name, customName),
|
createShortcut: (id, customName) => ipcRenderer.invoke('instances:shortcut', id, customName),
|
||||||
// Start über eine Desktop-Verknüpfung ("--launch=<id>", optional "--server=<ip>")
|
serversShortcut: (id, ip, name, customName) => ipcRenderer.invoke('servers:shortcut', id, ip, name, customName),
|
||||||
onLaunchRequest: (cb) => {
|
// Start über eine Desktop-Verknüpfung ("--launch=<id>", optional "--server=<ip>")
|
||||||
const l = (_e, payload) => cb(payload);
|
onLaunchRequest: (cb) => {
|
||||||
ipcRenderer.on('instance:launch-request', l);
|
const l = (_e, payload) => cb(payload);
|
||||||
return () => ipcRenderer.removeListener('instance:launch-request', l);
|
ipcRenderer.on('instance:launch-request', l);
|
||||||
},
|
return () => ipcRenderer.removeListener('instance:launch-request', l);
|
||||||
// Signalisiert dem Hauptprozess, dass onLaunchRequest jetzt registriert ist –
|
},
|
||||||
// verhindert, dass ein Start-Wunsch aus einer Verknüpfung verloren geht,
|
// Signalisiert dem Hauptprozess, dass onLaunchRequest jetzt registriert ist –
|
||||||
// weil er ankommt, bevor die Oberfläche fertig eingerichtet ist
|
// verhindert, dass ein Start-Wunsch aus einer Verknüpfung verloren geht,
|
||||||
notifyReady: () => ipcRenderer.send('renderer:ready'),
|
// weil er ankommt, bevor die Oberfläche fertig eingerichtet ist
|
||||||
importModpack: () => ipcRenderer.invoke('modpack:import'),
|
notifyReady: () => ipcRenderer.send('renderer:ready'),
|
||||||
// Datei per Drag&Drop aufs Fenster gezogen (.jar/.mrpack/.zip)
|
importModpack: () => ipcRenderer.invoke('modpack:import'),
|
||||||
filesDrop: (filePath, targetId, targetArt) => ipcRenderer.invoke('files:drop', filePath, targetId, targetArt),
|
// Datei per Drag&Drop aufs Fenster gezogen (.jar/.mrpack/.zip)
|
||||||
modpackSearch: (opts) => ipcRenderer.invoke('modpack:search', opts),
|
filesDrop: (filePath, targetId, targetArt) => ipcRenderer.invoke('files:drop', filePath, targetId, targetArt),
|
||||||
modpackInstallFromModrinth: (project) => ipcRenderer.invoke('modpack:installFromModrinth', project),
|
modpackSearch: (opts) => ipcRenderer.invoke('modpack:search', opts),
|
||||||
modpackInstallFromCurseForge: (project) => ipcRenderer.invoke('modpack:installFromCurseForge', project),
|
modpackInstallFromModrinth: (project) => ipcRenderer.invoke('modpack:installFromModrinth', project),
|
||||||
modpackCheck: (id) => ipcRenderer.invoke('modpack:check', id),
|
modpackInstallFromCurseForge: (project) => ipcRenderer.invoke('modpack:installFromCurseForge', project),
|
||||||
modpackUpdate: (id, versionId) => ipcRenderer.invoke('modpack:update', id, versionId),
|
modpackCheck: (id) => ipcRenderer.invoke('modpack:check', id),
|
||||||
onLaunchProgress: (cb) => {
|
modpackUpdate: (id, versionId) => ipcRenderer.invoke('modpack:update', id, versionId),
|
||||||
const listener = (_e, data) => cb(data);
|
onLaunchProgress: (cb) => {
|
||||||
ipcRenderer.on('launch:progress', listener);
|
const listener = (_e, data) => cb(data);
|
||||||
return () => ipcRenderer.removeListener('launch:progress', listener);
|
ipcRenderer.on('launch:progress', listener);
|
||||||
},
|
return () => ipcRenderer.removeListener('launch:progress', listener);
|
||||||
|
},
|
||||||
// eigenes Instanz-Bild
|
|
||||||
pickImage: () => ipcRenderer.invoke('dialog:pickImage'),
|
// eigenes Instanz-Bild
|
||||||
setInstanceImage: (id, sourcePath) => ipcRenderer.invoke('instances:setImage', id, sourcePath),
|
pickImage: () => ipcRenderer.invoke('dialog:pickImage'),
|
||||||
clearInstanceImage: (id) => ipcRenderer.invoke('instances:clearImage', id),
|
setInstanceImage: (id, sourcePath) => ipcRenderer.invoke('instances:setImage', id, sourcePath),
|
||||||
instanceIcon: (id) => ipcRenderer.invoke('instances:icon', id),
|
clearInstanceImage: (id) => ipcRenderer.invoke('instances:clearImage', id),
|
||||||
fileDataUri: (p) => ipcRenderer.invoke('file:dataUri', p),
|
instanceIcon: (id) => ipcRenderer.invoke('instances:icon', id),
|
||||||
|
fileDataUri: (p) => ipcRenderer.invoke('file:dataUri', p),
|
||||||
// Microsoft-Login (Phase 3) + Mehrkonten
|
|
||||||
authStatus: () => ipcRenderer.invoke('auth:status'),
|
// Microsoft-Login (Phase 3) + Mehrkonten
|
||||||
authAccounts: () => ipcRenderer.invoke('auth:accounts'),
|
authStatus: () => ipcRenderer.invoke('auth:status'),
|
||||||
authSetActive: (id) => ipcRenderer.invoke('auth:setActive', id),
|
authAccounts: () => ipcRenderer.invoke('auth:accounts'),
|
||||||
skinProfile: () => ipcRenderer.invoke('skin:profile'),
|
authSetActive: (id) => ipcRenderer.invoke('auth:setActive', id),
|
||||||
skinChoose: () => ipcRenderer.invoke('skin:choose'),
|
skinProfile: () => ipcRenderer.invoke('skin:profile'),
|
||||||
skinApply: (pfad, variante) => ipcRenderer.invoke('skin:apply', pfad, variante),
|
skinChoose: () => ipcRenderer.invoke('skin:choose'),
|
||||||
skinReset: () => ipcRenderer.invoke('skin:reset'),
|
skinApply: (pfad, variante) => ipcRenderer.invoke('skin:apply', pfad, variante),
|
||||||
authRemove: (id) => ipcRenderer.invoke('auth:remove', id),
|
skinReset: () => ipcRenderer.invoke('skin:reset'),
|
||||||
authLogin: () => ipcRenderer.invoke('auth:login'),
|
authRemove: (id) => ipcRenderer.invoke('auth:remove', id),
|
||||||
authLogout: () => ipcRenderer.invoke('auth:logout'),
|
authLogin: () => ipcRenderer.invoke('auth:login'),
|
||||||
authHealth: () => ipcRenderer.invoke('auth:health'),
|
authLogout: () => ipcRenderer.invoke('auth:logout'),
|
||||||
authRecheck: () => ipcRenderer.invoke('auth:recheck'),
|
authHealth: () => ipcRenderer.invoke('auth:health'),
|
||||||
onAuthHealth: (cb) => {
|
authRecheck: () => ipcRenderer.invoke('auth:recheck'),
|
||||||
const listener = (_e, data) => cb(data);
|
onAuthHealth: (cb) => {
|
||||||
ipcRenderer.on('auth:health', listener);
|
const listener = (_e, data) => cb(data);
|
||||||
return () => ipcRenderer.removeListener('auth:health', listener);
|
ipcRenderer.on('auth:health', listener);
|
||||||
},
|
return () => ipcRenderer.removeListener('auth:health', listener);
|
||||||
// meldet sich, wenn ein Spiel startet oder endet -> Kontenliste auffrischen
|
},
|
||||||
onAccountsChanged: (cb) => {
|
// meldet sich, wenn ein Spiel startet oder endet -> Kontenliste auffrischen
|
||||||
const listener = () => cb();
|
onAccountsChanged: (cb) => {
|
||||||
ipcRenderer.on('accounts:changed', listener);
|
const listener = () => cb();
|
||||||
return () => ipcRenderer.removeListener('accounts:changed', listener);
|
ipcRenderer.on('accounts:changed', listener);
|
||||||
},
|
return () => ipcRenderer.removeListener('accounts:changed', listener);
|
||||||
onAuthRefreshed: (cb) => {
|
},
|
||||||
const listener = () => cb();
|
onAuthRefreshed: (cb) => {
|
||||||
ipcRenderer.on('auth:refreshed', listener);
|
const listener = () => cb();
|
||||||
return () => ipcRenderer.removeListener('auth:refreshed', listener);
|
ipcRenderer.on('auth:refreshed', listener);
|
||||||
},
|
return () => ipcRenderer.removeListener('auth:refreshed', listener);
|
||||||
onAuthCode: (cb) => {
|
},
|
||||||
const listener = (_e, data) => cb(data);
|
onAuthCode: (cb) => {
|
||||||
ipcRenderer.on('auth:code', listener);
|
const listener = (_e, data) => cb(data);
|
||||||
return () => ipcRenderer.removeListener('auth:code', listener);
|
ipcRenderer.on('auth:code', listener);
|
||||||
},
|
return () => ipcRenderer.removeListener('auth:code', listener);
|
||||||
|
},
|
||||||
// Dialoge / App
|
|
||||||
// MultiMC-Import
|
// Dialoge / App
|
||||||
mmcPickFolder: () => ipcRenderer.invoke('mmc:pickFolder'),
|
// MultiMC-Import
|
||||||
mmcScan: (folder) => ipcRenderer.invoke('mmc:scan', folder),
|
mmcPickFolder: () => ipcRenderer.invoke('mmc:pickFolder'),
|
||||||
mmcImport: (entries, copyData) => ipcRenderer.invoke('mmc:import', entries, copyData),
|
mmcScan: (folder) => ipcRenderer.invoke('mmc:scan', folder),
|
||||||
onMmcProgress: (cb) => {
|
mmcImport: (entries, copyData) => ipcRenderer.invoke('mmc:import', entries, copyData),
|
||||||
const l = (_e, d) => cb(d);
|
onMmcProgress: (cb) => {
|
||||||
ipcRenderer.on('mmc:progress', l);
|
const l = (_e, d) => cb(d);
|
||||||
return () => ipcRenderer.removeListener('mmc:progress', l);
|
ipcRenderer.on('mmc:progress', l);
|
||||||
},
|
return () => ipcRenderer.removeListener('mmc:progress', l);
|
||||||
cfPickFolder: () => ipcRenderer.invoke('cf:pickFolder'),
|
},
|
||||||
cfScan: (folder) => ipcRenderer.invoke('cf:scan', folder),
|
cfPickFolder: () => ipcRenderer.invoke('cf:pickFolder'),
|
||||||
cfImport: (entries, copyData) => ipcRenderer.invoke('cf:import', entries, copyData),
|
cfScan: (folder) => ipcRenderer.invoke('cf:scan', folder),
|
||||||
onCfProgress: (cb) => {
|
cfImport: (entries, copyData) => ipcRenderer.invoke('cf:import', entries, copyData),
|
||||||
const l = (_e, d) => cb(d);
|
onCfProgress: (cb) => {
|
||||||
ipcRenderer.on('cf:progress', l);
|
const l = (_e, d) => cb(d);
|
||||||
return () => ipcRenderer.removeListener('cf:progress', l);
|
ipcRenderer.on('cf:progress', l);
|
||||||
},
|
return () => ipcRenderer.removeListener('cf:progress', l);
|
||||||
mcPickFolder: () => ipcRenderer.invoke('mc:pickFolder'),
|
},
|
||||||
mcScan: (folder) => ipcRenderer.invoke('mc:scan', folder),
|
mcPickFolder: () => ipcRenderer.invoke('mc:pickFolder'),
|
||||||
mcImport: (entries, copyData) => ipcRenderer.invoke('mc:import', entries, copyData),
|
mcScan: (folder) => ipcRenderer.invoke('mc:scan', folder),
|
||||||
onMcProgress: (cb) => {
|
mcImport: (entries, copyData) => ipcRenderer.invoke('mc:import', entries, copyData),
|
||||||
const l = (_e, d) => cb(d);
|
onMcProgress: (cb) => {
|
||||||
ipcRenderer.on('mc:progress', l);
|
const l = (_e, d) => cb(d);
|
||||||
return () => ipcRenderer.removeListener('mc:progress', l);
|
ipcRenderer.on('mc:progress', l);
|
||||||
},
|
return () => ipcRenderer.removeListener('mc:progress', l);
|
||||||
technicPickFolder: () => ipcRenderer.invoke('technic:pickFolder'),
|
},
|
||||||
technicScan: (folder) => ipcRenderer.invoke('technic:scan', folder),
|
technicPickFolder: () => ipcRenderer.invoke('technic:pickFolder'),
|
||||||
technicImport: (entries, copyData) => ipcRenderer.invoke('technic:import', entries, copyData),
|
technicScan: (folder) => ipcRenderer.invoke('technic:scan', folder),
|
||||||
onTechnicProgress: (cb) => {
|
technicImport: (entries, copyData) => ipcRenderer.invoke('technic:import', entries, copyData),
|
||||||
const l = (_e, d) => cb(d);
|
onTechnicProgress: (cb) => {
|
||||||
ipcRenderer.on('technic:progress', l);
|
const l = (_e, d) => cb(d);
|
||||||
return () => ipcRenderer.removeListener('technic:progress', l);
|
ipcRenderer.on('technic:progress', l);
|
||||||
},
|
return () => ipcRenderer.removeListener('technic:progress', l);
|
||||||
technicPlatformSearch: (query) => ipcRenderer.invoke('technicPlatform:search', query),
|
},
|
||||||
technicPlatformInstall: (slug) => ipcRenderer.invoke('technicPlatform:install', slug),
|
technicPlatformSearch: (query) => ipcRenderer.invoke('technicPlatform:search', query),
|
||||||
onTechnicPlatformProgress: (cb) => {
|
technicPlatformInstall: (slug) => ipcRenderer.invoke('technicPlatform:install', slug),
|
||||||
const l = (_e, d) => cb(d);
|
onTechnicPlatformProgress: (cb) => {
|
||||||
ipcRenderer.on('technicPlatform:progress', l);
|
const l = (_e, d) => cb(d);
|
||||||
return () => ipcRenderer.removeListener('technicPlatform:progress', l);
|
ipcRenderer.on('technicPlatform:progress', l);
|
||||||
},
|
return () => ipcRenderer.removeListener('technicPlatform:progress', l);
|
||||||
|
},
|
||||||
// Bedrock (Windows-Store-Ausgabe)
|
|
||||||
bedrockDetect: () => ipcRenderer.invoke('bedrock:detect'),
|
// Bedrock (Windows-Store-Ausgabe)
|
||||||
bedrockLaunch: () => ipcRenderer.invoke('bedrock:launch'),
|
bedrockDetect: () => ipcRenderer.invoke('bedrock:detect'),
|
||||||
bedrockWorlds: () => ipcRenderer.invoke('bedrock:worlds'),
|
bedrockLaunch: () => ipcRenderer.invoke('bedrock:launch'),
|
||||||
bedrockList: (kind) => ipcRenderer.invoke('bedrock:list', kind),
|
bedrockWorlds: () => ipcRenderer.invoke('bedrock:worlds'),
|
||||||
bedrockRemove: (kind, name) => ipcRenderer.invoke('bedrock:remove', kind, name),
|
bedrockList: (kind) => ipcRenderer.invoke('bedrock:list', kind),
|
||||||
bedrockOpenFolder: (kind) => ipcRenderer.invoke('bedrock:openFolder', kind),
|
bedrockRemove: (kind, name) => ipcRenderer.invoke('bedrock:remove', kind, name),
|
||||||
bedrockInstallAddon: () => ipcRenderer.invoke('bedrock:installAddon'),
|
bedrockOpenFolder: (kind) => ipcRenderer.invoke('bedrock:openFolder', kind),
|
||||||
bedrockOpenWorld: (dir) => ipcRenderer.invoke('bedrock:openWorld', dir),
|
bedrockInstallAddon: () => ipcRenderer.invoke('bedrock:installAddon'),
|
||||||
|
bedrockOpenWorld: (dir) => ipcRenderer.invoke('bedrock:openWorld', dir),
|
||||||
// echte Minecraft-Icons
|
|
||||||
iconsList: () => ipcRenderer.invoke('icons:list'),
|
// echte Minecraft-Icons
|
||||||
iconsExtract: () => ipcRenderer.invoke('icons:extract'),
|
iconsList: () => ipcRenderer.invoke('icons:list'),
|
||||||
|
iconsExtract: () => ipcRenderer.invoke('icons:extract'),
|
||||||
// Updates
|
|
||||||
updateCheck: () => ipcRenderer.invoke('update:check'),
|
// Updates
|
||||||
updateInstall: (asset) => ipcRenderer.invoke('update:install', asset),
|
updateCheck: () => ipcRenderer.invoke('update:check'),
|
||||||
onUpdateAvailable: (cb) => {
|
updateLastCheck: () => ipcRenderer.invoke('update:lastCheck'),
|
||||||
const l = (_e, d) => cb(d);
|
updateInstall: (asset) => ipcRenderer.invoke('update:install', asset),
|
||||||
ipcRenderer.on('update:available', l);
|
onUpdateAvailable: (cb) => {
|
||||||
return () => ipcRenderer.removeListener('update:available', l);
|
const l = (_e, d) => cb(d);
|
||||||
},
|
ipcRenderer.on('update:available', l);
|
||||||
// Ergebnis des automatischen Checks beim Start – auch wenn kein Update
|
return () => ipcRenderer.removeListener('update:available', l);
|
||||||
// verfügbar ist (im Unterschied zu onUpdateAvailable, das nur bei
|
},
|
||||||
// verfügbarem Update feuert)
|
// Ergebnis des automatischen Checks beim Start – auch wenn kein Update
|
||||||
onUpdateChecked: (cb) => {
|
// verfügbar ist (im Unterschied zu onUpdateAvailable, das nur bei
|
||||||
const l = (_e, d) => cb(d);
|
// verfügbarem Update feuert)
|
||||||
ipcRenderer.on('update:checked', l);
|
onUpdateChecked: (cb) => {
|
||||||
return () => ipcRenderer.removeListener('update:checked', l);
|
const l = (_e, d) => cb(d);
|
||||||
},
|
ipcRenderer.on('update:checked', l);
|
||||||
onUpdateProgress: (cb) => {
|
return () => ipcRenderer.removeListener('update:checked', l);
|
||||||
const l = (_e, d) => cb(d);
|
},
|
||||||
ipcRenderer.on('update:progress', l);
|
onUpdateProgress: (cb) => {
|
||||||
return () => ipcRenderer.removeListener('update:progress', l);
|
const l = (_e, d) => cb(d);
|
||||||
},
|
ipcRenderer.on('update:progress', l);
|
||||||
|
return () => ipcRenderer.removeListener('update:progress', l);
|
||||||
openExternal: (url) => ipcRenderer.invoke('app:openExternal', url),
|
},
|
||||||
pickFolder: (title) => ipcRenderer.invoke('dialog:pickFolder', title),
|
|
||||||
pickJava: () => ipcRenderer.invoke('dialog:pickJava'),
|
openExternal: (url) => ipcRenderer.invoke('app:openExternal', url),
|
||||||
appInfo: () => ipcRenderer.invoke('app:info'),
|
pickFolder: (title) => ipcRenderer.invoke('dialog:pickFolder', title),
|
||||||
appLicense: () => ipcRenderer.invoke('app:license'),
|
pickJava: () => ipcRenderer.invoke('dialog:pickJava'),
|
||||||
|
appInfo: () => ipcRenderer.invoke('app:info'),
|
||||||
// Einstellungen/Konsole als eigenes, frei verschiebbares Fenster
|
appLicense: () => ipcRenderer.invoke('app:license'),
|
||||||
popoutOpen: (modalId, params) => ipcRenderer.send('popout:open', modalId, params),
|
|
||||||
popoutHide: () => ipcRenderer.send('popout:hide'),
|
// Einstellungen/Konsole als eigenes, frei verschiebbares Fenster
|
||||||
onPopoutReopen: (cb) => {
|
popoutOpen: (modalId, params) => ipcRenderer.send('popout:open', modalId, params),
|
||||||
const l = (_e, params) => cb(params);
|
popoutHide: () => ipcRenderer.send('popout:hide'),
|
||||||
ipcRenderer.on('popout:reopen', l);
|
onPopoutReopen: (cb) => {
|
||||||
return () => ipcRenderer.removeListener('popout:reopen', l);
|
const l = (_e, params) => cb(params);
|
||||||
},
|
ipcRenderer.on('popout:reopen', l);
|
||||||
onSettingsChanged: (cb) => {
|
return () => ipcRenderer.removeListener('popout:reopen', l);
|
||||||
const l = (_e, s) => cb(s);
|
},
|
||||||
ipcRenderer.on('settings:changed', l);
|
onSettingsChanged: (cb) => {
|
||||||
return () => ipcRenderer.removeListener('settings:changed', l);
|
const l = (_e, s) => cb(s);
|
||||||
},
|
ipcRenderer.on('settings:changed', l);
|
||||||
onInstancesChanged: (cb) => {
|
return () => ipcRenderer.removeListener('settings:changed', l);
|
||||||
const l = () => cb();
|
},
|
||||||
ipcRenderer.on('instances:changed', l);
|
onInstancesChanged: (cb) => {
|
||||||
return () => ipcRenderer.removeListener('instances:changed', l);
|
const l = () => cb();
|
||||||
},
|
ipcRenderer.on('instances:changed', l);
|
||||||
onSkinChanged: (cb) => {
|
return () => ipcRenderer.removeListener('instances:changed', l);
|
||||||
const l = () => cb();
|
},
|
||||||
ipcRenderer.on('skin:changed', l);
|
onSkinChanged: (cb) => {
|
||||||
return () => ipcRenderer.removeListener('skin:changed', l);
|
const l = () => cb();
|
||||||
},
|
ipcRenderer.on('skin:changed', l);
|
||||||
updateGetPending: () => ipcRenderer.invoke('update:getPending'),
|
return () => ipcRenderer.removeListener('skin:changed', l);
|
||||||
|
},
|
||||||
// Spielaufzeichnung
|
updateGetPending: () => ipcRenderer.invoke('update:getPending'),
|
||||||
recordingListWindows: () => ipcRenderer.invoke('recording:listWindows'),
|
|
||||||
recordingGetAudioSourceId: () => ipcRenderer.invoke('recording:getAudioSourceId'),
|
// Spielaufzeichnung
|
||||||
recordingSave: (instanceName, buffer, serverLabel, fps) => ipcRenderer.invoke('recording:save', instanceName, buffer, serverLabel, fps),
|
recordingListWindows: () => ipcRenderer.invoke('recording:listWindows'),
|
||||||
recordingOpenFolder: (instanceName) => ipcRenderer.invoke('recording:openFolder', instanceName),
|
recordingGetAudioSourceId: () => ipcRenderer.invoke('recording:getAudioSourceId'),
|
||||||
recordingList: (instanceName) => ipcRenderer.invoke('recording:list', instanceName),
|
recordingSave: (instanceName, buffer, serverLabel, fps) => ipcRenderer.invoke('recording:save', instanceName, buffer, serverLabel, fps),
|
||||||
recordingThumbnail: (instanceName, dateiname) => ipcRenderer.invoke('recording:thumbnail', instanceName, dateiname),
|
recordingOpenFolder: (instanceName) => ipcRenderer.invoke('recording:openFolder', instanceName),
|
||||||
recordingDelete: (instanceName, dateiname) => ipcRenderer.invoke('recording:delete', instanceName, dateiname),
|
recordingList: (instanceName) => ipcRenderer.invoke('recording:list', instanceName),
|
||||||
recordingOpen: (instanceName, dateiname) => ipcRenderer.invoke('recording:open', instanceName, dateiname),
|
recordingThumbnail: (instanceName, dateiname) => ipcRenderer.invoke('recording:thumbnail', instanceName, dateiname),
|
||||||
|
recordingDelete: (instanceName, dateiname) => ipcRenderer.invoke('recording:delete', instanceName, dateiname),
|
||||||
// Freundesystem
|
recordingOpen: (instanceName, dateiname) => ipcRenderer.invoke('recording:open', instanceName, dateiname),
|
||||||
friendsEnabled: () => ipcRenderer.invoke('friends:enabled'),
|
|
||||||
friendsList: () => ipcRenderer.invoke('friends:list'),
|
// Freundesystem
|
||||||
friendsRequests: () => ipcRenderer.invoke('friends:requests'),
|
friendsEnabled: () => ipcRenderer.invoke('friends:enabled'),
|
||||||
friendsAdd: (targetUsername) => ipcRenderer.invoke('friends:add', targetUsername),
|
friendsList: () => ipcRenderer.invoke('friends:list'),
|
||||||
friendsRespond: (otherUuid, accept) => ipcRenderer.invoke('friends:respond', otherUuid, accept),
|
friendsRequests: () => ipcRenderer.invoke('friends:requests'),
|
||||||
friendsRemove: (otherUuid) => ipcRenderer.invoke('friends:remove', otherUuid),
|
friendsAdd: (targetUsername) => ipcRenderer.invoke('friends:add', targetUsername),
|
||||||
friendsSentRequests: () => ipcRenderer.invoke('friends:sentRequests'),
|
friendsRespond: (otherUuid, accept) => ipcRenderer.invoke('friends:respond', otherUuid, accept),
|
||||||
friendsCancel: (otherUuid) => ipcRenderer.invoke('friends:cancel', otherUuid),
|
friendsRemove: (otherUuid) => ipcRenderer.invoke('friends:remove', otherUuid),
|
||||||
friendsSetNickname: (uuid, nickname) => ipcRenderer.invoke('friends:setNickname', uuid, nickname),
|
friendsSentRequests: () => ipcRenderer.invoke('friends:sentRequests'),
|
||||||
friendsRequestCount: () => ipcRenderer.invoke('friends:requestCount'),
|
friendsCancel: (otherUuid) => ipcRenderer.invoke('friends:cancel', otherUuid),
|
||||||
onFriendsRequestCount: (cb) => {
|
friendsSetNickname: (uuid, nickname) => ipcRenderer.invoke('friends:setNickname', uuid, nickname),
|
||||||
const l = (_e, zahl) => cb(zahl);
|
friendsRequestCount: () => ipcRenderer.invoke('friends:requestCount'),
|
||||||
ipcRenderer.on('friends:requestCount', l);
|
clipSetActive: (active, instanceId) => ipcRenderer.invoke('clip:setActive', active, instanceId),
|
||||||
return () => ipcRenderer.removeListener('friends:requestCount', l);
|
clipSave: (data) => ipcRenderer.invoke('clip:save', data),
|
||||||
},
|
onClipHotkey: (cb) => {
|
||||||
recordingGetServer: (instanceId) => ipcRenderer.invoke('recording:getServer', instanceId),
|
const l = () => cb();
|
||||||
onRecordingServerDetected: (cb) => {
|
ipcRenderer.on('clip:hotkey', l);
|
||||||
const l = (_e, payload) => cb(payload);
|
return () => ipcRenderer.removeListener('clip:hotkey', l);
|
||||||
ipcRenderer.on('recording:serverDetected', l);
|
},
|
||||||
return () => ipcRenderer.removeListener('recording:serverDetected', l);
|
onUiToast: (cb) => {
|
||||||
},
|
const l = (_e, t) => cb(t);
|
||||||
recordingGetPid: (instanceId) => ipcRenderer.invoke('recording:getPid', instanceId),
|
ipcRenderer.on('ui:toast', l);
|
||||||
recordingSetCaptureTarget: (sourceId) => ipcRenderer.invoke('recording:setCaptureTarget', sourceId),
|
return () => ipcRenderer.removeListener('ui:toast', l);
|
||||||
recordingGetWindowBounds: (instanceId, fensterTitel) => ipcRenderer.invoke('recording:getWindowBounds', instanceId, fensterTitel),
|
},
|
||||||
recordingStartGameAudio: (pid) => ipcRenderer.invoke('recording:startGameAudio', pid),
|
friendsJoin: (server, version) => ipcRenderer.invoke('friends:join', server, version),
|
||||||
recordingStopGameAudio: () => ipcRenderer.invoke('recording:stopGameAudio'),
|
friendsJoinNew: (server, version) => ipcRenderer.invoke('friends:joinNew', server, version),
|
||||||
onGameAudioChunk: (cb) => {
|
friendsMyServer: () => ipcRenderer.invoke('friends:myServer'),
|
||||||
const l = (_e, chunk) => cb(chunk);
|
friendsInvite: (targetUuid) => ipcRenderer.invoke('friends:invite', targetUuid),
|
||||||
ipcRenderer.on('recording:gameAudioChunk', l);
|
friendsInvites: () => ipcRenderer.invoke('friends:invites'),
|
||||||
return () => ipcRenderer.removeListener('recording:gameAudioChunk', l);
|
friendsDismissInvite: (id) => ipcRenderer.invoke('friends:dismissInvite', id),
|
||||||
},
|
onFriendsInvite: (cb) => {
|
||||||
recordingSetGuard: (active) => ipcRenderer.send('recording:guard', active),
|
const l = (_e, einladung) => cb(einladung);
|
||||||
onRecordCloseAttempt: (cb) => {
|
ipcRenderer.on('friends:invite', l);
|
||||||
const l = () => cb();
|
return () => ipcRenderer.removeListener('friends:invite', l);
|
||||||
ipcRenderer.on('record:closeAttempt', l);
|
},
|
||||||
return () => ipcRenderer.removeListener('record:closeAttempt', l);
|
onFriendsRequestCount: (cb) => {
|
||||||
},
|
const l = (_e, zahl) => cb(zahl);
|
||||||
});
|
ipcRenderer.on('friends:requestCount', l);
|
||||||
|
return () => ipcRenderer.removeListener('friends:requestCount', l);
|
||||||
|
},
|
||||||
|
recordingGetServer: (instanceId) => ipcRenderer.invoke('recording:getServer', instanceId),
|
||||||
|
onRecordingServerDetected: (cb) => {
|
||||||
|
const l = (_e, payload) => cb(payload);
|
||||||
|
ipcRenderer.on('recording:serverDetected', l);
|
||||||
|
return () => ipcRenderer.removeListener('recording:serverDetected', l);
|
||||||
|
},
|
||||||
|
recordingGetPid: (instanceId) => ipcRenderer.invoke('recording:getPid', instanceId),
|
||||||
|
recordingSetCaptureTarget: (sourceId) => ipcRenderer.invoke('recording:setCaptureTarget', sourceId),
|
||||||
|
recordingGetWindowBounds: (instanceId, fensterTitel) => ipcRenderer.invoke('recording:getWindowBounds', instanceId, fensterTitel),
|
||||||
|
recordingStartGameAudio: (pid) => ipcRenderer.invoke('recording:startGameAudio', pid),
|
||||||
|
recordingStopGameAudio: () => ipcRenderer.invoke('recording:stopGameAudio'),
|
||||||
|
onGameAudioChunk: (cb) => {
|
||||||
|
const l = (_e, chunk) => cb(chunk);
|
||||||
|
ipcRenderer.on('recording:gameAudioChunk', l);
|
||||||
|
return () => ipcRenderer.removeListener('recording:gameAudioChunk', l);
|
||||||
|
},
|
||||||
|
recordingSetGuard: (active) => ipcRenderer.send('recording:guard', active),
|
||||||
|
onRecordCloseAttempt: (cb) => {
|
||||||
|
const l = () => cb();
|
||||||
|
ipcRenderer.on('record:closeAttempt', l);
|
||||||
|
return () => ipcRenderer.removeListener('record:closeAttempt', l);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
+5955
-5607
File diff suppressed because it is too large
Load Diff
+500
-495
@@ -1,495 +1,500 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* store.js – Persistenz für AeroMC
|
* store.js – Persistenz für AeroMC
|
||||||
* ------------------------------------
|
* ------------------------------------
|
||||||
* Jede Instanz ist ein isolierter Ordner (MultiMC-Prinzip):
|
* Jede Instanz ist ein isolierter Ordner (MultiMC-Prinzip):
|
||||||
* <instancesDir>/<id>/instance.json – Metadaten
|
* <instancesDir>/<id>/instance.json – Metadaten
|
||||||
* <instancesDir>/<id>/minecraft/ – das eigentliche Spielverzeichnis (.minecraft-Ersatz)
|
* <instancesDir>/<id>/minecraft/ – das eigentliche Spielverzeichnis (.minecraft-Ersatz)
|
||||||
*
|
*
|
||||||
* Globale Einstellungen liegen in <userData>/settings.json.
|
* Globale Einstellungen liegen in <userData>/settings.json.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
|
|
||||||
let USER_DATA = null; // wird von main.js via init() gesetzt
|
let USER_DATA = null; // wird von main.js via init() gesetzt
|
||||||
|
|
||||||
function init(userDataPath) {
|
function init(userDataPath) {
|
||||||
USER_DATA = userDataPath;
|
USER_DATA = userDataPath;
|
||||||
ensureDir(USER_DATA);
|
ensureDir(USER_DATA);
|
||||||
ensureDir(getInstancesDir());
|
ensureDir(getInstancesDir());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- kleine Helfer ----------
|
// ---------- kleine Helfer ----------
|
||||||
|
|
||||||
function ensureDir(dir) {
|
function ensureDir(dir) {
|
||||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
function readJson(file, fallback) {
|
function readJson(file, fallback) {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||||
} catch {
|
} catch {
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeJson(file, data) {
|
function writeJson(file, data) {
|
||||||
ensureDir(path.dirname(file));
|
ensureDir(path.dirname(file));
|
||||||
fs.writeFileSync(file, JSON.stringify(data, null, 2), 'utf8');
|
fs.writeFileSync(file, JSON.stringify(data, null, 2), 'utf8');
|
||||||
}
|
}
|
||||||
|
|
||||||
function newId() {
|
function newId() {
|
||||||
return crypto.randomBytes(8).toString('hex');
|
return crypto.randomBytes(8).toString('hex');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Einstellungen ----------
|
// ---------- Einstellungen ----------
|
||||||
|
|
||||||
const DEFAULT_SETTINGS = {
|
const DEFAULT_SETTINGS = {
|
||||||
instancesDir: null, // null => Standard (userData/instances)
|
instancesDir: null, // null => Standard (userData/instances)
|
||||||
javaPath: '', // leer => "java" aus PATH
|
javaPath: '', // leer => "java" aus PATH
|
||||||
autoJava: true, // fehlende Java-Version bei Bedarf selbst herunterladen
|
autoJava: true, // fehlende Java-Version bei Bedarf selbst herunterladen
|
||||||
autoBackupEnabled: false, // Sicherungen nach Zeitplan – standardmäßig aus
|
autoBackupEnabled: false, // Sicherungen nach Zeitplan – standardmäßig aus
|
||||||
autoBackupEvery: 'daily', // 'daily' | 'weekly'
|
autoBackupEvery: 'daily', // 'daily' | 'weekly'
|
||||||
autoBackupBeforeLaunch: false, // zusätzlich vor jedem Spielstart
|
autoBackupBeforeLaunch: false, // zusätzlich vor jedem Spielstart
|
||||||
autoBackupKeep: 5, // wie viele automatische Sicherungen behalten werden
|
autoBackupKeep: 5, // wie viele automatische Sicherungen behalten werden
|
||||||
autoBackupLast: '', // Zeitpunkt der letzten automatischen Sicherung
|
autoBackupLast: '', // Zeitpunkt der letzten automatischen Sicherung
|
||||||
defaultMinMemMb: 2048,
|
defaultMinMemMb: 2048,
|
||||||
defaultMaxMemMb: 4096,
|
defaultMaxMemMb: 4096,
|
||||||
azureClientId: '', // Azure-App-ID für den Microsoft-Login (Phase 3)
|
azureClientId: '', // Azure-App-ID für den Microsoft-Login (Phase 3)
|
||||||
curseforgeApiKey: '', // eigener CurseForge-API-Key (hat Vorrang)
|
curseforgeApiKey: '', // eigener CurseForge-API-Key (hat Vorrang)
|
||||||
curseforgeProxyUrl: '', // sonst: Adresse der eigenen CurseForge-Weiterleitung
|
curseforgeProxyUrl: '', // sonst: Adresse der eigenen CurseForge-Weiterleitung
|
||||||
curseforgeProxyToken: '', // optionales Geheimnis für diese Weiterleitung
|
curseforgeProxyToken: '', // optionales Geheimnis für diese Weiterleitung
|
||||||
globalPreLaunch: '',
|
globalPreLaunch: '',
|
||||||
globalPostExit: '',
|
globalPostExit: '',
|
||||||
lastSelectedId: '', // zuletzt ausgewählte Instanz (beim Start wieder markieren)
|
lastSelectedId: '', // zuletzt ausgewählte Instanz (beim Start wieder markieren)
|
||||||
showConsoleOnLaunch: false, // Konsole beim Spielstart anzeigen
|
showConsoleOnLaunch: false, // Konsole beim Spielstart anzeigen
|
||||||
autoCloseConsole: false, // Konsole nach Spielende automatisch schließen
|
autoCloseConsole: false, // Konsole nach Spielende automatisch schließen
|
||||||
showConsoleOnCrash: true, // Konsole anzeigen, wenn das Spiel abstürzt
|
showConsoleOnCrash: true, // Konsole anzeigen, wenn das Spiel abstürzt
|
||||||
consoleMaxLines: 100000, // Zeilenbegrenzung der Konsole
|
consoleMaxLines: 100000, // Zeilenbegrenzung der Konsole
|
||||||
mcMaximized: false, // Minecraft maximiert starten
|
mcMaximized: false, // Minecraft maximiert starten
|
||||||
mcWidth: 854,
|
mcWidth: 854,
|
||||||
mcHeight: 480,
|
mcHeight: 480,
|
||||||
sortBy: 'lastPlayed', // lastPlayed | name
|
sortBy: 'lastPlayed', // lastPlayed | name
|
||||||
iso3dIcons: true, // Block-Symbole räumlich darstellen
|
iso3dIcons: true, // Block-Symbole räumlich darstellen
|
||||||
bedrockEnabled: true, // Bedrock-Bereich anzeigen (nur wenn auch installiert)
|
bedrockEnabled: true, // Bedrock-Bereich anzeigen (nur wenn auch installiert)
|
||||||
desktopNotifications: true, // Windows-Meldung bei fertigem Hintergrund-Download/-Backup
|
desktopNotifications: true, // Windows-Meldung bei fertigem Hintergrund-Download/-Backup
|
||||||
friendsEnabled: true, // Freundesystem: eigene Online-Meldung + auffindbar für Anfragen
|
friendsEnabled: true,
|
||||||
friendNicknames: {}, // rein lokal: uuid -> eigener Spitzname für einen Freund
|
friendsShareServer: true, // Freunde sehen, auf welchem Server man spielt (für "Beitreten")
|
||||||
autostart: false, // mit Windows starten, im Infobereich statt als Fenster
|
autoModUpdates: false, // vor jedem Start Mods über Modrinth aktualisieren // Freundesystem: eigene Online-Meldung + auffindbar für Anfragen
|
||||||
discordClientId: '', // optional, nur per Hand in settings.json; leer => eingebaute AeroMC-App-ID
|
friendNicknames: {}, // rein lokal: uuid -> eigener Spitzname für einen Freund
|
||||||
bgSkin: '', // '' = aus, 'active' = aktives Konto, sonst Konto-UUID
|
autostart: false, // mit Windows starten, im Infobereich statt als Fenster
|
||||||
bgSkinOpacity: 55, // Deckkraft des Hintergrund-Skins in %
|
discordClientId: '', // optional, nur per Hand in settings.json; leer => eingebaute AeroMC-App-ID
|
||||||
bgSkinPose: 'walking', // Pose des 3D-Renders (falls der Pose-Dienst erreichbar ist)
|
bgSkin: '', // '' = aus, 'active' = aktives Konto, sonst Konto-UUID
|
||||||
bgSkinCycle: false, // bei 2+ angemeldeten Konten automatisch durchwechseln
|
bgSkinOpacity: 55, // Deckkraft des Hintergrund-Skins in %
|
||||||
bgSkinCycleSeconds: 30, // Sekunden zwischen zwei Kontowechseln
|
bgSkinPose: 'walking', // Pose des 3D-Renders (falls der Pose-Dienst erreichbar ist)
|
||||||
wrapperCommand: '', // Externe Programme: Startprogramm, das Java umschließt
|
bgSkinCycle: false, // bei 2+ angemeldeten Konten automatisch durchwechseln
|
||||||
proxyEnabled: false, // HTTP-Proxy für Downloads/Netzwerk
|
bgSkinCycleSeconds: 30, // Sekunden zwischen zwei Kontowechseln
|
||||||
proxyHost: '',
|
wrapperCommand: '', // Externe Programme: Startprogramm, das Java umschließt
|
||||||
proxyPort: '',
|
proxyEnabled: false, // HTTP-Proxy für Downloads/Netzwerk
|
||||||
proxyUser: '',
|
proxyHost: '',
|
||||||
proxyPass: '',
|
proxyPort: '',
|
||||||
theme: 'dark',
|
proxyUser: '',
|
||||||
// Spielaufzeichnung - bleibt über Neustarts erhalten (letzte Wahl je Feld)
|
proxyPass: '',
|
||||||
recordingResolution: 'native',
|
theme: 'dark',
|
||||||
recordingQuality: 'medium',
|
// Spielaufzeichnung - bleibt über Neustarts erhalten (letzte Wahl je Feld)
|
||||||
recordingFramerate: 30,
|
recordingResolution: 'native',
|
||||||
recordingAudioSystem: true,
|
recordingQuality: 'medium',
|
||||||
recordingAudioGame: false,
|
recordingFramerate: 30,
|
||||||
recordingVolSystem: 100,
|
recordingAudioSystem: true,
|
||||||
recordingVolGame: 100,
|
recordingAudioGame: false,
|
||||||
recordingMicSelected: [], // Liste gewählter Mikrofon-deviceIds
|
recordingVolSystem: 100,
|
||||||
recordingMicVolumes: {}, // deviceId -> Lautstärke (0-100)
|
recordingVolGame: 100,
|
||||||
};
|
recordingMicSelected: [], // Liste gewählter Mikrofon-deviceIds
|
||||||
|
recordingMicVolumes: {}, // deviceId -> Lautstärke (0-100)
|
||||||
function settingsFile() {
|
};
|
||||||
return path.join(USER_DATA, 'settings.json');
|
|
||||||
}
|
function settingsFile() {
|
||||||
|
return path.join(USER_DATA, 'settings.json');
|
||||||
function getSettings() {
|
}
|
||||||
return Object.assign({}, DEFAULT_SETTINGS, readJson(settingsFile(), {}));
|
|
||||||
}
|
function getSettings() {
|
||||||
|
return Object.assign({}, DEFAULT_SETTINGS, readJson(settingsFile(), {}));
|
||||||
function saveSettings(patch) {
|
}
|
||||||
const merged = Object.assign({}, getSettings(), patch || {});
|
|
||||||
writeJson(settingsFile(), merged);
|
function saveSettings(patch) {
|
||||||
// Falls sich das Instanzverzeichnis geändert hat: sicherstellen, dass es existiert
|
const merged = Object.assign({}, getSettings(), patch || {});
|
||||||
ensureDir(getInstancesDir());
|
writeJson(settingsFile(), merged);
|
||||||
return merged;
|
// Falls sich das Instanzverzeichnis geändert hat: sicherstellen, dass es existiert
|
||||||
}
|
ensureDir(getInstancesDir());
|
||||||
|
return merged;
|
||||||
// ---------- Instanzen ----------
|
}
|
||||||
|
|
||||||
function getInstancesDir() {
|
// ---------- Instanzen ----------
|
||||||
const s = getSettings();
|
|
||||||
return s.instancesDir && s.instancesDir.trim()
|
function getInstancesDir() {
|
||||||
? s.instancesDir
|
const s = getSettings();
|
||||||
: path.join(USER_DATA, 'instances');
|
return s.instancesDir && s.instancesDir.trim()
|
||||||
}
|
? s.instancesDir
|
||||||
|
: path.join(USER_DATA, 'instances');
|
||||||
function instancePath(id) {
|
}
|
||||||
return path.join(getInstancesDir(), id);
|
|
||||||
}
|
function instancePath(id) {
|
||||||
|
return path.join(getInstancesDir(), id);
|
||||||
function instanceFile(id) {
|
}
|
||||||
return path.join(instancePath(id), 'instance.json');
|
|
||||||
}
|
function instanceFile(id) {
|
||||||
|
return path.join(instancePath(id), 'instance.json');
|
||||||
function gameDir(id) {
|
}
|
||||||
return path.join(instancePath(id), 'minecraft');
|
|
||||||
}
|
function gameDir(id) {
|
||||||
|
return path.join(instancePath(id), 'minecraft');
|
||||||
/*
|
}
|
||||||
* Inhalte je Art: Mods, Ressourcenpakete und Shader liegen in verschiedenen
|
|
||||||
* Ordnern des Spielverzeichnisses und werden getrennt verzeichnet.
|
/*
|
||||||
* "mod" behält die bisherigen Namen, damit vorhandene Instanzen unverändert
|
* Inhalte je Art: Mods, Ressourcenpakete und Shader liegen in verschiedenen
|
||||||
* weiterlaufen.
|
* Ordnern des Spielverzeichnisses und werden getrennt verzeichnet.
|
||||||
*/
|
* "mod" behält die bisherigen Namen, damit vorhandene Instanzen unverändert
|
||||||
const INHALT_ORDNER = {
|
* weiterlaufen.
|
||||||
mod: 'mods', resourcepack: 'resourcepacks', shader: 'shaderpacks', datapack: 'datapacks',
|
*/
|
||||||
};
|
const INHALT_ORDNER = {
|
||||||
const INHALT_INDEX = {
|
mod: 'mods', resourcepack: 'resourcepacks', shader: 'shaderpacks', datapack: 'datapacks',
|
||||||
mod: 'mods.json', resourcepack: 'resourcepacks.json', shader: 'shaders.json', datapack: 'datapacks.json',
|
};
|
||||||
};
|
const INHALT_INDEX = {
|
||||||
|
mod: 'mods.json', resourcepack: 'resourcepacks.json', shader: 'shaders.json', datapack: 'datapacks.json',
|
||||||
function inhaltsDir(id, art = 'mod') {
|
};
|
||||||
return path.join(gameDir(id), INHALT_ORDNER[art] || INHALT_ORDNER.mod);
|
|
||||||
}
|
function inhaltsDir(id, art = 'mod') {
|
||||||
|
return path.join(gameDir(id), INHALT_ORDNER[art] || INHALT_ORDNER.mod);
|
||||||
function modsDir(id, art = 'mod') {
|
}
|
||||||
return inhaltsDir(id, art);
|
|
||||||
}
|
function modsDir(id, art = 'mod') {
|
||||||
|
return inhaltsDir(id, art);
|
||||||
function modsIndexFile(id, art = 'mod') {
|
}
|
||||||
return path.join(instancePath(id), INHALT_INDEX[art] || INHALT_INDEX.mod);
|
|
||||||
}
|
function modsIndexFile(id, art = 'mod') {
|
||||||
|
return path.join(instancePath(id), INHALT_INDEX[art] || INHALT_INDEX.mod);
|
||||||
// ---------- installierte Mods (Metadaten je Instanz) ----------
|
}
|
||||||
|
|
||||||
/*
|
// ---------- installierte Mods (Metadaten je Instanz) ----------
|
||||||
* Abgeschaltete Mods heißen "<name>.jar.disabled" – so übergeht Minecraft sie,
|
|
||||||
* ohne dass die Datei verloren geht. In mods.json steht immer der eingeschaltete
|
/*
|
||||||
* Name; ob eine Mod an ist, entscheidet allein die Datei auf der Platte.
|
* Abgeschaltete Mods heißen "<name>.jar.disabled" – so übergeht Minecraft sie,
|
||||||
*/
|
* ohne dass die Datei verloren geht. In mods.json steht immer der eingeschaltete
|
||||||
const MOD_ENDUNG = /\.(jar|zip)$/i;
|
* Name; ob eine Mod an ist, entscheidet allein die Datei auf der Platte.
|
||||||
const AUS_ENDUNG = /\.(jar|zip)\.disabled$/i;
|
*/
|
||||||
|
const MOD_ENDUNG = /\.(jar|zip)$/i;
|
||||||
function scanModsDir(id, art = 'mod') {
|
const AUS_ENDUNG = /\.(jar|zip)\.disabled$/i;
|
||||||
const dir = modsDir(id, art);
|
|
||||||
if (!fs.existsSync(dir)) return [];
|
function scanModsDir(id, art = 'mod') {
|
||||||
const gefunden = [];
|
const dir = modsDir(id, art);
|
||||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
if (!fs.existsSync(dir)) return [];
|
||||||
if (!entry.isFile()) continue;
|
const gefunden = [];
|
||||||
const aus = AUS_ENDUNG.test(entry.name);
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||||
if (!aus && !MOD_ENDUNG.test(entry.name)) continue;
|
if (!entry.isFile()) continue;
|
||||||
const filename = aus ? entry.name.replace(/\.disabled$/i, '') : entry.name;
|
const aus = AUS_ENDUNG.test(entry.name);
|
||||||
gefunden.push({
|
if (!aus && !MOD_ENDUNG.test(entry.name)) continue;
|
||||||
projectId: null,
|
const filename = aus ? entry.name.replace(/\.disabled$/i, '') : entry.name;
|
||||||
title: filename.replace(MOD_ENDUNG, ''),
|
gefunden.push({
|
||||||
filename,
|
projectId: null,
|
||||||
versionNumber: 'Importiert',
|
title: filename.replace(MOD_ENDUNG, ''),
|
||||||
enabled: !aus,
|
filename,
|
||||||
});
|
versionNumber: 'Importiert',
|
||||||
}
|
enabled: !aus,
|
||||||
gefunden.sort((a, b) => a.filename.localeCompare(b.filename));
|
});
|
||||||
return gefunden;
|
}
|
||||||
}
|
gefunden.sort((a, b) => a.filename.localeCompare(b.filename));
|
||||||
|
return gefunden;
|
||||||
function listMods(id, art = 'mod') {
|
}
|
||||||
const aufDerPlatte = new Map(scanModsDir(id, art).map((m) => [m.filename, m]));
|
|
||||||
const indexed = readJson(modsIndexFile(id, art), []);
|
function listMods(id, art = 'mod') {
|
||||||
const merged = new Map();
|
const aufDerPlatte = new Map(scanModsDir(id, art).map((m) => [m.filename, m]));
|
||||||
|
const indexed = readJson(modsIndexFile(id, art), []);
|
||||||
for (const entry of indexed) {
|
const merged = new Map();
|
||||||
if (!entry || !entry.filename) continue;
|
|
||||||
const platte = aufDerPlatte.get(entry.filename);
|
for (const entry of indexed) {
|
||||||
merged.set(entry.filename, Object.assign({}, entry, {
|
if (!entry || !entry.filename) continue;
|
||||||
enabled: platte ? platte.enabled : true,
|
const platte = aufDerPlatte.get(entry.filename);
|
||||||
fehlt: !platte, // steht im Verzeichnis, Datei ist aber weg
|
merged.set(entry.filename, Object.assign({}, entry, {
|
||||||
}));
|
enabled: platte ? platte.enabled : true,
|
||||||
}
|
fehlt: !platte, // steht im Verzeichnis, Datei ist aber weg
|
||||||
for (const [name, entry] of aufDerPlatte) {
|
}));
|
||||||
if (!merged.has(name)) merged.set(name, Object.assign({ fehlt: false }, entry));
|
}
|
||||||
}
|
for (const [name, entry] of aufDerPlatte) {
|
||||||
return [...merged.values()];
|
if (!merged.has(name)) merged.set(name, Object.assign({ fehlt: false }, entry));
|
||||||
}
|
}
|
||||||
|
return [...merged.values()];
|
||||||
/*
|
}
|
||||||
* Schaltet eine Mod an oder aus, indem die Datei umbenannt wird.
|
|
||||||
* -> { ok, enabled } oder { ok: false, reason }
|
/*
|
||||||
*/
|
* Schaltet eine Mod an oder aus, indem die Datei umbenannt wird.
|
||||||
function setModEnabled(id, filename, enabled, art = 'mod') {
|
* -> { ok, enabled } oder { ok: false, reason }
|
||||||
const dir = modsDir(id, art);
|
*/
|
||||||
const anPfad = path.join(dir, filename);
|
function setModEnabled(id, filename, enabled, art = 'mod') {
|
||||||
const ausPfad = anPfad + '.disabled';
|
const dir = modsDir(id, art);
|
||||||
// Pfad darf den mods-Ordner nicht verlassen
|
const anPfad = path.join(dir, filename);
|
||||||
if (path.dirname(anPfad) !== dir) return { ok: false, reason: 'bad-name' };
|
const ausPfad = anPfad + '.disabled';
|
||||||
|
// Pfad darf den mods-Ordner nicht verlassen
|
||||||
const vonPfad = enabled ? ausPfad : anPfad;
|
if (path.dirname(anPfad) !== dir) return { ok: false, reason: 'bad-name' };
|
||||||
const nachPfad = enabled ? anPfad : ausPfad;
|
|
||||||
|
const vonPfad = enabled ? ausPfad : anPfad;
|
||||||
if (fs.existsSync(anPfad) && fs.existsSync(ausPfad)) {
|
const nachPfad = enabled ? anPfad : ausPfad;
|
||||||
// beide Fassungen da – nichts anfassen, sonst geht eine verloren
|
|
||||||
return { ok: false, reason: 'both-exist' };
|
if (fs.existsSync(anPfad) && fs.existsSync(ausPfad)) {
|
||||||
}
|
// beide Fassungen da – nichts anfassen, sonst geht eine verloren
|
||||||
if (!fs.existsSync(vonPfad)) {
|
return { ok: false, reason: 'both-exist' };
|
||||||
if (fs.existsSync(nachPfad)) return { ok: true, enabled, unveraendert: true };
|
}
|
||||||
return { ok: false, reason: 'not-found' };
|
if (!fs.existsSync(vonPfad)) {
|
||||||
}
|
if (fs.existsSync(nachPfad)) return { ok: true, enabled, unveraendert: true };
|
||||||
try {
|
return { ok: false, reason: 'not-found' };
|
||||||
fs.renameSync(vonPfad, nachPfad);
|
}
|
||||||
return { ok: true, enabled };
|
try {
|
||||||
} catch (err) {
|
fs.renameSync(vonPfad, nachPfad);
|
||||||
return { ok: false, reason: err.message };
|
return { ok: true, enabled };
|
||||||
}
|
} catch (err) {
|
||||||
}
|
return { ok: false, reason: err.message };
|
||||||
|
}
|
||||||
function addMod(id, entry, art = 'mod') {
|
}
|
||||||
const list = listMods(id, art).filter((m) => m.filename !== entry.filename);
|
|
||||||
list.push(entry);
|
function addMod(id, entry, art = 'mod') {
|
||||||
writeJson(modsIndexFile(id, art), list);
|
const list = listMods(id, art).filter((m) => m.filename !== entry.filename);
|
||||||
return list;
|
list.push(entry);
|
||||||
}
|
writeJson(modsIndexFile(id, art), list);
|
||||||
|
return list;
|
||||||
/*
|
}
|
||||||
* Findet installierte Mods, die den per "filename" identifizierten Mod als
|
|
||||||
* Pflicht-Abhängigkeit brauchen (requiredDeps wird beim Installieren über
|
/*
|
||||||
* AeroMC gesetzt – von Hand hinzugefügte oder vor diesem Feature installierte
|
* Findet installierte Mods, die den per "filename" identifizierten Mod als
|
||||||
* Mods haben keine requiredDeps und werden daher nicht als Ziel erkannt,
|
* Pflicht-Abhängigkeit brauchen (requiredDeps wird beim Installieren über
|
||||||
* können aber selbst weiterhin als Abhängiger auftauchen).
|
* AeroMC gesetzt – von Hand hinzugefügte oder vor diesem Feature installierte
|
||||||
* -> [Titel, Titel, ...] der abhängigen Mods
|
* Mods haben keine requiredDeps und werden daher nicht als Ziel erkannt,
|
||||||
*/
|
* können aber selbst weiterhin als Abhängiger auftauchen).
|
||||||
function findDependents(id, filename, art = 'mod') {
|
* -> [Titel, Titel, ...] der abhängigen Mods
|
||||||
const list = listMods(id, art);
|
*/
|
||||||
const target = list.find((m) => m.filename === filename);
|
function findDependents(id, filename, art = 'mod') {
|
||||||
if (!target || !target.projectId) return [];
|
const list = listMods(id, art);
|
||||||
return list
|
const target = list.find((m) => m.filename === filename);
|
||||||
.filter((m) => m.filename !== filename
|
if (!target || !target.projectId) return [];
|
||||||
&& Array.isArray(m.requiredDeps) && m.requiredDeps.includes(target.projectId))
|
return list
|
||||||
.map((m) => m.title || m.filename);
|
.filter((m) => m.filename !== filename
|
||||||
}
|
&& Array.isArray(m.requiredDeps) && m.requiredDeps.includes(target.projectId))
|
||||||
|
.map((m) => m.title || m.filename);
|
||||||
function removeMod(id, filename, art = 'mod') {
|
}
|
||||||
const list = listMods(id, art).filter((m) => m.filename !== filename);
|
|
||||||
writeJson(modsIndexFile(id, art), list);
|
function removeMod(id, filename, art = 'mod') {
|
||||||
// zugehörige Datei löschen – auch, wenn sie gerade abgeschaltet ist
|
const list = listMods(id, art).filter((m) => m.filename !== filename);
|
||||||
const jar = path.join(modsDir(id, art), filename);
|
writeJson(modsIndexFile(id, art), list);
|
||||||
for (const p of [jar, jar + '.disabled']) {
|
// zugehörige Datei löschen – auch, wenn sie gerade abgeschaltet ist
|
||||||
try { if (fs.existsSync(p)) fs.unlinkSync(p); } catch { /* egal */ }
|
const jar = path.join(modsDir(id, art), filename);
|
||||||
}
|
for (const p of [jar, jar + '.disabled']) {
|
||||||
return list;
|
try { if (fs.existsSync(p)) fs.unlinkSync(p); } catch { /* egal */ }
|
||||||
}
|
}
|
||||||
|
return list;
|
||||||
function defaultInstance(name) {
|
}
|
||||||
const now = new Date().toISOString();
|
|
||||||
return {
|
function defaultInstance(name) {
|
||||||
id: newId(),
|
const now = new Date().toISOString();
|
||||||
name: name || 'Neue Instanz',
|
return {
|
||||||
icon: 'grass', // Schlüssel für ein Emoji/Icon im Renderer
|
id: newId(),
|
||||||
customImage: null, // Dateiname eines eigenen Bildes im Instanzordner (überschreibt Emoji)
|
name: name || 'Neue Instanz',
|
||||||
group: '', // optionale Gruppe (MultiMC-artig)
|
icon: 'grass', // Schlüssel für ein Emoji/Icon im Renderer
|
||||||
preLaunchCommand: '', // Befehl vor dem Start
|
customImage: null, // Dateiname eines eigenen Bildes im Instanzordner (überschreibt Emoji)
|
||||||
postExitCommand: '', // Befehl nach dem Beenden
|
group: '', // optionale Gruppe (MultiMC-artig)
|
||||||
created: now,
|
preLaunchCommand: '', // Befehl vor dem Start
|
||||||
lastPlayed: null,
|
postExitCommand: '', // Befehl nach dem Beenden
|
||||||
totalPlaySeconds: 0,
|
created: now,
|
||||||
minecraft: {
|
lastPlayed: null,
|
||||||
version: '', // z. B. "1.21.4" – wird in Phase 2 aus dem Mojang-Manifest befüllt
|
totalPlaySeconds: 0,
|
||||||
loader: 'vanilla', // vanilla | fabric | forge | neoforge | quilt
|
launchCount: 0, // Anzahl Spielstarts (Spielzeit-Statistik)
|
||||||
loaderVersion: '',
|
minecraft: {
|
||||||
},
|
version: '', // z. B. "1.21.4" – wird in Phase 2 aus dem Mojang-Manifest befüllt
|
||||||
java: {
|
loader: 'vanilla', // vanilla | fabric | forge | neoforge | quilt
|
||||||
path: '', // leer => globale Einstellung
|
loaderVersion: '',
|
||||||
minMemMb: null, // null => globale Einstellung
|
},
|
||||||
maxMemMb: null,
|
java: {
|
||||||
extraArgs: '',
|
path: '', // leer => globale Einstellung
|
||||||
},
|
minMemMb: null, // null => globale Einstellung
|
||||||
notes: '',
|
maxMemMb: null,
|
||||||
};
|
extraArgs: '',
|
||||||
}
|
},
|
||||||
|
notes: '',
|
||||||
// liest ein eigenes Instanz-Bild als data:-URI (für die Anzeige im Renderer)
|
};
|
||||||
function iconDataUri(id) {
|
}
|
||||||
const meta = readJson(instanceFile(id), null);
|
|
||||||
if (!meta || !meta.customImage) return null;
|
// liest ein eigenes Instanz-Bild als data:-URI (für die Anzeige im Renderer)
|
||||||
try {
|
function iconDataUri(id) {
|
||||||
const buf = fs.readFileSync(path.join(instancePath(id), meta.customImage));
|
const meta = readJson(instanceFile(id), null);
|
||||||
let ext = path.extname(meta.customImage).slice(1).toLowerCase();
|
if (!meta || !meta.customImage) return null;
|
||||||
if (ext === 'jpg') ext = 'jpeg';
|
try {
|
||||||
return `data:image/${ext};base64,` + buf.toString('base64');
|
const buf = fs.readFileSync(path.join(instancePath(id), meta.customImage));
|
||||||
} catch { return null; }
|
let ext = path.extname(meta.customImage).slice(1).toLowerCase();
|
||||||
}
|
if (ext === 'jpg') ext = 'jpeg';
|
||||||
|
return `data:image/${ext};base64,` + buf.toString('base64');
|
||||||
function setCustomImage(id, sourcePath) {
|
} catch { return null; }
|
||||||
let ext = path.extname(sourcePath).toLowerCase();
|
}
|
||||||
if (!ext) ext = '.png';
|
|
||||||
const dest = 'icon' + ext;
|
function setCustomImage(id, sourcePath) {
|
||||||
fs.copyFileSync(sourcePath, path.join(instancePath(id), dest));
|
let ext = path.extname(sourcePath).toLowerCase();
|
||||||
return updateInstance(id, { customImage: dest });
|
if (!ext) ext = '.png';
|
||||||
}
|
const dest = 'icon' + ext;
|
||||||
|
fs.copyFileSync(sourcePath, path.join(instancePath(id), dest));
|
||||||
function clearCustomImage(id) {
|
return updateInstance(id, { customImage: dest });
|
||||||
const meta = getInstance(id);
|
}
|
||||||
if (meta && meta.customImage) {
|
|
||||||
try { fs.unlinkSync(path.join(instancePath(id), meta.customImage)); } catch { /* egal */ }
|
function clearCustomImage(id) {
|
||||||
}
|
const meta = getInstance(id);
|
||||||
return updateInstance(id, { customImage: null });
|
if (meta && meta.customImage) {
|
||||||
}
|
try { fs.unlinkSync(path.join(instancePath(id), meta.customImage)); } catch { /* egal */ }
|
||||||
|
}
|
||||||
function listInstances() {
|
return updateInstance(id, { customImage: null });
|
||||||
const dir = getInstancesDir();
|
}
|
||||||
ensureDir(dir);
|
|
||||||
const result = [];
|
function listInstances() {
|
||||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
const dir = getInstancesDir();
|
||||||
if (!entry.isDirectory()) continue;
|
ensureDir(dir);
|
||||||
const meta = readJson(path.join(dir, entry.name, 'instance.json'), null);
|
const result = [];
|
||||||
if (meta && meta.id) {
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||||
// eigenes Bild direkt für die Anzeige mitliefern (nicht persistiert)
|
if (!entry.isDirectory()) continue;
|
||||||
if (meta.customImage) meta.iconData = iconDataUri(meta.id);
|
const meta = readJson(path.join(dir, entry.name, 'instance.json'), null);
|
||||||
result.push(meta);
|
if (meta && meta.id) {
|
||||||
}
|
// eigenes Bild direkt für die Anzeige mitliefern (nicht persistiert)
|
||||||
}
|
if (meta.customImage) meta.iconData = iconDataUri(meta.id);
|
||||||
// Sortierung laut Einstellung: zuletzt gespielt oder nach Name
|
result.push(meta);
|
||||||
const byName = getSettings().sortBy === 'name';
|
}
|
||||||
result.sort((a, b) => {
|
}
|
||||||
if (byName) return a.name.localeCompare(b.name);
|
// Sortierung laut Einstellung: zuletzt gespielt oder nach Name
|
||||||
if (a.lastPlayed && b.lastPlayed) return b.lastPlayed.localeCompare(a.lastPlayed);
|
const byName = getSettings().sortBy === 'name';
|
||||||
if (a.lastPlayed) return -1;
|
result.sort((a, b) => {
|
||||||
if (b.lastPlayed) return 1;
|
if (byName) return a.name.localeCompare(b.name);
|
||||||
return a.name.localeCompare(b.name);
|
if (a.lastPlayed && b.lastPlayed) return b.lastPlayed.localeCompare(a.lastPlayed);
|
||||||
});
|
if (a.lastPlayed) return -1;
|
||||||
return result;
|
if (b.lastPlayed) return 1;
|
||||||
}
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
function getInstance(id) {
|
return result;
|
||||||
return readJson(instanceFile(id), null);
|
}
|
||||||
}
|
|
||||||
|
function getInstance(id) {
|
||||||
function normalizeInstanceName(name) {
|
return readJson(instanceFile(id), null);
|
||||||
return String(name || '').trim().toLowerCase();
|
}
|
||||||
}
|
|
||||||
|
function normalizeInstanceName(name) {
|
||||||
function findInstanceByName(name) {
|
return String(name || '').trim().toLowerCase();
|
||||||
const needle = normalizeInstanceName(name);
|
}
|
||||||
if (!needle) return null;
|
|
||||||
return listInstances().find((inst) => normalizeInstanceName(inst.name) === needle) || null;
|
function findInstanceByName(name) {
|
||||||
}
|
const needle = normalizeInstanceName(name);
|
||||||
|
if (!needle) return null;
|
||||||
// Ordnername aus dem Instanznamen ableiten (wie MultiMC), ungültige Zeichen raus
|
return listInstances().find((inst) => normalizeInstanceName(inst.name) === needle) || null;
|
||||||
function slugForName(name) {
|
}
|
||||||
const base = String(name || 'Instanz').trim()
|
|
||||||
.replace(/[<>:"/\\|?*\u0000-\u001f]/g, '')
|
// Ordnername aus dem Instanznamen ableiten (wie MultiMC), ungültige Zeichen raus
|
||||||
.replace(/[.\s]+$/, '')
|
function slugForName(name) {
|
||||||
.trim();
|
const base = String(name || 'Instanz').trim()
|
||||||
return (base || 'Instanz').slice(0, 60);
|
.replace(/[<>:"/\\|?*\u0000-\u001f]/g, '')
|
||||||
}
|
.replace(/[.\s]+$/, '')
|
||||||
// eindeutiger Ordnername (bei Kollision "Name 2", "Name 3", …)
|
.trim();
|
||||||
function uniqueFolderId(name) {
|
return (base || 'Instanz').slice(0, 60);
|
||||||
const dir = getInstancesDir();
|
}
|
||||||
const base = slugForName(name);
|
// eindeutiger Ordnername (bei Kollision "Name 2", "Name 3", …)
|
||||||
let id = base;
|
function uniqueFolderId(name) {
|
||||||
let n = 2;
|
const dir = getInstancesDir();
|
||||||
while (fs.existsSync(path.join(dir, id))) { id = base + ' ' + n; n += 1; }
|
const base = slugForName(name);
|
||||||
return id;
|
let id = base;
|
||||||
}
|
let n = 2;
|
||||||
|
while (fs.existsSync(path.join(dir, id))) { id = base + ' ' + n; n += 1; }
|
||||||
function applyInstanceData(inst, data) {
|
return id;
|
||||||
if (!data) return inst;
|
}
|
||||||
if (data.name) inst.name = data.name;
|
|
||||||
if (data.icon) inst.icon = data.icon;
|
function applyInstanceData(inst, data) {
|
||||||
if (data.minecraft) Object.assign(inst.minecraft, data.minecraft);
|
if (!data) return inst;
|
||||||
if (data.notes !== undefined) inst.notes = data.notes;
|
if (data.name) inst.name = data.name;
|
||||||
if (data.group !== undefined) inst.group = data.group;
|
if (data.icon) inst.icon = data.icon;
|
||||||
return inst;
|
if (data.minecraft) Object.assign(inst.minecraft, data.minecraft);
|
||||||
}
|
if (data.notes !== undefined) inst.notes = data.notes;
|
||||||
|
if (data.group !== undefined) inst.group = data.group;
|
||||||
function writeInstance(inst) {
|
return inst;
|
||||||
ensureDir(gameDir(inst.id));
|
}
|
||||||
writeJson(instanceFile(inst.id), inst);
|
|
||||||
return inst;
|
function writeInstance(inst) {
|
||||||
}
|
ensureDir(gameDir(inst.id));
|
||||||
|
writeJson(instanceFile(inst.id), inst);
|
||||||
function createInstance(data) {
|
return inst;
|
||||||
const inst = applyInstanceData(defaultInstance(data && data.name), data);
|
}
|
||||||
inst.id = uniqueFolderId(inst.name); // Ordner = Instanzname statt Zufalls-ID
|
|
||||||
return writeInstance(inst);
|
function createInstance(data) {
|
||||||
}
|
const inst = applyInstanceData(defaultInstance(data && data.name), data);
|
||||||
|
inst.id = uniqueFolderId(inst.name); // Ordner = Instanzname statt Zufalls-ID
|
||||||
function createOrReplaceImportedInstance(data) {
|
return writeInstance(inst);
|
||||||
const existing = findInstanceByName(data && data.name);
|
}
|
||||||
if (!existing) return createInstance(data);
|
|
||||||
|
function createOrReplaceImportedInstance(data) {
|
||||||
const inst = applyInstanceData(defaultInstance(data && data.name), data);
|
const existing = findInstanceByName(data && data.name);
|
||||||
inst.id = existing.id;
|
if (!existing) return createInstance(data);
|
||||||
inst.created = existing.created || inst.created;
|
|
||||||
inst.lastPlayed = existing.lastPlayed || null;
|
const inst = applyInstanceData(defaultInstance(data && data.name), data);
|
||||||
inst.totalPlaySeconds = existing.totalPlaySeconds || 0;
|
inst.id = existing.id;
|
||||||
|
inst.created = existing.created || inst.created;
|
||||||
const p = instancePath(existing.id);
|
inst.lastPlayed = existing.lastPlayed || null;
|
||||||
if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true });
|
inst.totalPlaySeconds = existing.totalPlaySeconds || 0;
|
||||||
return writeInstance(inst);
|
inst.launchCount = existing.launchCount || 0;
|
||||||
}
|
|
||||||
|
const p = instancePath(existing.id);
|
||||||
function updateInstance(id, patch) {
|
if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true });
|
||||||
const inst = getInstance(id);
|
return writeInstance(inst);
|
||||||
if (!inst) throw new Error('Instanz nicht gefunden: ' + id);
|
}
|
||||||
// flaches Merge mit Sonderbehandlung der verschachtelten Objekte
|
|
||||||
const next = Object.assign({}, inst, patch);
|
function updateInstance(id, patch) {
|
||||||
if (patch.minecraft) next.minecraft = Object.assign({}, inst.minecraft, patch.minecraft);
|
const inst = getInstance(id);
|
||||||
if (patch.java) next.java = Object.assign({}, inst.java, patch.java);
|
if (!inst) throw new Error('Instanz nicht gefunden: ' + id);
|
||||||
writeJson(instanceFile(id), next);
|
// flaches Merge mit Sonderbehandlung der verschachtelten Objekte
|
||||||
return next;
|
const next = Object.assign({}, inst, patch);
|
||||||
}
|
if (patch.minecraft) next.minecraft = Object.assign({}, inst.minecraft, patch.minecraft);
|
||||||
|
if (patch.java) next.java = Object.assign({}, inst.java, patch.java);
|
||||||
function deleteInstance(id) {
|
writeJson(instanceFile(id), next);
|
||||||
const p = instancePath(id);
|
return next;
|
||||||
if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true });
|
}
|
||||||
return true;
|
|
||||||
}
|
function deleteInstance(id) {
|
||||||
|
const p = instancePath(id);
|
||||||
function duplicateInstance(id) {
|
if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true });
|
||||||
const src = getInstance(id);
|
return true;
|
||||||
if (!src) throw new Error('Instanz nicht gefunden: ' + id);
|
}
|
||||||
const copy = JSON.parse(JSON.stringify(src));
|
|
||||||
copy.name = src.name + ' (Kopie)';
|
function duplicateInstance(id) {
|
||||||
copy.id = uniqueFolderId(copy.name);
|
const src = getInstance(id);
|
||||||
copy.created = new Date().toISOString();
|
if (!src) throw new Error('Instanz nicht gefunden: ' + id);
|
||||||
copy.lastPlayed = null;
|
const copy = JSON.parse(JSON.stringify(src));
|
||||||
copy.totalPlaySeconds = 0;
|
copy.name = src.name + ' (Kopie)';
|
||||||
ensureDir(gameDir(copy.id));
|
copy.id = uniqueFolderId(copy.name);
|
||||||
// Spielinhalte mitkopieren (Mods/Configs) – ohne Fehler wenn leer
|
copy.created = new Date().toISOString();
|
||||||
try {
|
copy.lastPlayed = null;
|
||||||
fs.cpSync(gameDir(id), gameDir(copy.id), { recursive: true });
|
copy.totalPlaySeconds = 0;
|
||||||
} catch { /* Quelle evtl. leer */ }
|
copy.launchCount = 0;
|
||||||
writeJson(instanceFile(copy.id), copy);
|
ensureDir(gameDir(copy.id));
|
||||||
return copy;
|
// Spielinhalte mitkopieren (Mods/Configs) – ohne Fehler wenn leer
|
||||||
}
|
try {
|
||||||
|
fs.cpSync(gameDir(id), gameDir(copy.id), { recursive: true });
|
||||||
module.exports = {
|
} catch { /* Quelle evtl. leer */ }
|
||||||
init,
|
writeJson(instanceFile(copy.id), copy);
|
||||||
getSettings,
|
return copy;
|
||||||
saveSettings,
|
}
|
||||||
getInstancesDir,
|
|
||||||
instancePath,
|
module.exports = {
|
||||||
gameDir,
|
init,
|
||||||
modsDir,
|
getSettings,
|
||||||
inhaltsDir,
|
saveSettings,
|
||||||
iconDataUri,
|
getInstancesDir,
|
||||||
setCustomImage,
|
instancePath,
|
||||||
clearCustomImage,
|
gameDir,
|
||||||
listMods,
|
modsDir,
|
||||||
addMod,
|
inhaltsDir,
|
||||||
removeMod,
|
iconDataUri,
|
||||||
findDependents,
|
setCustomImage,
|
||||||
setModEnabled,
|
clearCustomImage,
|
||||||
listInstances,
|
listMods,
|
||||||
getInstance,
|
addMod,
|
||||||
findInstanceByName,
|
removeMod,
|
||||||
createInstance,
|
findDependents,
|
||||||
createOrReplaceImportedInstance,
|
setModEnabled,
|
||||||
updateInstance,
|
listInstances,
|
||||||
deleteInstance,
|
getInstance,
|
||||||
duplicateInstance,
|
findInstanceByName,
|
||||||
};
|
createInstance,
|
||||||
|
createOrReplaceImportedInstance,
|
||||||
|
updateInstance,
|
||||||
|
deleteInstance,
|
||||||
|
duplicateInstance,
|
||||||
|
};
|
||||||
|
|||||||
@@ -391,6 +391,42 @@ img.account-av{object-fit:cover;image-rendering:auto}
|
|||||||
.tb-badge{display:inline-block;min-width:16px;height:16px;padding:0 4px;margin-left:6px;border-radius:8px;background:#d9453c;color:#fff;font-size:11px;font-weight:600;line-height:16px;text-align:center;box-sizing:border-box}
|
.tb-badge{display:inline-block;min-width:16px;height:16px;padding:0 4px;margin-left:6px;border-radius:8px;background:#d9453c;color:#fff;font-size:11px;font-weight:600;line-height:16px;text-align:center;box-sizing:border-box}
|
||||||
#modal-friends .field-row{margin-bottom:14px}
|
#modal-friends .field-row{margin-bottom:14px}
|
||||||
#modal-friends .field-row input{flex:1}
|
#modal-friends .field-row input{flex:1}
|
||||||
|
/* ---- Spielzeit-Statistik: Kennzahlen + Balken pro Instanz ---- */
|
||||||
|
:root{--stat-bar:#3a6ea5}
|
||||||
|
:root[data-theme="dark"]{--stat-bar:#3d8fe0}
|
||||||
|
.stats-tiles{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:6px}
|
||||||
|
.stat-tile{border:1px solid var(--border);border-radius:4px;padding:8px 10px;min-width:0}
|
||||||
|
.stat-tile .stat-label{font-size:11px;color:var(--dim)}
|
||||||
|
.stat-tile .stat-value{font-size:18px;font-weight:600;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
.stat-tile .stat-sub{font-size:11px;color:var(--dim);line-height:1.35}
|
||||||
|
.stats-list{display:flex;flex-direction:column;gap:2px}
|
||||||
|
.stat-row{display:grid;grid-template-columns:minmax(0,38%) 1fr auto;align-items:center;gap:10px;padding:5px 4px;border-radius:3px}
|
||||||
|
.stat-row:hover{background:var(--sel)}
|
||||||
|
.stat-name{min-width:0}
|
||||||
|
.stat-name b{display:block;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
.stat-name span{font-size:11px;color:var(--dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block}
|
||||||
|
.stat-track{height:8px}
|
||||||
|
.stat-bar{height:8px;background:var(--stat-bar);border-radius:0 4px 4px 0;min-width:2px}
|
||||||
|
.stat-value-cell{font-variant-numeric:tabular-nums;white-space:nowrap;text-align:right;min-width:92px}
|
||||||
|
.popout-host .stats-list{max-height:none}
|
||||||
|
|
||||||
|
/* Clip-Modus-Leiste im Aufzeichnungsfenster */
|
||||||
|
.clip-bar{display:flex;align-items:center;gap:14px;flex-wrap:wrap;padding:8px 12px;border:1px solid var(--border);border-radius:4px;background:var(--area)}
|
||||||
|
.clip-bar.aktiv{border-color:var(--accent);box-shadow:inset 3px 0 0 var(--accent)}
|
||||||
|
.clip-switch{display:inline-flex;align-items:center;gap:8px;cursor:pointer;font-size:13px;font-weight:600;user-select:none}
|
||||||
|
.clip-switch input{position:absolute;opacity:0;width:1px;height:1px;pointer-events:none}
|
||||||
|
.clip-switch-track{position:relative;width:32px;height:18px;border-radius:9px;background:var(--border);transition:background .15s}
|
||||||
|
.clip-switch-knob{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.35);transition:left .15s}
|
||||||
|
.clip-switch input:checked + .clip-switch-track{background:var(--accent)}
|
||||||
|
.clip-switch input:checked + .clip-switch-track .clip-switch-knob{left:16px}
|
||||||
|
.clip-switch input:focus-visible + .clip-switch-track{outline:2px solid var(--accent);outline-offset:2px}
|
||||||
|
.clip-length{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--dim)}
|
||||||
|
.clip-length select{padding:3px 6px;border:1px solid var(--border);border-radius:3px;background:var(--input,#fff);color:var(--text);font-family:inherit;font-size:12px}
|
||||||
|
.clip-length select:disabled{opacity:.6}
|
||||||
|
.clip-status{flex:1;min-width:200px;font-size:12px;color:var(--dim)}
|
||||||
|
.clip-bar.aktiv .clip-status{color:var(--text)}
|
||||||
|
.clip-bar.aktiv .clip-status::before{content:"";display:inline-block;width:8px;height:8px;margin-right:7px;border-radius:50%;background:var(--danger);vertical-align:1px;animation:rec-pulse 1.6s infinite}
|
||||||
|
|
||||||
.friends-section-title{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.03em;margin:10px 0 6px}
|
.friends-section-title{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.03em;margin:10px 0 6px}
|
||||||
.friend-row{display:flex;align-items:center;gap:10px;padding:6px 4px;border-radius:3px}
|
.friend-row{display:flex;align-items:center;gap:10px;padding:6px 4px;border-radius:3px}
|
||||||
.friend-row:hover{background:var(--sel,rgba(0,0,0,.04))}
|
.friend-row:hover{background:var(--sel,rgba(0,0,0,.04))}
|
||||||
|
|||||||
+4
-3
@@ -42,7 +42,7 @@ function computeInstallKind() {
|
|||||||
// 2. Notnagel über den Dateinamen, falls die Variablen einmal fehlen
|
// 2. Notnagel über den Dateinamen, falls die Variablen einmal fehlen
|
||||||
if (/portable/i.test(path.basename(exe))) return 'portable';
|
if (/portable/i.test(path.basename(exe))) return 'portable';
|
||||||
|
|
||||||
// 3. Setup: NSIS legt neben der EXE immer einen Uninstaller ab. Nur wenn der
|
// 3. Setup: das Setup legt neben der EXE immer "Uninstall AeroMC.exe" ab. Nur wenn der
|
||||||
// da ist, ist es wirklich eine Installation.
|
// da ist, ist es wirklich eine Installation.
|
||||||
if (hasUninstaller(path.dirname(exe))) return 'setup';
|
if (hasUninstaller(path.dirname(exe))) return 'setup';
|
||||||
|
|
||||||
@@ -319,7 +319,7 @@ exit /b 1
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* Wendet das geladene Update an.
|
* Wendet das geladene Update an.
|
||||||
* setup -> NSIS-Installer starten, er überschreibt die Installation selbst
|
* setup -> AeroMC-Setup mit --update starten, es überschreibt die Installation selbst
|
||||||
* portable -> Skript starten, das die laufende EXE nach dem Beenden ersetzt
|
* portable -> Skript starten, das die laufende EXE nach dem Beenden ersetzt
|
||||||
* Der Launcher muss sich danach beenden.
|
* Der Launcher muss sich danach beenden.
|
||||||
*/
|
*/
|
||||||
@@ -331,7 +331,8 @@ function applyUpdate(file) {
|
|||||||
const kind = installKind;
|
const kind = installKind;
|
||||||
|
|
||||||
if (kind !== 'portable') {
|
if (kind !== 'portable') {
|
||||||
const child = spawn(file.path, [], { detached: true, stdio: 'ignore' });
|
// --update: das AeroMC-Setup aktualisiert ohne Rückfragen und startet AeroMC danach neu
|
||||||
|
const child = spawn(file.path, ['--update'], { detached: true, stdio: 'ignore' });
|
||||||
child.unref();
|
child.unref();
|
||||||
return { started: 'installer' };
|
return { started: 'installer' };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
/**
|
/**
|
||||||
* Plugin Name: AeroMC Freunde
|
* Plugin Name: AeroMC Freunde
|
||||||
* Description: Backend für das Freundesystem des AeroMC Launchers - meldet, wer gerade online/im Spiel ist, und verwaltet Freundschaftsanfragen. Keine eigene Anmeldung: der Launcher meldet sich direkt mit der Minecraft-UUID des jeweiligen Kontos.
|
* Description: Backend für das Freundesystem des AeroMC Launchers - meldet, wer gerade online/im Spiel ist, und verwaltet Freundschaftsanfragen. Keine eigene Anmeldung: der Launcher meldet sich direkt mit der Minecraft-UUID des jeweiligen Kontos.
|
||||||
* Version: 1.0.0
|
* Version: 1.1.0
|
||||||
* Author: M_Viper
|
* Author: M_Viper
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -10,9 +10,10 @@ if (!defined('ABSPATH')) {
|
|||||||
exit; // kein direkter Aufruf der Datei
|
exit; // kein direkter Aufruf der Datei
|
||||||
}
|
}
|
||||||
|
|
||||||
define('AEROMC_FRIENDS_VERSION', '1.0.0');
|
define('AEROMC_FRIENDS_VERSION', '1.1.0');
|
||||||
define('AEROMC_FRIENDS_PRESENCE_TABLE', 'aeromc_presence');
|
define('AEROMC_FRIENDS_PRESENCE_TABLE', 'aeromc_presence');
|
||||||
define('AEROMC_FRIENDS_FRIENDS_TABLE', 'aeromc_friends');
|
define('AEROMC_FRIENDS_FRIENDS_TABLE', 'aeromc_friends');
|
||||||
|
define('AEROMC_FRIENDS_INVITES_TABLE', 'aeromc_invites');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Legt die beiden Tabellen an (bzw. bringt sie bei einem Update auf den
|
* Legt die beiden Tabellen an (bzw. bringt sie bei einem Update auf den
|
||||||
@@ -23,6 +24,7 @@ function aeromc_friends_install() {
|
|||||||
$charset_collate = $wpdb->get_charset_collate();
|
$charset_collate = $wpdb->get_charset_collate();
|
||||||
$presence_table = $wpdb->prefix . AEROMC_FRIENDS_PRESENCE_TABLE;
|
$presence_table = $wpdb->prefix . AEROMC_FRIENDS_PRESENCE_TABLE;
|
||||||
$friends_table = $wpdb->prefix . AEROMC_FRIENDS_FRIENDS_TABLE;
|
$friends_table = $wpdb->prefix . AEROMC_FRIENDS_FRIENDS_TABLE;
|
||||||
|
$invites_table = $wpdb->prefix . AEROMC_FRIENDS_INVITES_TABLE;
|
||||||
|
|
||||||
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||||
|
|
||||||
@@ -34,6 +36,7 @@ function aeromc_friends_install() {
|
|||||||
username varchar(32) NOT NULL,
|
username varchar(32) NOT NULL,
|
||||||
instance_name varchar(128) DEFAULT NULL,
|
instance_name varchar(128) DEFAULT NULL,
|
||||||
server_label varchar(128) DEFAULT NULL,
|
server_label varchar(128) DEFAULT NULL,
|
||||||
|
mc_version varchar(32) DEFAULT NULL,
|
||||||
last_seen datetime NOT NULL,
|
last_seen datetime NOT NULL,
|
||||||
PRIMARY KEY (uuid)
|
PRIMARY KEY (uuid)
|
||||||
) $charset_collate;";
|
) $charset_collate;";
|
||||||
@@ -50,7 +53,31 @@ function aeromc_friends_install() {
|
|||||||
KEY uuid_b (uuid_b)
|
KEY uuid_b (uuid_b)
|
||||||
) $charset_collate;";
|
) $charset_collate;";
|
||||||
dbDelta($sql_friends);
|
dbDelta($sql_friends);
|
||||||
|
|
||||||
|
// Einladungen "Komm auf meinen Server" - gelten nur ein paar Minuten
|
||||||
|
$sql_invites = "CREATE TABLE $invites_table (
|
||||||
|
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
from_uuid varchar(32) NOT NULL,
|
||||||
|
to_uuid varchar(32) NOT NULL,
|
||||||
|
server varchar(128) NOT NULL,
|
||||||
|
mc_version varchar(32) DEFAULT NULL,
|
||||||
|
created_at datetime NOT NULL,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY to_uuid (to_uuid)
|
||||||
|
) $charset_collate;";
|
||||||
|
dbDelta($sql_invites);
|
||||||
|
|
||||||
|
update_option('aeromc_friends_db_version', AEROMC_FRIENDS_VERSION);
|
||||||
}
|
}
|
||||||
register_activation_hook(__FILE__, 'aeromc_friends_install');
|
register_activation_hook(__FILE__, 'aeromc_friends_install');
|
||||||
|
|
||||||
|
// Beim Ersetzen der Plugin-Dateien (Update per Upload) läuft der
|
||||||
|
// Aktivierungs-Hook NICHT erneut - neue Spalten/Tabellen deshalb anlegen,
|
||||||
|
// sobald die gespeicherte DB-Version nicht mehr zur Plugin-Version passt.
|
||||||
|
add_action('plugins_loaded', function () {
|
||||||
|
if (get_option('aeromc_friends_db_version') !== AEROMC_FRIENDS_VERSION) {
|
||||||
|
aeromc_friends_install();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
require_once plugin_dir_path(__FILE__) . 'includes/routes.php';
|
require_once plugin_dir_path(__FILE__) . 'includes/routes.php';
|
||||||
|
|||||||
@@ -20,7 +20,38 @@ if (!defined('ABSPATH')) {
|
|||||||
define('AEROMC_FRIENDS_ONLINE_SECONDS', 180);
|
define('AEROMC_FRIENDS_ONLINE_SECONDS', 180);
|
||||||
// Häufigere Herzschläge als das werden ignoriert (Schutz gegen Missbrauch/
|
// Häufigere Herzschläge als das werden ignoriert (Schutz gegen Missbrauch/
|
||||||
// versehentliche Anfrageflut) - der Launcher selbst sendet ohnehin nur alle 60s.
|
// versehentliche Anfrageflut) - der Launcher selbst sendet ohnehin nur alle 60s.
|
||||||
|
// Ausnahme: hat sich der Server geändert, wird sofort gespeichert ("Beitreten").
|
||||||
define('AEROMC_FRIENDS_HEARTBEAT_MIN_GAP', 20);
|
define('AEROMC_FRIENDS_HEARTBEAT_MIN_GAP', 20);
|
||||||
|
// So lange ist eine Einladung "Komm auf meinen Server" gültig.
|
||||||
|
define('AEROMC_FRIENDS_INVITE_SECONDS', 600);
|
||||||
|
// Frühestens nach so vielen Sekunden darf derselbe Freund erneut eingeladen werden.
|
||||||
|
define('AEROMC_FRIENDS_INVITE_MIN_GAP', 30);
|
||||||
|
|
||||||
|
function aeromc_friends_invites_table() {
|
||||||
|
global $wpdb;
|
||||||
|
return $wpdb->prefix . AEROMC_FRIENDS_INVITES_TABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Server-Adresse (Host oder IP, optional :Port) - nur harmlose Zeichen. */
|
||||||
|
function aeromc_friends_valid_server($server) {
|
||||||
|
return is_string($server) && preg_match('/^[A-Za-z0-9.\-_:\[\]]{1,128}$/', $server);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Minecraft-Version wie "1.21.1", "24w14a" oder "1.20.1-pre2". */
|
||||||
|
function aeromc_friends_clean_version($version) {
|
||||||
|
$version = (string) $version;
|
||||||
|
return preg_match('/^[A-Za-z0-9.\-_ ]{1,32}$/', $version) ? $version : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sind die beiden befreundet (Anfrage angenommen)? */
|
||||||
|
function aeromc_friends_are_friends($a, $b) {
|
||||||
|
global $wpdb;
|
||||||
|
$friends_table = aeromc_friends_friends_table();
|
||||||
|
return (bool) $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT id FROM $friends_table WHERE ((uuid_a = %s AND uuid_b = %s) OR (uuid_a = %s AND uuid_b = %s)) AND status = 'accepted'",
|
||||||
|
$a, $b, $b, $a
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
function aeromc_friends_presence_table() {
|
function aeromc_friends_presence_table() {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
@@ -89,9 +120,24 @@ add_action('rest_api_init', function () {
|
|||||||
'callback' => 'aeromc_friends_route_remove',
|
'callback' => 'aeromc_friends_route_remove',
|
||||||
'permission_callback' => '__return_true',
|
'permission_callback' => '__return_true',
|
||||||
]);
|
]);
|
||||||
|
register_rest_route('aeromc/v1', '/friends/invite', [
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => 'aeromc_friends_route_invite',
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
]);
|
||||||
|
register_rest_route('aeromc/v1', '/friends/invites', [
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => 'aeromc_friends_route_invites',
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
]);
|
||||||
|
register_rest_route('aeromc/v1', '/friends/invites/dismiss', [
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => 'aeromc_friends_route_invite_dismiss',
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
/** POST /presence {uuid, username, instance, server} */
|
/** POST /presence {uuid, username, instance, server, version} */
|
||||||
function aeromc_friends_route_presence($request) {
|
function aeromc_friends_route_presence($request) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
||||||
@@ -101,12 +147,14 @@ function aeromc_friends_route_presence($request) {
|
|||||||
}
|
}
|
||||||
$instance = sanitize_text_field((string) $request->get_param('instance'));
|
$instance = sanitize_text_field((string) $request->get_param('instance'));
|
||||||
$server = sanitize_text_field((string) $request->get_param('server'));
|
$server = sanitize_text_field((string) $request->get_param('server'));
|
||||||
|
$version = aeromc_friends_clean_version($request->get_param('version'));
|
||||||
$table = aeromc_friends_presence_table();
|
$table = aeromc_friends_presence_table();
|
||||||
|
|
||||||
$letzte = $wpdb->get_var($wpdb->prepare(
|
$letzte = $wpdb->get_row($wpdb->prepare(
|
||||||
"SELECT last_seen FROM $table WHERE uuid = %s", $uuid
|
"SELECT last_seen, server_label FROM $table WHERE uuid = %s", $uuid
|
||||||
));
|
));
|
||||||
if ($letzte && (time() - strtotime($letzte)) < AEROMC_FRIENDS_HEARTBEAT_MIN_GAP) {
|
$server_gleich = $letzte && (string) $letzte->server_label === (string) $server;
|
||||||
|
if ($letzte && $server_gleich && (time() - strtotime($letzte->last_seen)) < AEROMC_FRIENDS_HEARTBEAT_MIN_GAP) {
|
||||||
return ['ok' => true]; // zu häufig - stillschweigend ignoriert
|
return ['ok' => true]; // zu häufig - stillschweigend ignoriert
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,11 +166,11 @@ function aeromc_friends_route_presence($request) {
|
|||||||
// Zeitzone praktisch nie als offline erkannt worden wäre.
|
// Zeitzone praktisch nie als offline erkannt worden wäre.
|
||||||
$jetzt_utc = current_time('mysql', true);
|
$jetzt_utc = current_time('mysql', true);
|
||||||
$wpdb->query($wpdb->prepare(
|
$wpdb->query($wpdb->prepare(
|
||||||
"INSERT INTO $table (uuid, username, instance_name, server_label, last_seen)
|
"INSERT INTO $table (uuid, username, instance_name, server_label, mc_version, last_seen)
|
||||||
VALUES (%s, %s, %s, %s, %s)
|
VALUES (%s, %s, %s, %s, %s, %s)
|
||||||
ON DUPLICATE KEY UPDATE username = %s, instance_name = %s, server_label = %s, last_seen = %s",
|
ON DUPLICATE KEY UPDATE username = %s, instance_name = %s, server_label = %s, mc_version = %s, last_seen = %s",
|
||||||
$uuid, $username, $instance ?: null, $server ?: null, $jetzt_utc,
|
$uuid, $username, $instance ?: null, $server ?: null, $version ?: null, $jetzt_utc,
|
||||||
$username, $instance ?: null, $server ?: null, $jetzt_utc
|
$username, $instance ?: null, $server ?: null, $version ?: null, $jetzt_utc
|
||||||
));
|
));
|
||||||
return ['ok' => true];
|
return ['ok' => true];
|
||||||
}
|
}
|
||||||
@@ -147,7 +195,7 @@ function aeromc_friends_route_list($request) {
|
|||||||
$ergebnis = [];
|
$ergebnis = [];
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
$p = $wpdb->get_row($wpdb->prepare(
|
$p = $wpdb->get_row($wpdb->prepare(
|
||||||
"SELECT username, instance_name, server_label, last_seen FROM $presence_table WHERE uuid = %s",
|
"SELECT username, instance_name, server_label, mc_version, last_seen FROM $presence_table WHERE uuid = %s",
|
||||||
$row->other_uuid
|
$row->other_uuid
|
||||||
));
|
));
|
||||||
$online = $p && (time() - strtotime($p->last_seen)) < AEROMC_FRIENDS_ONLINE_SECONDS;
|
$online = $p && (time() - strtotime($p->last_seen)) < AEROMC_FRIENDS_ONLINE_SECONDS;
|
||||||
@@ -157,6 +205,7 @@ function aeromc_friends_route_list($request) {
|
|||||||
'online' => (bool) $online,
|
'online' => (bool) $online,
|
||||||
'instance' => $online ? $p->instance_name : null,
|
'instance' => $online ? $p->instance_name : null,
|
||||||
'server' => $online ? $p->server_label : null,
|
'server' => $online ? $p->server_label : null,
|
||||||
|
'version' => $online ? $p->mc_version : null,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return $ergebnis;
|
return $ergebnis;
|
||||||
@@ -301,3 +350,87 @@ function aeromc_friends_route_remove($request) {
|
|||||||
));
|
));
|
||||||
return ['ok' => true];
|
return ['ok' => true];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** POST /friends/invite {uuid, targetUuid, server, version} - "Komm auf meinen Server" */
|
||||||
|
function aeromc_friends_route_invite($request) {
|
||||||
|
global $wpdb;
|
||||||
|
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
||||||
|
$target = aeromc_friends_normalize_uuid($request->get_param('targetUuid'));
|
||||||
|
$server = (string) $request->get_param('server');
|
||||||
|
$version = aeromc_friends_clean_version($request->get_param('version'));
|
||||||
|
if (!$uuid || !$target || !aeromc_friends_valid_server($server)) {
|
||||||
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid/targetUuid/server.', ['status' => 400]);
|
||||||
|
}
|
||||||
|
if (!aeromc_friends_are_friends($uuid, $target)) {
|
||||||
|
return new WP_Error('aeromc_not_friends', 'Ihr seid nicht befreundet.', ['status' => 403]);
|
||||||
|
}
|
||||||
|
$table = aeromc_friends_invites_table();
|
||||||
|
|
||||||
|
$letzte = $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT created_at FROM $table WHERE from_uuid = %s AND to_uuid = %s ORDER BY id DESC LIMIT 1",
|
||||||
|
$uuid, $target
|
||||||
|
));
|
||||||
|
if ($letzte && (time() - strtotime($letzte)) < AEROMC_FRIENDS_INVITE_MIN_GAP) {
|
||||||
|
return new WP_Error('aeromc_too_fast', 'Du hast diesen Freund gerade erst eingeladen.', ['status' => 429]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ältere Einladung an denselben Freund ersetzen
|
||||||
|
$wpdb->query($wpdb->prepare(
|
||||||
|
"DELETE FROM $table WHERE from_uuid = %s AND to_uuid = %s", $uuid, $target
|
||||||
|
));
|
||||||
|
$wpdb->insert($table, [
|
||||||
|
'from_uuid' => $uuid,
|
||||||
|
'to_uuid' => $target,
|
||||||
|
'server' => $server,
|
||||||
|
'mc_version' => $version ?: null,
|
||||||
|
'created_at' => current_time('mysql', true),
|
||||||
|
]);
|
||||||
|
return ['ok' => true];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GET /friends/invites?uuid= - an mich gerichtete, noch gültige Einladungen */
|
||||||
|
function aeromc_friends_route_invites($request) {
|
||||||
|
global $wpdb;
|
||||||
|
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
||||||
|
if (!$uuid) {
|
||||||
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid.', ['status' => 400]);
|
||||||
|
}
|
||||||
|
$table = aeromc_friends_invites_table();
|
||||||
|
$presence_table = aeromc_friends_presence_table();
|
||||||
|
|
||||||
|
// abgelaufene Einladungen (aller Spieler) wegräumen
|
||||||
|
$grenze = gmdate('Y-m-d H:i:s', time() - AEROMC_FRIENDS_INVITE_SECONDS);
|
||||||
|
$wpdb->query($wpdb->prepare("DELETE FROM $table WHERE created_at < %s", $grenze));
|
||||||
|
|
||||||
|
$rows = $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT i.id, i.from_uuid, p.username AS from_username, i.server, i.mc_version, i.created_at
|
||||||
|
FROM $table i
|
||||||
|
LEFT JOIN $presence_table p ON p.uuid = i.from_uuid
|
||||||
|
WHERE i.to_uuid = %s
|
||||||
|
ORDER BY i.id DESC",
|
||||||
|
$uuid
|
||||||
|
));
|
||||||
|
return array_map(function ($r) {
|
||||||
|
return [
|
||||||
|
'id' => (int) $r->id,
|
||||||
|
'fromUuid' => $r->from_uuid,
|
||||||
|
'fromUsername' => $r->from_username,
|
||||||
|
'server' => $r->server,
|
||||||
|
'version' => $r->mc_version,
|
||||||
|
'createdAt' => $r->created_at,
|
||||||
|
];
|
||||||
|
}, $rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST /friends/invites/dismiss {uuid, id} - Einladung erledigt/abgelehnt */
|
||||||
|
function aeromc_friends_route_invite_dismiss($request) {
|
||||||
|
global $wpdb;
|
||||||
|
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
||||||
|
$id = (int) $request->get_param('id');
|
||||||
|
if (!$uuid || $id <= 0) {
|
||||||
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid/id.', ['status' => 400]);
|
||||||
|
}
|
||||||
|
$table = aeromc_friends_invites_table();
|
||||||
|
$wpdb->query($wpdb->prepare("DELETE FROM $table WHERE id = %d AND to_uuid = %s", $id, $uuid));
|
||||||
|
return ['ok' => true];
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user