Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d268e548f5 | ||
|
|
ee0e6dec32 | ||
|
|
3b54217989 | ||
|
|
425b94fd93 | ||
|
|
e0bdcb01fb | ||
|
|
c840a49f75 | ||
|
|
c535d8b830 | ||
|
|
248a387f01 | ||
|
|
87fded4cb6 | ||
|
|
33e533009e | ||
|
|
17f8726075 | ||
|
|
deec93e5fa | ||
|
|
fc57dfa186 |
@@ -0,0 +1,8 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# beim Bauen erzeugt, enthält Adresse und Token der CurseForge-Weiterleitung
|
||||||
|
src/build-config.json
|
||||||
|
|
||||||
|
# Zugangsdaten des Weiterleitungs-Servers
|
||||||
|
server/curseforge-proxy/.env
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
AeroMC – Lizenz- und Nutzungsbedingungen
|
||||||
|
|
||||||
|
Copyright (c) M_Viper. Alle Rechte vorbehalten.
|
||||||
|
|
||||||
|
Diese Software („AeroMC") sowie sämtlicher zugehöriger Quellcode, Ressourcen und
|
||||||
|
Dokumentation sind urheberrechtlich geschützt. Mit der Nutzung dieser Software
|
||||||
|
erkennst du die folgenden Bedingungen an.
|
||||||
|
|
||||||
|
1. Erlaubte Nutzung
|
||||||
|
Die Nutzung von AeroMC ist ausschließlich für den privaten, nicht-kommerziellen
|
||||||
|
Gebrauch gestattet.
|
||||||
|
|
||||||
|
2. Untersagt ist insbesondere
|
||||||
|
- Die Weitergabe dieses Launchers oder von Teilen davon an Dritte
|
||||||
|
- Das Verändern, Dekompilieren, Zurückentwickeln (Reverse Engineering) oder
|
||||||
|
Bearbeiten des Codes
|
||||||
|
- Die Veröffentlichung – auch in veränderter oder abgeleiteter Form –
|
||||||
|
an anderer Stelle als den offiziellen Kanälen des Autors
|
||||||
|
- Der Verkauf sowie jede sonstige kommerzielle Nutzung
|
||||||
|
|
||||||
|
3. Keine Gewährleistung
|
||||||
|
Die Software wird ohne jegliche Gewährleistung bereitgestellt, weder
|
||||||
|
ausdrücklich noch stillschweigend, einschließlich, aber nicht beschränkt auf
|
||||||
|
die Gewährleistung der Marktgängigkeit, der Eignung für einen bestimmten
|
||||||
|
Zweck und der Nichtverletzung von Rechten Dritter. Der Autor haftet nicht für
|
||||||
|
Schäden, die aus der Nutzung oder Unmöglichkeit der Nutzung der Software
|
||||||
|
entstehen.
|
||||||
|
|
||||||
|
4. Rechtsfolgen bei Verstoß
|
||||||
|
Wer gegen diese Bedingungen verstößt, macht sich strafbar und kann rechtlich
|
||||||
|
belangt werden. Alle Rechte, die hier nicht ausdrücklich eingeräumt werden,
|
||||||
|
bleiben beim Autor vorbehalten.
|
||||||
|
|
||||||
|
5. Markenhinweis
|
||||||
|
AeroMC ist kein offizielles Produkt von Mojang oder Microsoft. Minecraft ist
|
||||||
|
eine Marke von Mojang AB.
|
||||||
|
|
||||||
|
Kontakt: https://m-viper.de
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
Ein Minecraft-Launcher für Windows mit getrennten Instanzen, Mod-Unterstützung und Microsoft-Anmeldung.
|
Ein Minecraft-Launcher für Windows mit getrennten Instanzen, Mod-Unterstützung und Microsoft-Anmeldung.
|
||||||
|
|
||||||
Aktueller Stand: Version 1.0.6
|
Aktueller Stand: Version 1.0.9
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ Der Launcher konzentriert sich auf den Alltag: Instanz anlegen, Konto auswählen
|
|||||||
- Warnt beim Entfernen eines Mods, falls ein anderer installierter Mod ihn noch als Pflicht-Abhängigkeit braucht
|
- Warnt beim Entfernen eines Mods, falls ein anderer installierter Mod ihn noch als Pflicht-Abhängigkeit braucht
|
||||||
- Globales Backup inkl. automatischer Zeitplan-Sicherungen, dazu Schnellsicherung einzelner Welten
|
- Globales Backup inkl. automatischer Zeitplan-Sicherungen, dazu Schnellsicherung einzelner Welten
|
||||||
- Welten, Ressourcenpakete, Datenpakete, Screenshots, Serverliste und Logdateien verwalten
|
- Welten, Ressourcenpakete, Datenpakete, Screenshots, Serverliste und Logdateien verwalten
|
||||||
- Serverliste mit Live-Status (Spieleranzahl, MOTD, Version) und Übernahme einzelner Einträge in andere Instanzen
|
- Serverliste mit Live-Status (Spieleranzahl, MOTD, Version), Übernahme einzelner Einträge in andere Instanzen sowie Desktop-Verknüpfung pro Server (startet die Instanz und verbindet direkt, ab MC 1.20)
|
||||||
- Online- und Offline-Start
|
- Online- und Offline-Start
|
||||||
- Eingebaute Konsole/Log-Ansicht inkl. Absturzberichte pro Instanz
|
- Eingebaute Konsole/Log-Ansicht inkl. Absturzberichte pro Instanz
|
||||||
- Windows-Benachrichtigung, wenn ein großer Download oder ein Backup im Hintergrund fertig wird
|
- Windows-Benachrichtigung, wenn ein großer Download oder ein Backup im Hintergrund fertig wird
|
||||||
@@ -45,6 +45,7 @@ Der Launcher konzentriert sich auf den Alltag: Instanz anlegen, Konto auswählen
|
|||||||
- Optionaler Wrapper-Befehl und HTTP-Proxy für Downloads
|
- Optionaler Wrapper-Befehl und HTTP-Proxy für Downloads
|
||||||
- Bedrock-Bereich für vorhandene Bedrock-Installation unter Windows, inkl. Installation zusätzlicher Add-ons
|
- Bedrock-Bereich für vorhandene Bedrock-Installation unter Windows, inkl. Installation zusätzlicher Add-ons
|
||||||
- Update-Suche läuft automatisch beim Start, dazu manuelle Prüfung in den Einstellungen
|
- Update-Suche läuft automatisch beim Start, dazu manuelle Prüfung in den Einstellungen
|
||||||
|
- Optionaler Autostart mit Windows, dann im Infobereich statt als Fenster
|
||||||
- Design Hell/Dunkel und Sortierung der Instanzen
|
- Design Hell/Dunkel und Sortierung der Instanzen
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
@@ -88,7 +89,7 @@ Jede Instanz hat:
|
|||||||
- Instanz und Notizen bearbeiten
|
- Instanz und Notizen bearbeiten
|
||||||
- Mods, Ressourcenpakete und Shader
|
- Mods, Ressourcenpakete und Shader
|
||||||
- Welten (inkl. Schnellsicherung einzelner Welten), Datenpakete und Bildschirmfotos
|
- Welten (inkl. Schnellsicherung einzelner Welten), Datenpakete und Bildschirmfotos
|
||||||
- Server verwalten (Live-Status, Übernahme eines Eintrags in andere Instanzen)
|
- Server verwalten (Live-Status, Übernahme eines Eintrags in andere Instanzen, Desktop-Verknüpfung pro Server)
|
||||||
- Logdateien und Absturzberichte ansehen
|
- Logdateien und Absturzberichte ansehen
|
||||||
- Minecraft-, Konfigurations- und Instanz-Ordner öffnen
|
- Minecraft-, Konfigurations- und Instanz-Ordner öffnen
|
||||||
- Desktop-Verknüpfung erstellen
|
- Desktop-Verknüpfung erstellen
|
||||||
@@ -174,6 +175,7 @@ Speicherort: `Dokumente\AeroMC Launcher\Backups`.
|
|||||||
- Instanz-Verzeichnis
|
- Instanz-Verzeichnis
|
||||||
- Bedrock-Bereich anzeigen
|
- Bedrock-Bereich anzeigen
|
||||||
- Windows-Benachrichtigung bei fertigem Hintergrund-Download/-Backup (nur wenn das Fenster nicht im Vordergrund ist)
|
- Windows-Benachrichtigung bei fertigem Hintergrund-Download/-Backup (nur wenn das Fenster nicht im Vordergrund ist)
|
||||||
|
- Autostart: mit Windows starten, im Infobereich statt als Fenster – solange aktiv, versteckt „Schließen" (X) den Launcher nur, statt ihn zu beenden (über das Symbol im Infobereich wieder öffnen oder wirklich beenden)
|
||||||
|
|
||||||
### Darstellung
|
### Darstellung
|
||||||
- Block-Symbole räumlich (3D)
|
- Block-Symbole räumlich (3D)
|
||||||
@@ -222,7 +224,7 @@ Nutzerdaten liegen lokal unter `%APPDATA%\aeromc`.
|
|||||||
## Links
|
## Links
|
||||||
|
|
||||||
- Webseite: https://m-viper.de
|
- Webseite: https://m-viper.de
|
||||||
- Gitseite: https://git.viper.ipv64.net
|
- Launcher-Webseite: https://aeromc.viper.ipv64.net
|
||||||
- Autor: M_Viper
|
- Autor: M_Viper
|
||||||
|
|
||||||
## Rechtliches
|
## Rechtliches
|
||||||
|
|||||||
Generated
+315
-10
@@ -1,15 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "aeromc",
|
"name": "aeromc",
|
||||||
"version": "1.0.6",
|
"version": "1.1.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "aeromc",
|
"name": "aeromc",
|
||||||
"version": "1.0.6",
|
"version": "1.1.3",
|
||||||
"license": "MIT",
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@xhayper/discord-rpc": "^1.3.4",
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
|
"ffmpeg-static": "^5.3.0",
|
||||||
|
"loopback-capture": "^3.0.2",
|
||||||
|
"node-window-manager": "^2.2.4",
|
||||||
"skinview3d": "^3.4.2",
|
"skinview3d": "^3.4.2",
|
||||||
"undici": "^6.28.0",
|
"undici": "^6.28.0",
|
||||||
"unzipper": "^0.12.5"
|
"unzipper": "^0.12.5"
|
||||||
@@ -19,6 +23,21 @@
|
|||||||
"electron-builder": "^24.0.0"
|
"electron-builder": "^24.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@derhuerst/http-basic": {
|
||||||
|
"version": "8.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@derhuerst/http-basic/-/http-basic-8.2.4.tgz",
|
||||||
|
"integrity": "sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"caseless": "^0.12.0",
|
||||||
|
"concat-stream": "^2.0.0",
|
||||||
|
"http-response-object": "^3.0.1",
|
||||||
|
"parse-cache-control": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@develar/schema-utils": {
|
"node_modules/@develar/schema-utils": {
|
||||||
"version": "2.6.5",
|
"version": "2.6.5",
|
||||||
"resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
|
"resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
|
||||||
@@ -37,6 +56,56 @@
|
|||||||
"url": "https://opencollective.com/webpack"
|
"url": "https://opencollective.com/webpack"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@discordjs/collection": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@discordjs/rest": {
|
||||||
|
"version": "2.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.3.tgz",
|
||||||
|
"integrity": "sha512-wvOylxNYJkwKjctS/Mn5GP1w9r3/rzyH+ThD1JlAca6zEdlHs8QWBBUQJpU5Q+W6DoIj/Ljh1IPlZs7hTU+UAg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@discordjs/collection": "^2.1.1",
|
||||||
|
"@discordjs/util": "^1.2.0",
|
||||||
|
"@sapphire/async-queue": "^1.5.3",
|
||||||
|
"@sapphire/snowflake": "^3.5.5",
|
||||||
|
"@vladfrangu/async_event_emitter": "^2.4.6",
|
||||||
|
"discord-api-types": "^0.38.50",
|
||||||
|
"magic-bytes.js": "^1.13.0",
|
||||||
|
"tslib": "^2.6.3",
|
||||||
|
"undici": "^6.27.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@discordjs/util": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"discord-api-types": "^0.38.33"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@electron/asar": {
|
"node_modules/@electron/asar": {
|
||||||
"version": "3.4.1",
|
"version": "3.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz",
|
||||||
@@ -494,6 +563,26 @@
|
|||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@sapphire/async-queue": {
|
||||||
|
"version": "1.5.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz",
|
||||||
|
"integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=v14.0.0",
|
||||||
|
"npm": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sapphire/snowflake": {
|
||||||
|
"version": "3.5.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.5.tgz",
|
||||||
|
"integrity": "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=v14.0.0",
|
||||||
|
"npm": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@sindresorhus/is": {
|
"node_modules/@sindresorhus/is": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
|
||||||
@@ -662,6 +751,31 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vladfrangu/async_event_emitter": {
|
||||||
|
"version": "2.4.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz",
|
||||||
|
"integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=v14.0.0",
|
||||||
|
"npm": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@xhayper/discord-rpc": {
|
||||||
|
"version": "1.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xhayper/discord-rpc/-/discord-rpc-1.3.4.tgz",
|
||||||
|
"integrity": "sha512-ff0uEXuibh9wi+l4vOj7xInLUjtlTaQBje/SCyQkeXZ0j2V0y+Zge5PQIQFRHH9TjjGaYJkTofEcQhncM2q7/w==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@discordjs/rest": "^2.6.1",
|
||||||
|
"@vladfrangu/async_event_emitter": "^2.4.7",
|
||||||
|
"discord-api-types": "^0.38.47",
|
||||||
|
"ws": "^8.20.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@xmldom/xmldom": {
|
"node_modules/@xmldom/xmldom": {
|
||||||
"version": "0.9.10",
|
"version": "0.9.10",
|
||||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz",
|
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz",
|
||||||
@@ -695,7 +809,6 @@
|
|||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "4"
|
"debug": "4"
|
||||||
@@ -1128,6 +1241,15 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/bindings": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"file-uri-to-path": "1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bl": {
|
"node_modules/bl": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||||
@@ -1243,7 +1365,6 @@
|
|||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/builder-util": {
|
"node_modules/builder-util": {
|
||||||
@@ -1366,6 +1487,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/caseless": {
|
||||||
|
"version": "0.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
|
||||||
|
"integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/chalk": {
|
"node_modules/chalk": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
@@ -1536,6 +1663,35 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/concat-stream": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
|
||||||
|
"engines": [
|
||||||
|
"node >= 6.0"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-from": "^1.0.0",
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"readable-stream": "^3.0.2",
|
||||||
|
"typedarray": "^0.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/concat-stream/node_modules/readable-stream": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"string_decoder": "^1.1.1",
|
||||||
|
"util-deprecate": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/config-file-ts": {
|
"node_modules/config-file-ts": {
|
||||||
"version": "0.2.6",
|
"version": "0.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.6.tgz",
|
||||||
@@ -1655,7 +1811,6 @@
|
|||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "^2.1.3"
|
"ms": "^2.1.3"
|
||||||
@@ -1799,6 +1954,15 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/discord-api-types": {
|
||||||
|
"version": "0.38.53",
|
||||||
|
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.53.tgz",
|
||||||
|
"integrity": "sha512-HL1zz/UuZ+bbJjA/X8Kbxx9gk8v9rJAbTeWRNYKmIdjwJ7EovjlHgoJTxcLpATfNJ+AonOtMdy3Y5MVIJAAd/A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"workspaces": [
|
||||||
|
"scripts/actions/documentation"
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/dmg-builder": {
|
"node_modules/dmg-builder": {
|
||||||
"version": "24.13.3",
|
"version": "24.13.3",
|
||||||
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.13.3.tgz",
|
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.13.3.tgz",
|
||||||
@@ -2372,7 +2536,6 @@
|
|||||||
"version": "2.2.1",
|
"version": "2.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
|
||||||
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
|
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
@@ -2493,6 +2656,22 @@
|
|||||||
"bare-events": "^2.7.0"
|
"bare-events": "^2.7.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/extract-file-icon": {
|
||||||
|
"version": "0.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/extract-file-icon/-/extract-file-icon-0.3.2.tgz",
|
||||||
|
"integrity": "sha512-We5/HaMDlioOx3c9zDjit8RtKWEvT94sJF8jTujaKVOyWL8Kkqu4QGnwDr5fID6XsbKvWT0JJNuWuV2Tmr7yiw==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"node-addon-api": "1.7.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/extract-file-icon/node_modules/node-addon-api": {
|
||||||
|
"version": "1.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.1.tgz",
|
||||||
|
"integrity": "sha512-2+DuKodWvwRTrCfKOeR24KIc5unKjOh8mz17NCzVnHWfjAdDqbfbjqh7gUT+BkXBRQM52+xCHciKWonJ3CbJMQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/extract-zip": {
|
"node_modules/extract-zip": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
||||||
@@ -2561,6 +2740,28 @@
|
|||||||
"integrity": "sha512-3JyEFWGjFn7zHmoa9+zG1BmW7X2okcmAB+0Cnu9UFbVs/jCBnl2A8o065ZlXiw145K3eBM3uLuzrYXC0RK7eDg==",
|
"integrity": "sha512-3JyEFWGjFn7zHmoa9+zG1BmW7X2okcmAB+0Cnu9UFbVs/jCBnl2A8o065ZlXiw145K3eBM3uLuzrYXC0RK7eDg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/ffmpeg-static": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ffmpeg-static/-/ffmpeg-static-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-H+K6sW6TiIX6VGend0KQwthe+kaceeH/luE8dIZyOP35ik7ahYojDuqlTV1bOrtEwl01sy2HFNGQfi5IDJvotg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "GPL-3.0-or-later",
|
||||||
|
"dependencies": {
|
||||||
|
"@derhuerst/http-basic": "^8.2.0",
|
||||||
|
"env-paths": "^2.2.0",
|
||||||
|
"https-proxy-agent": "^5.0.0",
|
||||||
|
"progress": "^2.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/file-uri-to-path": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/filelist": {
|
"node_modules/filelist": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz",
|
||||||
@@ -2978,6 +3179,21 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/http-response-object": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "^10.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/http-response-object/node_modules/@types/node": {
|
||||||
|
"version": "10.17.60",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
|
||||||
|
"integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/http2-wrapper": {
|
"node_modules/http2-wrapper": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
|
||||||
@@ -2996,7 +3212,6 @@
|
|||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||||
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"agent-base": "6",
|
"agent-base": "6",
|
||||||
@@ -3340,6 +3555,25 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"node_modules/loopback-capture": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/loopback-capture/-/loopback-capture-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-65qyF9L7e+InnbEZP3Ic4C4J+SszoxEq84XpIZWJYH3/IgUpTKqBzJ4wCF+ShL3bvqV7I2wOEgQv9VfpaO2AMw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"os": [
|
||||||
|
"linux",
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"bindings": "^1.5.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.17.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lowercase-keys": {
|
"node_modules/lowercase-keys": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
|
||||||
@@ -3363,6 +3597,12 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/magic-bytes.js": {
|
||||||
|
"version": "1.13.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.13.1.tgz",
|
||||||
|
"integrity": "sha512-x5sn4UX2k5gCWlcfmoFwG4TPie8+dctESyqOBdhB5p6MsgWXdBKGmt9nXPObj/JI50TTL928lc5Yt1WntMn1bw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/matcher": {
|
"node_modules/matcher": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
|
||||||
@@ -3514,7 +3754,6 @@
|
|||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/node-addon-api": {
|
"node_modules/node-addon-api": {
|
||||||
@@ -3525,12 +3764,41 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/node-gyp-build": {
|
||||||
|
"version": "4.8.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||||
|
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"node-gyp-build": "bin.js",
|
||||||
|
"node-gyp-build-optional": "optional.js",
|
||||||
|
"node-gyp-build-test": "build-test.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-int64": {
|
"node_modules/node-int64": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
||||||
"integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==",
|
"integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/node-window-manager": {
|
||||||
|
"version": "2.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-window-manager/-/node-window-manager-2.2.4.tgz",
|
||||||
|
"integrity": "sha512-Dgq9a7qx6+Gi+BifWM79leteNz76QUtwSsdiqEe7SjvoJsc8TYQsRl1IA5uxqz5iSLeHS1iA/8YI/snoq0DblA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"extract-file-icon": "^0.3.2",
|
||||||
|
"node-addon-api": "2.0.0",
|
||||||
|
"node-gyp-build": "^4.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-window-manager/node_modules/node-addon-api": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-ASCL5U13as7HhOExbT6OlWJJUV/lLzL2voOSP1UVehpRD8FbSrSDjfScK/KwAvVTI5AS6r4VwbOMlIqtvRidnA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/normalize-path": {
|
"node_modules/normalize-path": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||||
@@ -3590,6 +3858,11 @@
|
|||||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||||
"license": "BlueOak-1.0.0"
|
"license": "BlueOak-1.0.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/parse-cache-control": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg=="
|
||||||
|
},
|
||||||
"node_modules/path-is-absolute": {
|
"node_modules/path-is-absolute": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||||
@@ -3679,7 +3952,6 @@
|
|||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||||
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
|
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
@@ -4318,6 +4590,12 @@
|
|||||||
"utf8-byte-length": "^1.0.1"
|
"utf8-byte-length": "^1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
"node_modules/type-fest": {
|
"node_modules/type-fest": {
|
||||||
"version": "0.13.1",
|
"version": "0.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
|
||||||
@@ -4332,6 +4610,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/typedarray": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
@@ -4517,6 +4801,27 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.21.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
|
||||||
|
"integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/xmlbuilder": {
|
"node_modules/xmlbuilder": {
|
||||||
"version": "15.1.1",
|
"version": "15.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
|
||||||
|
|||||||
+15
-7
@@ -1,16 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "aeromc",
|
"name": "aeromc",
|
||||||
"productName": "AeroMC",
|
"productName": "AeroMC",
|
||||||
"version": "1.0.6",
|
"version": "1.1.3",
|
||||||
"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",
|
||||||
"main": "src/main.js",
|
"main": "src/main.js",
|
||||||
"author": "M_Viper",
|
"author": "M_Viper",
|
||||||
"repository": {
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
"type": "git",
|
"private": true,
|
||||||
"url": "https://git.viper.ipv64.net"
|
|
||||||
},
|
|
||||||
"license": "MIT",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron .",
|
"start": "electron .",
|
||||||
"prebuild": "node scripts/write-build-config.js",
|
"prebuild": "node scripts/write-build-config.js",
|
||||||
@@ -25,7 +23,13 @@
|
|||||||
"compression": "store",
|
"compression": "store",
|
||||||
"files": [
|
"files": [
|
||||||
"src/**/*",
|
"src/**/*",
|
||||||
"package.json"
|
"package.json",
|
||||||
|
"LICENSE"
|
||||||
|
],
|
||||||
|
"asarUnpack": [
|
||||||
|
"node_modules/loopback-capture/**/*",
|
||||||
|
"node_modules/node-window-manager/**/*",
|
||||||
|
"node_modules/ffmpeg-static/**/*"
|
||||||
],
|
],
|
||||||
"win": {
|
"win": {
|
||||||
"target": "nsis",
|
"target": "nsis",
|
||||||
@@ -41,7 +45,11 @@
|
|||||||
"electron-builder": "^24.0.0"
|
"electron-builder": "^24.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@xhayper/discord-rpc": "^1.3.4",
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
|
"ffmpeg-static": "^5.3.0",
|
||||||
|
"loopback-capture": "^3.0.2",
|
||||||
|
"node-window-manager": "^2.2.4",
|
||||||
"skinview3d": "^3.4.2",
|
"skinview3d": "^3.4.2",
|
||||||
"undici": "^6.28.0",
|
"undici": "^6.28.0",
|
||||||
"unzipper": "^0.12.5"
|
"unzipper": "^0.12.5"
|
||||||
|
|||||||
+451
-439
@@ -1,440 +1,452 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const archiver = require('archiver');
|
const archiver = require('archiver');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { pipeline } = require('stream/promises');
|
const { pipeline } = require('stream/promises');
|
||||||
const unzipper = require('unzipper');
|
const unzipper = require('unzipper');
|
||||||
|
|
||||||
function pad2(value) {
|
function pad2(value) {
|
||||||
return String(value).padStart(2, '0');
|
return String(value).padStart(2, '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultBackupFileName(date = new Date()) {
|
function defaultBackupFileName(date = new Date()) {
|
||||||
return 'AeroMC-Backup-' +
|
return 'AeroMC-Backup-' +
|
||||||
date.getFullYear() + '-' +
|
date.getFullYear() + '-' +
|
||||||
pad2(date.getMonth() + 1) + '-' +
|
pad2(date.getMonth() + 1) + '-' +
|
||||||
pad2(date.getDate()) + '_' +
|
pad2(date.getDate()) + '_' +
|
||||||
pad2(date.getHours()) + '-' +
|
pad2(date.getHours()) + '-' +
|
||||||
pad2(date.getMinutes()) + '-' +
|
pad2(date.getMinutes()) + '-' +
|
||||||
pad2(date.getSeconds()) +
|
pad2(date.getSeconds()) +
|
||||||
'.zip';
|
'.zip';
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultBackupDir(documentsDir) {
|
function defaultBackupDir(documentsDir) {
|
||||||
return path.join(documentsDir, 'AeroMC Launcher', 'Backups');
|
return path.join(documentsDir, 'AeroMC Launcher', 'Backups');
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveAutoBackupPath(documentsDir, date = new Date()) {
|
function resolveAutoBackupPath(documentsDir, date = new Date()) {
|
||||||
const dir = defaultBackupDir(documentsDir);
|
const dir = defaultBackupDir(documentsDir);
|
||||||
ensureDir(dir);
|
ensureDir(dir);
|
||||||
|
|
||||||
const baseName = defaultBackupFileName(date);
|
const baseName = defaultBackupFileName(date);
|
||||||
const ext = path.extname(baseName);
|
const ext = path.extname(baseName);
|
||||||
const stem = baseName.slice(0, -ext.length);
|
const stem = baseName.slice(0, -ext.length);
|
||||||
let attempt = 0;
|
let attempt = 0;
|
||||||
let candidate = path.join(dir, baseName);
|
let candidate = path.join(dir, baseName);
|
||||||
|
|
||||||
while (fs.existsSync(candidate)) {
|
while (fs.existsSync(candidate)) {
|
||||||
attempt += 1;
|
attempt += 1;
|
||||||
candidate = path.join(dir, `${stem}-${attempt}${ext}`);
|
candidate = path.join(dir, `${stem}-${attempt}${ext}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return candidate;
|
return candidate;
|
||||||
}
|
}
|
||||||
|
|
||||||
function listGlobalBackups(documentsDir) {
|
function listGlobalBackups(documentsDir) {
|
||||||
const dir = defaultBackupDir(documentsDir);
|
const dir = defaultBackupDir(documentsDir);
|
||||||
ensureDir(dir);
|
ensureDir(dir);
|
||||||
|
|
||||||
return fs.readdirSync(dir, { withFileTypes: true })
|
return fs.readdirSync(dir, { withFileTypes: true })
|
||||||
.filter((entry) => entry.isFile() && /\.zip$/i.test(entry.name) && !/\.partial\.zip$/i.test(entry.name))
|
.filter((entry) => entry.isFile() && /\.zip$/i.test(entry.name) && !/\.partial\.zip$/i.test(entry.name))
|
||||||
.map((entry) => {
|
.map((entry) => {
|
||||||
const filePath = path.join(dir, entry.name);
|
const filePath = path.join(dir, entry.name);
|
||||||
const stat = fs.statSync(filePath);
|
const stat = fs.statSync(filePath);
|
||||||
return {
|
return {
|
||||||
name: entry.name,
|
name: entry.name,
|
||||||
path: filePath,
|
path: filePath,
|
||||||
size: stat.size,
|
size: stat.size,
|
||||||
modifiedAt: stat.mtime.toISOString(),
|
modifiedAt: stat.mtime.toISOString(),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const timeDiff = new Date(b.modifiedAt).getTime() - new Date(a.modifiedAt).getTime();
|
const timeDiff = new Date(b.modifiedAt).getTime() - new Date(a.modifiedAt).getTime();
|
||||||
return timeDiff || a.name.localeCompare(b.name, 'de');
|
return timeDiff || a.name.localeCompare(b.name, 'de');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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 countInstanceDirs(instancesDir) {
|
function countInstanceDirs(instancesDir) {
|
||||||
if (!fs.existsSync(instancesDir)) return 0;
|
if (!fs.existsSync(instancesDir)) return 0;
|
||||||
return fs.readdirSync(instancesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).length;
|
return fs.readdirSync(instancesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
function readJson(file, fallback) {
|
function readJson(file, fallback) {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
|
return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
|
||||||
} 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 emitProgress(cb, mode, percent, detail) {
|
function emitProgress(cb, mode, percent, detail) {
|
||||||
if (!cb) return;
|
if (!cb) return;
|
||||||
cb({ mode, percent: Math.max(0, Math.min(100, Math.round(percent))), detail: detail || '' });
|
cb({ mode, percent: Math.max(0, Math.min(100, Math.round(percent))), detail: detail || '' });
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeZipEntryPath(entryPath) {
|
function normalizeZipEntryPath(entryPath) {
|
||||||
const parts = String(entryPath || '')
|
const parts = String(entryPath || '')
|
||||||
.replace(/\\/g, '/')
|
.replace(/\\/g, '/')
|
||||||
.split('/')
|
.split('/')
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
if (!parts.length) return '';
|
if (!parts.length) return '';
|
||||||
if (parts.some((part) => part === '.' || part === '..')) {
|
if (parts.some((part) => part === '.' || part === '..')) {
|
||||||
throw new Error('Das Backup-Archiv enthält ungültige Pfade.');
|
throw new Error('Das Backup-Archiv enthält ungültige Pfade.');
|
||||||
}
|
}
|
||||||
|
|
||||||
return path.join(...parts);
|
return path.join(...parts);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function extractBackupArchive(sourcePath, destinationDir, options) {
|
async function extractBackupArchive(sourcePath, destinationDir, options) {
|
||||||
const opts = options || {};
|
const opts = options || {};
|
||||||
const onProgress = opts.onProgress;
|
const onProgress = opts.onProgress;
|
||||||
const mode = opts.mode || 'restore';
|
const mode = opts.mode || 'restore';
|
||||||
const startPercent = opts.startPercent ?? 5;
|
const startPercent = opts.startPercent ?? 5;
|
||||||
const endPercent = opts.endPercent ?? 18;
|
const endPercent = opts.endPercent ?? 18;
|
||||||
const detail = opts.detail || 'Entpacke Backup';
|
const detail = opts.detail || 'Entpacke Backup';
|
||||||
const directory = await unzipper.Open.file(sourcePath);
|
const directory = await unzipper.Open.file(sourcePath);
|
||||||
const files = directory.files || [];
|
const files = directory.files || [];
|
||||||
const fileEntries = files.filter((entry) => entry.type !== 'Directory');
|
const fileEntries = files.filter((entry) => entry.type !== 'Directory');
|
||||||
const totalBytes = fileEntries.reduce((sum, entry) => sum + Number(entry.uncompressedSize || 0), 0);
|
const totalBytes = fileEntries.reduce((sum, entry) => sum + Number(entry.uncompressedSize || 0), 0);
|
||||||
let processedBytes = 0;
|
let processedBytes = 0;
|
||||||
let processedEntries = 0;
|
let processedEntries = 0;
|
||||||
|
|
||||||
ensureDir(destinationDir);
|
ensureDir(destinationDir);
|
||||||
if (!files.length) {
|
if (!files.length) {
|
||||||
emitProgress(onProgress, mode, endPercent, detail + ' abgeschlossen');
|
emitProgress(onProgress, mode, endPercent, detail + ' abgeschlossen');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const entry of files) {
|
for (const entry of files) {
|
||||||
const relativePath = normalizeZipEntryPath(entry.path);
|
const relativePath = normalizeZipEntryPath(entry.path);
|
||||||
if (!relativePath) continue;
|
if (!relativePath) continue;
|
||||||
|
|
||||||
const targetPath = path.join(destinationDir, relativePath);
|
const targetPath = path.join(destinationDir, relativePath);
|
||||||
if (entry.type === 'Directory') {
|
if (entry.type === 'Directory') {
|
||||||
ensureDir(targetPath);
|
ensureDir(targetPath);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureDir(path.dirname(targetPath));
|
ensureDir(path.dirname(targetPath));
|
||||||
await pipeline(entry.stream(), fs.createWriteStream(targetPath));
|
await pipeline(entry.stream(), fs.createWriteStream(targetPath));
|
||||||
|
|
||||||
processedBytes += Number(entry.uncompressedSize || 0);
|
processedBytes += Number(entry.uncompressedSize || 0);
|
||||||
processedEntries += 1;
|
processedEntries += 1;
|
||||||
const ratio = totalBytes > 0
|
const ratio = totalBytes > 0
|
||||||
? Math.max(0, Math.min(1, processedBytes / totalBytes))
|
? Math.max(0, Math.min(1, processedBytes / totalBytes))
|
||||||
: Math.max(0, Math.min(1, processedEntries / Math.max(1, fileEntries.length)));
|
: Math.max(0, Math.min(1, processedEntries / Math.max(1, fileEntries.length)));
|
||||||
const percent = startPercent + ((endPercent - startPercent) * ratio);
|
const percent = startPercent + ((endPercent - startPercent) * ratio);
|
||||||
emitProgress(onProgress, mode, percent, detail + ' ...');
|
emitProgress(onProgress, mode, percent, detail + ' ...');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createBackupArchive(options) {
|
function createBackupArchive(options) {
|
||||||
const opts = options || {};
|
const opts = options || {};
|
||||||
const instancesDir = opts.instancesDir;
|
const instancesDir = opts.instancesDir;
|
||||||
const settingsFile = opts.settingsFile;
|
const settingsFile = opts.settingsFile;
|
||||||
const includeSettings = opts.includeSettings !== false;
|
const includeSettings = opts.includeSettings !== false;
|
||||||
const backupInfo = opts.backupInfo || {};
|
const backupInfo = opts.backupInfo || {};
|
||||||
const destinationPath = opts.destinationPath;
|
const destinationPath = opts.destinationPath;
|
||||||
const onProgress = opts.onProgress;
|
const onProgress = opts.onProgress;
|
||||||
const mode = opts.mode || 'create';
|
const mode = opts.mode || 'create';
|
||||||
const startPercent = opts.startPercent ?? 15;
|
const startPercent = opts.startPercent ?? 15;
|
||||||
const endPercent = opts.endPercent ?? 98;
|
const endPercent = opts.endPercent ?? 98;
|
||||||
const detail = opts.detail || 'Packe ZIP-Archiv';
|
const detail = opts.detail || 'Packe ZIP-Archiv';
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
ensureDir(path.dirname(destinationPath));
|
ensureDir(path.dirname(destinationPath));
|
||||||
|
|
||||||
const output = fs.createWriteStream(destinationPath);
|
const output = fs.createWriteStream(destinationPath);
|
||||||
const archive = archiver('zip', { store: true });
|
const archive = archiver('zip', { store: true });
|
||||||
let done = false;
|
let done = false;
|
||||||
|
|
||||||
const fail = (err) => {
|
const fail = (err) => {
|
||||||
if (done) return;
|
if (done) return;
|
||||||
done = true;
|
done = true;
|
||||||
try { archive.destroy(); } catch { /* ignore */ }
|
try { archive.destroy(); } catch { /* ignore */ }
|
||||||
try { output.destroy(); } catch { /* ignore */ }
|
try { output.destroy(); } catch { /* ignore */ }
|
||||||
reject(err);
|
reject(err);
|
||||||
};
|
};
|
||||||
|
|
||||||
output.on('close', () => {
|
output.on('close', () => {
|
||||||
if (done) return;
|
if (done) return;
|
||||||
done = true;
|
done = true;
|
||||||
resolve({ bytes: archive.pointer() });
|
resolve({ bytes: archive.pointer() });
|
||||||
});
|
});
|
||||||
output.on('error', fail);
|
output.on('error', fail);
|
||||||
archive.on('error', fail);
|
archive.on('error', fail);
|
||||||
archive.on('warning', (err) => {
|
archive.on('warning', (err) => {
|
||||||
if (err && err.code === 'ENOENT') return;
|
if (err && err.code === 'ENOENT') return;
|
||||||
fail(err);
|
fail(err);
|
||||||
});
|
});
|
||||||
archive.on('progress', (progress) => {
|
archive.on('progress', (progress) => {
|
||||||
const totalBytes = progress && progress.fs ? progress.fs.totalBytes : 0;
|
const totalBytes = progress && progress.fs ? progress.fs.totalBytes : 0;
|
||||||
const processedBytes = progress && progress.fs ? progress.fs.processedBytes : 0;
|
const processedBytes = progress && progress.fs ? progress.fs.processedBytes : 0;
|
||||||
if (!totalBytes) return;
|
if (!totalBytes) return;
|
||||||
|
|
||||||
const ratio = Math.max(0, Math.min(1, processedBytes / totalBytes));
|
const ratio = Math.max(0, Math.min(1, processedBytes / totalBytes));
|
||||||
const percent = startPercent + ((endPercent - startPercent) * ratio);
|
const percent = startPercent + ((endPercent - startPercent) * ratio);
|
||||||
emitProgress(onProgress, mode, percent, detail + ' ...');
|
emitProgress(onProgress, mode, percent, detail + ' ...');
|
||||||
});
|
});
|
||||||
|
|
||||||
archive.pipe(output);
|
archive.pipe(output);
|
||||||
if (fs.existsSync(instancesDir)) archive.directory(instancesDir, 'instances');
|
if (fs.existsSync(instancesDir)) archive.directory(instancesDir, 'instances');
|
||||||
else archive.append('', { name: 'instances/.keep' });
|
else archive.append('', { name: 'instances/.keep' });
|
||||||
|
|
||||||
if (includeSettings && settingsFile && fs.existsSync(settingsFile)) {
|
if (includeSettings && settingsFile && fs.existsSync(settingsFile)) {
|
||||||
archive.file(settingsFile, { name: 'settings.json' });
|
archive.file(settingsFile, { name: 'settings.json' });
|
||||||
}
|
}
|
||||||
|
|
||||||
archive.append(`${JSON.stringify(backupInfo, null, 2)}\n`, { name: 'backup-info.json' });
|
archive.append(`${JSON.stringify(backupInfo, null, 2)}\n`, { name: 'backup-info.json' });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const finalizeResult = archive.finalize();
|
const finalizeResult = archive.finalize();
|
||||||
if (finalizeResult && typeof finalizeResult.catch === 'function') finalizeResult.catch(fail);
|
if (finalizeResult && typeof finalizeResult.catch === 'function') finalizeResult.catch(fail);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
fail(err);
|
fail(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyDirContentsTracked(sourceDir, targetDir, options) {
|
function copyDirContentsTracked(sourceDir, targetDir, options) {
|
||||||
const opts = options || {};
|
const opts = options || {};
|
||||||
const mode = opts.mode || 'create';
|
const mode = opts.mode || 'create';
|
||||||
const onProgress = opts.onProgress;
|
const onProgress = opts.onProgress;
|
||||||
const startPercent = opts.startPercent ?? 0;
|
const startPercent = opts.startPercent ?? 0;
|
||||||
const endPercent = opts.endPercent ?? 100;
|
const endPercent = opts.endPercent ?? 100;
|
||||||
const detailPrefix = opts.detailPrefix || 'Kopiere';
|
const detailPrefix = opts.detailPrefix || 'Kopiere';
|
||||||
|
|
||||||
ensureDir(targetDir);
|
ensureDir(targetDir);
|
||||||
if (!fs.existsSync(sourceDir)) {
|
if (!fs.existsSync(sourceDir)) {
|
||||||
emitProgress(onProgress, mode, endPercent, detailPrefix + ' abgeschlossen');
|
emitProgress(onProgress, mode, endPercent, detailPrefix + ' abgeschlossen');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
|
const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
|
||||||
if (!entries.length) {
|
if (!entries.length) {
|
||||||
emitProgress(onProgress, mode, endPercent, detailPrefix + ' abgeschlossen');
|
emitProgress(onProgress, mode, endPercent, detailPrefix + ' abgeschlossen');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
entries.forEach((entry, index) => {
|
entries.forEach((entry, index) => {
|
||||||
fs.cpSync(path.join(sourceDir, entry.name), path.join(targetDir, entry.name), { recursive: true });
|
fs.cpSync(path.join(sourceDir, entry.name), path.join(targetDir, entry.name), { recursive: true });
|
||||||
const ratio = (index + 1) / entries.length;
|
const ratio = (index + 1) / entries.length;
|
||||||
const percent = startPercent + ((endPercent - startPercent) * ratio);
|
const percent = startPercent + ((endPercent - startPercent) * ratio);
|
||||||
emitProgress(onProgress, mode, percent, `${detailPrefix}: ${entry.name}`);
|
emitProgress(onProgress, mode, percent, `${detailPrefix}: ${entry.name}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveDirTracked(sourceDir, targetDir, options) {
|
function moveDirTracked(sourceDir, targetDir, options) {
|
||||||
const opts = options || {};
|
const opts = options || {};
|
||||||
const mode = opts.mode || 'restore';
|
const mode = opts.mode || 'restore';
|
||||||
const onProgress = opts.onProgress;
|
const onProgress = opts.onProgress;
|
||||||
const startPercent = opts.startPercent ?? 0;
|
const startPercent = opts.startPercent ?? 0;
|
||||||
const endPercent = opts.endPercent ?? 100;
|
const endPercent = opts.endPercent ?? 100;
|
||||||
const detailPrefix = opts.detailPrefix || 'Verschiebe';
|
const detailPrefix = opts.detailPrefix || 'Verschiebe';
|
||||||
|
|
||||||
if (!fs.existsSync(sourceDir)) {
|
if (!fs.existsSync(sourceDir)) {
|
||||||
emitProgress(onProgress, mode, endPercent, detailPrefix + ' abgeschlossen');
|
emitProgress(onProgress, mode, endPercent, detailPrefix + ' abgeschlossen');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureDir(path.dirname(targetDir));
|
ensureDir(path.dirname(targetDir));
|
||||||
try {
|
try {
|
||||||
fs.renameSync(sourceDir, targetDir);
|
fs.renameSync(sourceDir, targetDir);
|
||||||
emitProgress(onProgress, mode, endPercent, detailPrefix + ' abgeschlossen');
|
emitProgress(onProgress, mode, endPercent, detailPrefix + ' abgeschlossen');
|
||||||
return;
|
return;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!err || !['EXDEV', 'EPERM', 'EACCES'].includes(err.code)) throw err;
|
if (!err || !['EXDEV', 'EPERM', 'EACCES'].includes(err.code)) throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
copyDirContentsTracked(sourceDir, targetDir, opts);
|
copyDirContentsTracked(sourceDir, targetDir, opts);
|
||||||
try { fs.rmSync(sourceDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
try { fs.rmSync(sourceDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveExtractedBackupRoot(extractDir) {
|
function resolveExtractedBackupRoot(extractDir) {
|
||||||
if (fs.existsSync(path.join(extractDir, 'instances'))) return extractDir;
|
if (fs.existsSync(path.join(extractDir, 'instances'))) return extractDir;
|
||||||
const entries = fs.readdirSync(extractDir, { withFileTypes: true }).filter((entry) => entry.isDirectory());
|
const entries = fs.readdirSync(extractDir, { withFileTypes: true }).filter((entry) => entry.isDirectory());
|
||||||
if (entries.length === 1) {
|
if (entries.length === 1) {
|
||||||
const nested = path.join(extractDir, entries[0].name);
|
const nested = path.join(extractDir, entries[0].name);
|
||||||
if (fs.existsSync(path.join(nested, 'instances'))) return nested;
|
if (fs.existsSync(path.join(nested, 'instances'))) return nested;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createGlobalBackup(options) {
|
async function createGlobalBackup(options) {
|
||||||
const instancesDir = options && options.instancesDir;
|
const instancesDir = options && options.instancesDir;
|
||||||
const userDataDir = options && options.userDataDir;
|
const userDataDir = options && options.userDataDir;
|
||||||
const destinationPath = options && options.destinationPath;
|
const destinationPath = options && options.destinationPath;
|
||||||
const includeSettings = !options || options.includeSettings !== false;
|
const includeSettings = !options || options.includeSettings !== false;
|
||||||
const onProgress = options && options.onProgress;
|
const onProgress = options && options.onProgress;
|
||||||
if (!instancesDir || !userDataDir || !destinationPath) throw new Error('Backup-Parameter unvollständig.');
|
if (!instancesDir || !userDataDir || !destinationPath) throw new Error('Backup-Parameter unvollständig.');
|
||||||
|
|
||||||
const settingsFile = path.join(userDataDir, 'settings.json');
|
const settingsFile = path.join(userDataDir, 'settings.json');
|
||||||
const workingArchivePath = destinationPath.replace(/\.zip$/i, '') + '.partial.zip';
|
const workingArchivePath = destinationPath.replace(/\.zip$/i, '') + '.partial.zip';
|
||||||
const backupInfo = {
|
const backupInfo = {
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
instanceCount: countInstanceDirs(instancesDir),
|
instanceCount: countInstanceDirs(instancesDir),
|
||||||
includeSettings,
|
includeSettings,
|
||||||
app: 'AeroMC',
|
app: 'AeroMC',
|
||||||
version: 1,
|
version: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
emitProgress(onProgress, 'create', 5, 'Bereite Backup vor');
|
emitProgress(onProgress, 'create', 5, 'Bereite Backup vor');
|
||||||
emitProgress(onProgress, 'create', 10, 'Ermittle Backup-Inhalt');
|
emitProgress(onProgress, 'create', 10, 'Ermittle Backup-Inhalt');
|
||||||
|
|
||||||
if (fs.existsSync(destinationPath)) fs.unlinkSync(destinationPath);
|
if (fs.existsSync(destinationPath)) fs.unlinkSync(destinationPath);
|
||||||
if (fs.existsSync(workingArchivePath)) fs.unlinkSync(workingArchivePath);
|
if (fs.existsSync(workingArchivePath)) fs.unlinkSync(workingArchivePath);
|
||||||
emitProgress(onProgress, 'create', 90, 'Packe ZIP-Archiv');
|
emitProgress(onProgress, 'create', 90, 'Packe ZIP-Archiv');
|
||||||
await createBackupArchive({
|
await createBackupArchive({
|
||||||
instancesDir,
|
instancesDir,
|
||||||
settingsFile,
|
settingsFile,
|
||||||
includeSettings,
|
includeSettings,
|
||||||
backupInfo,
|
backupInfo,
|
||||||
destinationPath: workingArchivePath,
|
destinationPath: workingArchivePath,
|
||||||
mode: 'create',
|
mode: 'create',
|
||||||
onProgress,
|
onProgress,
|
||||||
startPercent: 15,
|
startPercent: 15,
|
||||||
endPercent: 98,
|
endPercent: 98,
|
||||||
detail: 'Packe ZIP-Archiv',
|
detail: 'Packe ZIP-Archiv',
|
||||||
});
|
});
|
||||||
emitProgress(onProgress, 'create', 99, 'Finalisiere Backup');
|
emitProgress(onProgress, 'create', 99, 'Finalisiere Backup');
|
||||||
fs.renameSync(workingArchivePath, destinationPath);
|
fs.renameSync(workingArchivePath, destinationPath);
|
||||||
emitProgress(onProgress, 'create', 100, 'Backup abgeschlossen');
|
emitProgress(onProgress, 'create', 100, 'Backup abgeschlossen');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
path: destinationPath,
|
path: destinationPath,
|
||||||
instanceCount: countInstanceDirs(instancesDir),
|
instanceCount: countInstanceDirs(instancesDir),
|
||||||
includedSettings: includeSettings && fs.existsSync(settingsFile),
|
includedSettings: includeSettings && fs.existsSync(settingsFile),
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(workingArchivePath)) fs.rmSync(workingArchivePath, { force: true });
|
if (fs.existsSync(workingArchivePath)) fs.rmSync(workingArchivePath, { force: true });
|
||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function restoreGlobalBackup(options) {
|
async function restoreGlobalBackup(options) {
|
||||||
const instancesDir = options && options.instancesDir;
|
const instancesDir = options && options.instancesDir;
|
||||||
const userDataDir = options && options.userDataDir;
|
const userDataDir = options && options.userDataDir;
|
||||||
const sourcePath = options && options.sourcePath;
|
const sourcePath = options && options.sourcePath;
|
||||||
const includeSettings = !!(options && options.includeSettings);
|
const includeSettings = !!(options && options.includeSettings);
|
||||||
const onProgress = options && options.onProgress;
|
const onProgress = options && options.onProgress;
|
||||||
const instancesDirSetting = options && Object.prototype.hasOwnProperty.call(options, 'instancesDirSetting')
|
const instancesDirSetting = options && Object.prototype.hasOwnProperty.call(options, 'instancesDirSetting')
|
||||||
? options.instancesDirSetting
|
? options.instancesDirSetting
|
||||||
: null;
|
: null;
|
||||||
if (!instancesDir || !userDataDir || !sourcePath) throw new Error('Restore-Parameter unvollständig.');
|
if (!instancesDir || !userDataDir || !sourcePath) throw new Error('Restore-Parameter unvollständig.');
|
||||||
|
|
||||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'aeromc-restore-'));
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'aeromc-restore-'));
|
||||||
const extractDir = path.join(tmp, 'extract');
|
const extractDir = path.join(tmp, 'extract');
|
||||||
const rollbackDir = path.join(tmp, 'rollback');
|
const rollbackDir = path.join(tmp, 'rollback');
|
||||||
const rollbackInstances = path.join(rollbackDir, 'instances');
|
const rollbackInstances = path.join(rollbackDir, 'instances');
|
||||||
const settingsFile = path.join(userDataDir, 'settings.json');
|
const settingsFile = path.join(userDataDir, 'settings.json');
|
||||||
const rollbackSettings = path.join(rollbackDir, 'settings.json');
|
const rollbackSettings = path.join(rollbackDir, 'settings.json');
|
||||||
let settingsPreviouslyExisted = false;
|
let settingsPreviouslyExisted = false;
|
||||||
|
// wird erst true, sobald der aktuelle Stand tatsächlich weggesichert wurde -
|
||||||
try {
|
// vorher darf der Rollback nichts anfassen, sonst löscht ein früher Fehler
|
||||||
ensureDir(extractDir);
|
// (z. B. ungültiges Archiv) den unveränderten, intakten Originalbestand
|
||||||
emitProgress(onProgress, 'restore', 5, 'Entpacke Backup');
|
let originalMovedAway = false;
|
||||||
await extractBackupArchive(sourcePath, extractDir, {
|
|
||||||
mode: 'restore',
|
try {
|
||||||
onProgress,
|
ensureDir(extractDir);
|
||||||
startPercent: 5,
|
emitProgress(onProgress, 'restore', 5, 'Entpacke Backup');
|
||||||
endPercent: 18,
|
await extractBackupArchive(sourcePath, extractDir, {
|
||||||
detail: 'Entpacke Backup',
|
mode: 'restore',
|
||||||
});
|
onProgress,
|
||||||
|
startPercent: 5,
|
||||||
const backupRoot = resolveExtractedBackupRoot(extractDir);
|
endPercent: 18,
|
||||||
if (!backupRoot) throw new Error('Das Archiv enthält kein gültiges AeroMC-Backup.');
|
detail: 'Entpacke Backup',
|
||||||
emitProgress(onProgress, 'restore', 18, 'Backup geprüft');
|
});
|
||||||
|
|
||||||
const sourceInstances = path.join(backupRoot, 'instances');
|
const backupRoot = resolveExtractedBackupRoot(extractDir);
|
||||||
const sourceSettings = path.join(backupRoot, 'settings.json');
|
if (!backupRoot) throw new Error('Das Archiv enthält kein gültiges AeroMC-Backup.');
|
||||||
ensureDir(rollbackDir);
|
emitProgress(onProgress, 'restore', 18, 'Backup geprüft');
|
||||||
|
|
||||||
emitProgress(onProgress, 'restore', 25, 'Sichere aktuellen Stand');
|
const sourceInstances = path.join(backupRoot, 'instances');
|
||||||
moveDirTracked(instancesDir, rollbackInstances, {
|
const sourceSettings = path.join(backupRoot, 'settings.json');
|
||||||
mode: 'restore',
|
ensureDir(rollbackDir);
|
||||||
onProgress,
|
|
||||||
startPercent: 28,
|
emitProgress(onProgress, 'restore', 25, 'Sichere aktuellen Stand');
|
||||||
endPercent: 40,
|
moveDirTracked(instancesDir, rollbackInstances, {
|
||||||
detailPrefix: 'Sichere aktuelle Instanzen',
|
mode: 'restore',
|
||||||
});
|
onProgress,
|
||||||
settingsPreviouslyExisted = fs.existsSync(settingsFile);
|
startPercent: 28,
|
||||||
if (settingsPreviouslyExisted) fs.copyFileSync(settingsFile, rollbackSettings);
|
endPercent: 40,
|
||||||
|
detailPrefix: 'Sichere aktuelle Instanzen',
|
||||||
ensureDir(path.dirname(instancesDir));
|
});
|
||||||
moveDirTracked(sourceInstances, instancesDir, {
|
originalMovedAway = true;
|
||||||
mode: 'restore',
|
settingsPreviouslyExisted = fs.existsSync(settingsFile);
|
||||||
onProgress,
|
if (settingsPreviouslyExisted) fs.copyFileSync(settingsFile, rollbackSettings);
|
||||||
startPercent: 45,
|
|
||||||
endPercent: 78,
|
ensureDir(path.dirname(instancesDir));
|
||||||
detailPrefix: 'Stelle Instanzen wieder her',
|
moveDirTracked(sourceInstances, instancesDir, {
|
||||||
});
|
mode: 'restore',
|
||||||
|
onProgress,
|
||||||
let restoredSettings = false;
|
startPercent: 45,
|
||||||
if (includeSettings && fs.existsSync(sourceSettings)) {
|
endPercent: 78,
|
||||||
const restored = readJson(sourceSettings, {});
|
detailPrefix: 'Stelle Instanzen wieder her',
|
||||||
restored.instancesDir = instancesDirSetting;
|
});
|
||||||
writeJson(settingsFile, restored);
|
|
||||||
restoredSettings = true;
|
let restoredSettings = false;
|
||||||
emitProgress(onProgress, 'restore', 90, 'Stelle Einstellungen wieder her');
|
if (includeSettings && fs.existsSync(sourceSettings)) {
|
||||||
} else {
|
const restored = readJson(sourceSettings, {});
|
||||||
emitProgress(onProgress, 'restore', 90, 'Einstellungen übersprungen');
|
restored.instancesDir = instancesDirSetting;
|
||||||
}
|
writeJson(settingsFile, restored);
|
||||||
|
restoredSettings = true;
|
||||||
emitProgress(onProgress, 'restore', 100, 'Wiederherstellen abgeschlossen');
|
emitProgress(onProgress, 'restore', 90, 'Stelle Einstellungen wieder her');
|
||||||
|
} else {
|
||||||
return {
|
emitProgress(onProgress, 'restore', 90, 'Einstellungen übersprungen');
|
||||||
ok: true,
|
}
|
||||||
path: sourcePath,
|
|
||||||
instanceCount: countInstanceDirs(instancesDir),
|
emitProgress(onProgress, 'restore', 100, 'Wiederherstellen abgeschlossen');
|
||||||
restoredSettings,
|
|
||||||
};
|
return {
|
||||||
} catch (err) {
|
ok: true,
|
||||||
try {
|
path: sourcePath,
|
||||||
if (fs.existsSync(instancesDir)) fs.rmSync(instancesDir, { recursive: true, force: true });
|
instanceCount: countInstanceDirs(instancesDir),
|
||||||
if (fs.existsSync(rollbackInstances)) fs.cpSync(rollbackInstances, instancesDir, { recursive: true });
|
restoredSettings,
|
||||||
else ensureDir(instancesDir);
|
};
|
||||||
|
} catch (err) {
|
||||||
if (includeSettings) {
|
// Nur zurückrollen, wenn wir den ursprünglichen Bestand überhaupt schon
|
||||||
if (fs.existsSync(rollbackSettings)) fs.copyFileSync(rollbackSettings, settingsFile);
|
// angefasst haben (siehe originalMovedAway oben). Schlägt die
|
||||||
else if (!settingsPreviouslyExisted && fs.existsSync(settingsFile)) fs.rmSync(settingsFile, { force: true });
|
// Wiederherstellung vorher fehl (ungültiges/beschädigtes Archiv o. Ä.),
|
||||||
}
|
// ist instancesDir noch der unveränderte Originalbestand - den lassen wir
|
||||||
} catch { /* ignore rollback errors */ }
|
// dann bewusst in Ruhe, statt ihn zu löschen.
|
||||||
throw err;
|
if (originalMovedAway) {
|
||||||
} finally {
|
try {
|
||||||
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch { /* ignore */ }
|
if (fs.existsSync(instancesDir)) fs.rmSync(instancesDir, { recursive: true, force: true });
|
||||||
}
|
if (fs.existsSync(rollbackInstances)) fs.cpSync(rollbackInstances, instancesDir, { recursive: true });
|
||||||
}
|
else ensureDir(instancesDir);
|
||||||
|
|
||||||
module.exports = {
|
if (includeSettings) {
|
||||||
createGlobalBackup,
|
if (fs.existsSync(rollbackSettings)) fs.copyFileSync(rollbackSettings, settingsFile);
|
||||||
restoreGlobalBackup,
|
else if (!settingsPreviouslyExisted && fs.existsSync(settingsFile)) fs.rmSync(settingsFile, { force: true });
|
||||||
defaultBackupFileName,
|
}
|
||||||
defaultBackupDir,
|
} catch { /* ignore rollback errors */ }
|
||||||
listGlobalBackups,
|
}
|
||||||
resolveAutoBackupPath,
|
throw err;
|
||||||
|
} finally {
|
||||||
|
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createGlobalBackup,
|
||||||
|
restoreGlobalBackup,
|
||||||
|
defaultBackupFileName,
|
||||||
|
defaultBackupDir,
|
||||||
|
listGlobalBackups,
|
||||||
|
resolveAutoBackupPath,
|
||||||
};
|
};
|
||||||
+195
-195
@@ -1,196 +1,196 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* cfimport.js – Instanzen aus dem CurseForge-Launcher übernehmen
|
* cfimport.js – Instanzen aus dem CurseForge-Launcher übernehmen
|
||||||
* ---------------------------------------------------------------
|
* ---------------------------------------------------------------
|
||||||
* Unterstützt sowohl den Launcher-Wurzelordner als auch direkt den
|
* Unterstützt sowohl den Launcher-Wurzelordner als auch direkt den
|
||||||
* Instances-Ordner. Gelesen werden nach Möglichkeit minecraftinstance.json
|
* Instances-Ordner. Gelesen werden nach Möglichkeit minecraftinstance.json
|
||||||
* und die vorhandenen Spieldaten im Profilordner.
|
* und die vorhandenen Spieldaten im Profilordner.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
const META_FILES = ['minecraftinstance.json', 'instance.json'];
|
const META_FILES = ['minecraftinstance.json', 'instance.json'];
|
||||||
|
|
||||||
function readJson(file, fallback) {
|
function readJson(file, fallback) {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
|
return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
|
||||||
} catch {
|
} catch {
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function existingMetaFile(dir) {
|
function existingMetaFile(dir) {
|
||||||
return META_FILES.map((name) => path.join(dir, name)).find((file) => fs.existsSync(file)) || null;
|
return META_FILES.map((name) => path.join(dir, name)).find((file) => fs.existsSync(file)) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function metaVersion(meta) {
|
function metaVersion(meta) {
|
||||||
return String(
|
return String(
|
||||||
(meta && (
|
(meta && (
|
||||||
meta.gameVersion || meta.minecraftVersion || meta.mcVersion ||
|
meta.gameVersion || meta.minecraftVersion || meta.mcVersion ||
|
||||||
(meta.installedModpack && meta.installedModpack.gameVersion)
|
(meta.installedModpack && meta.installedModpack.gameVersion)
|
||||||
)) || ''
|
)) || ''
|
||||||
).trim();
|
).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveInstancesDir(inputPath) {
|
function resolveInstancesDir(inputPath) {
|
||||||
if (!inputPath || !fs.existsSync(inputPath)) return null;
|
if (!inputPath || !fs.existsSync(inputPath)) return null;
|
||||||
const directNames = new Set(['instances', 'minecraftinstances']);
|
const directNames = new Set(['instances', 'minecraftinstances']);
|
||||||
if (directNames.has(path.basename(inputPath).toLowerCase())) return inputPath;
|
if (directNames.has(path.basename(inputPath).toLowerCase())) return inputPath;
|
||||||
|
|
||||||
const candidates = [
|
const candidates = [
|
||||||
path.join(inputPath, 'Instances'),
|
path.join(inputPath, 'Instances'),
|
||||||
path.join(inputPath, 'instances'),
|
path.join(inputPath, 'instances'),
|
||||||
path.join(inputPath, 'minecraftInstances'),
|
path.join(inputPath, 'minecraftInstances'),
|
||||||
path.join(inputPath, 'Minecraft', 'Instances'),
|
path.join(inputPath, 'Minecraft', 'Instances'),
|
||||||
path.join(inputPath, 'minecraft', 'Instances'),
|
path.join(inputPath, 'minecraft', 'Instances'),
|
||||||
];
|
];
|
||||||
|
|
||||||
const looksLikeWindowsInstall =
|
const looksLikeWindowsInstall =
|
||||||
/curseforge windows/i.test(inputPath) ||
|
/curseforge windows/i.test(inputPath) ||
|
||||||
(fs.existsSync(path.join(inputPath, 'CurseForge.exe')) && fs.existsSync(path.join(inputPath, 'resources')));
|
(fs.existsSync(path.join(inputPath, 'CurseForge.exe')) && fs.existsSync(path.join(inputPath, 'resources')));
|
||||||
if (looksLikeWindowsInstall) {
|
if (looksLikeWindowsInstall) {
|
||||||
candidates.push(
|
candidates.push(
|
||||||
path.join(os.homedir(), 'curseforge', 'minecraft', 'Instances'),
|
path.join(os.homedir(), 'curseforge', 'minecraft', 'Instances'),
|
||||||
path.join(os.homedir(), 'CurseForge', 'minecraft', 'Instances'),
|
path.join(os.homedir(), 'CurseForge', 'minecraft', 'Instances'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return candidates.find((candidate) => fs.existsSync(candidate)) || null;
|
return candidates.find((candidate) => fs.existsSync(candidate)) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeLoaderVersion(loader, rawValue, mcVersion) {
|
function normalizeLoaderVersion(loader, rawValue, mcVersion) {
|
||||||
let value = String(rawValue || '').trim();
|
let value = String(rawValue || '').trim();
|
||||||
if (!value) return '';
|
if (!value) return '';
|
||||||
|
|
||||||
if (loader === 'forge') {
|
if (loader === 'forge') {
|
||||||
value = value.replace(/^forge-/i, '');
|
value = value.replace(/^forge-/i, '');
|
||||||
if (mcVersion && value.startsWith(mcVersion + '-')) value = value.slice(mcVersion.length + 1);
|
if (mcVersion && value.startsWith(mcVersion + '-')) value = value.slice(mcVersion.length + 1);
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
if (loader === 'neoforge') {
|
if (loader === 'neoforge') {
|
||||||
return value.replace(/^neoforge-/i, '');
|
return value.replace(/^neoforge-/i, '');
|
||||||
}
|
}
|
||||||
if (loader === 'fabric') {
|
if (loader === 'fabric') {
|
||||||
value = value.replace(/^fabric(?:-loader)?-/i, '');
|
value = value.replace(/^fabric(?:-loader)?-/i, '');
|
||||||
if (mcVersion && value.endsWith('-' + mcVersion)) value = value.slice(0, -1 * (mcVersion.length + 1));
|
if (mcVersion && value.endsWith('-' + mcVersion)) value = value.slice(0, -1 * (mcVersion.length + 1));
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
if (loader === 'quilt') {
|
if (loader === 'quilt') {
|
||||||
value = value.replace(/^quilt(?:-loader)?-/i, '');
|
value = value.replace(/^quilt(?:-loader)?-/i, '');
|
||||||
if (mcVersion && value.endsWith('-' + mcVersion)) value = value.slice(0, -1 * (mcVersion.length + 1));
|
if (mcVersion && value.endsWith('-' + mcVersion)) value = value.slice(0, -1 * (mcVersion.length + 1));
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectLoader(meta, mcVersion) {
|
function detectLoader(meta, mcVersion) {
|
||||||
const raw = meta && (
|
const raw = meta && (
|
||||||
meta.baseModLoader || meta.modLoader || meta.modloader || meta.loader || meta.modLoaderId
|
meta.baseModLoader || meta.modLoader || meta.modloader || meta.loader || meta.modLoaderId
|
||||||
);
|
);
|
||||||
const value = typeof raw === 'object'
|
const value = typeof raw === 'object'
|
||||||
? (raw.name || raw.id || raw.value || raw.version || '')
|
? (raw.name || raw.id || raw.value || raw.version || '')
|
||||||
: (raw || '');
|
: (raw || '');
|
||||||
const lower = String(value).toLowerCase();
|
const lower = String(value).toLowerCase();
|
||||||
const objectVersion = raw && typeof raw === 'object'
|
const objectVersion = raw && typeof raw === 'object'
|
||||||
? (raw.version || raw.name || raw.id || '')
|
? (raw.version || raw.name || raw.id || '')
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
if (!lower) return { loader: 'vanilla', loaderVersion: '' };
|
if (!lower) return { loader: 'vanilla', loaderVersion: '' };
|
||||||
if (lower.includes('neoforge')) return { loader: 'neoforge', loaderVersion: normalizeLoaderVersion('neoforge', objectVersion || value, mcVersion) };
|
if (lower.includes('neoforge')) return { loader: 'neoforge', loaderVersion: normalizeLoaderVersion('neoforge', objectVersion || value, mcVersion) };
|
||||||
if (lower.includes('fabric')) return { loader: 'fabric', loaderVersion: normalizeLoaderVersion('fabric', objectVersion || value, mcVersion) };
|
if (lower.includes('fabric')) return { loader: 'fabric', loaderVersion: normalizeLoaderVersion('fabric', objectVersion || value, mcVersion) };
|
||||||
if (lower.includes('quilt')) return { loader: 'quilt', loaderVersion: normalizeLoaderVersion('quilt', objectVersion || value, mcVersion) };
|
if (lower.includes('quilt')) return { loader: 'quilt', loaderVersion: normalizeLoaderVersion('quilt', objectVersion || value, mcVersion) };
|
||||||
if (lower.includes('forge')) return { loader: 'forge', loaderVersion: normalizeLoaderVersion('forge', objectVersion || value, mcVersion) };
|
if (lower.includes('forge')) return { loader: 'forge', loaderVersion: normalizeLoaderVersion('forge', objectVersion || value, mcVersion) };
|
||||||
return { loader: 'vanilla', loaderVersion: '' };
|
return { loader: 'vanilla', loaderVersion: '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectVersion(meta, loaderVersion) {
|
function detectVersion(meta, loaderVersion) {
|
||||||
return metaVersion(meta) || (loaderVersion && loaderVersion.includes('-') ? loaderVersion.split('-')[0] : '');
|
return metaVersion(meta) || (loaderVersion && loaderVersion.includes('-') ? loaderVersion.split('-')[0] : '');
|
||||||
}
|
}
|
||||||
|
|
||||||
function findGameDir(dir) {
|
function findGameDir(dir) {
|
||||||
const nested = ['minecraft', '.minecraft']
|
const nested = ['minecraft', '.minecraft']
|
||||||
.map((name) => path.join(dir, name))
|
.map((name) => path.join(dir, name))
|
||||||
.find((candidate) => fs.existsSync(candidate));
|
.find((candidate) => fs.existsSync(candidate));
|
||||||
if (nested) return nested;
|
if (nested) return nested;
|
||||||
|
|
||||||
const markers = ['mods', 'config', 'resourcepacks', 'shaderpacks', 'saves', 'options.txt'];
|
const markers = ['mods', 'config', 'resourcepacks', 'shaderpacks', 'saves', 'options.txt'];
|
||||||
return markers.some((name) => fs.existsSync(path.join(dir, name))) ? dir : null;
|
return markers.some((name) => fs.existsSync(path.join(dir, name))) ? dir : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function scan(inputPath) {
|
function scan(inputPath) {
|
||||||
const instancesDir = resolveInstancesDir(inputPath);
|
const instancesDir = resolveInstancesDir(inputPath);
|
||||||
if (!instancesDir) return { ok: false, reason: 'not-found' };
|
if (!instancesDir) return { ok: false, reason: 'not-found' };
|
||||||
|
|
||||||
const list = [];
|
const list = [];
|
||||||
for (const entry of fs.readdirSync(instancesDir, { withFileTypes: true })) {
|
for (const entry of fs.readdirSync(instancesDir, { withFileTypes: true })) {
|
||||||
if (!entry.isDirectory()) continue;
|
if (!entry.isDirectory()) continue;
|
||||||
const dir = path.join(instancesDir, entry.name);
|
const dir = path.join(instancesDir, entry.name);
|
||||||
const metaFile = existingMetaFile(dir);
|
const metaFile = existingMetaFile(dir);
|
||||||
const meta = metaFile ? readJson(metaFile, {}) : {};
|
const meta = metaFile ? readJson(metaFile, {}) : {};
|
||||||
const gameDir = findGameDir(dir);
|
const gameDir = findGameDir(dir);
|
||||||
if (!metaFile && !gameDir) continue;
|
if (!metaFile && !gameDir) continue;
|
||||||
|
|
||||||
const versionHint = metaVersion(meta);
|
const versionHint = metaVersion(meta);
|
||||||
const loaderInfo = detectLoader(meta, versionHint);
|
const loaderInfo = detectLoader(meta, versionHint);
|
||||||
list.push({
|
list.push({
|
||||||
folder: entry.name,
|
folder: entry.name,
|
||||||
dir,
|
dir,
|
||||||
name: meta.name || meta.displayName || entry.name,
|
name: meta.name || meta.displayName || entry.name,
|
||||||
group: '',
|
group: '',
|
||||||
notes: meta.notes || meta.summary || '',
|
notes: meta.notes || meta.summary || '',
|
||||||
version: versionHint || detectVersion(meta, loaderInfo.loaderVersion),
|
version: versionHint || detectVersion(meta, loaderInfo.loaderVersion),
|
||||||
loader: loaderInfo.loader,
|
loader: loaderInfo.loader,
|
||||||
loaderVersion: loaderInfo.loaderVersion,
|
loaderVersion: loaderInfo.loaderVersion,
|
||||||
javaPath: String(meta.javaPath || meta.javaExecutable || '').trim(),
|
javaPath: String(meta.javaPath || meta.javaExecutable || '').trim(),
|
||||||
minMemMb: Number(meta.minimumMemory || meta.minMemory || meta.minMemAlloc) || null,
|
minMemMb: Number(meta.minimumMemory || meta.minMemory || meta.minMemAlloc) || null,
|
||||||
maxMemMb: Number(meta.maximumMemory || meta.maxMemory || meta.maxMemAlloc || meta.allocatedMemory) || null,
|
maxMemMb: Number(meta.maximumMemory || meta.maxMemory || meta.maxMemAlloc || meta.allocatedMemory) || null,
|
||||||
gameDir: gameDir || dir,
|
gameDir: gameDir || dir,
|
||||||
hasGameData: !!gameDir,
|
hasGameData: !!gameDir,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
list.sort((a, b) => a.name.localeCompare(b.name));
|
list.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
return { ok: true, instancesDir, count: list.length, instances: list };
|
return { ok: true, instancesDir, count: list.length, instances: list };
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyInto(sourceDir, targetDir) {
|
function copyInto(sourceDir, targetDir) {
|
||||||
fs.mkdirSync(targetDir, { recursive: true });
|
fs.mkdirSync(targetDir, { recursive: true });
|
||||||
for (const name of fs.readdirSync(sourceDir)) {
|
for (const name of fs.readdirSync(sourceDir)) {
|
||||||
if (META_FILES.includes(name)) continue;
|
if (META_FILES.includes(name)) continue;
|
||||||
fs.cpSync(path.join(sourceDir, name), path.join(targetDir, name), { recursive: true });
|
fs.cpSync(path.join(sourceDir, name), path.join(targetDir, name), { recursive: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function importOne(store, entry, copyData = true) {
|
function importOne(store, entry, copyData = true) {
|
||||||
const created = store.createOrReplaceImportedInstance({
|
const created = store.createOrReplaceImportedInstance({
|
||||||
name: entry.name,
|
name: entry.name,
|
||||||
group: entry.group,
|
group: entry.group,
|
||||||
notes: entry.notes,
|
notes: entry.notes,
|
||||||
minecraft: {
|
minecraft: {
|
||||||
version: entry.version,
|
version: entry.version,
|
||||||
loader: entry.loader,
|
loader: entry.loader,
|
||||||
loaderVersion: entry.loaderVersion,
|
loaderVersion: entry.loaderVersion,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const patch = {};
|
const patch = {};
|
||||||
if (entry.javaPath || entry.minMemMb || entry.maxMemMb) {
|
if (entry.javaPath || entry.minMemMb || entry.maxMemMb) {
|
||||||
patch.java = {
|
patch.java = {
|
||||||
path: entry.javaPath || '',
|
path: entry.javaPath || '',
|
||||||
minMemMb: entry.minMemMb || null,
|
minMemMb: entry.minMemMb || null,
|
||||||
maxMemMb: entry.maxMemMb || null,
|
maxMemMb: entry.maxMemMb || null,
|
||||||
extraArgs: '',
|
extraArgs: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (Object.keys(patch).length) store.updateInstance(created.id, patch);
|
if (Object.keys(patch).length) store.updateInstance(created.id, patch);
|
||||||
|
|
||||||
let copied = false;
|
let copied = false;
|
||||||
if (copyData && entry.gameDir && fs.existsSync(entry.gameDir)) {
|
if (copyData && entry.gameDir && fs.existsSync(entry.gameDir)) {
|
||||||
copyInto(entry.gameDir, store.gameDir(created.id));
|
copyInto(entry.gameDir, store.gameDir(created.id));
|
||||||
copied = true;
|
copied = true;
|
||||||
}
|
}
|
||||||
return { id: created.id, name: created.name, copied };
|
return { id: created.id, name: created.name, copied };
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { scan, importOne, resolveInstancesDir };
|
module.exports = { scan, importOne, resolveInstancesDir };
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* discordpresence.js – Discord Rich Presence ("Spielt Minecraft über AeroMC")
|
||||||
|
* -----------------------------------------------------------------------------
|
||||||
|
* Verbindet sich rein lokal über die IPC-Pipe des laufenden Discord-Clients.
|
||||||
|
* SET_ACTIVITY braucht dafür bewusst KEINE Authentifizierung/OAuth (anders als
|
||||||
|
* der Rest der RPC-API) – daher hier absichtlich client.request(...) direkt
|
||||||
|
* statt client.user.setActivity(...), das erst nach einem Login funktioniert.
|
||||||
|
*
|
||||||
|
* Immer aktiv, ohne eigene Einstellung – braucht also keine Zustimmung/kein
|
||||||
|
* Häkchen. Läuft trotzdem komplett unauffällig: ist Discord nicht installiert
|
||||||
|
* oder nicht gestartet, bleibt es einfach inaktiv, ohne den Launcher oder den
|
||||||
|
* Spielstart zu stören – jeder Fehler hier wird bewusst verschluckt.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { Client } = require('@xhayper/discord-rpc');
|
||||||
|
|
||||||
|
// Von M_Viper unter discord.com/developers/applications angelegte AeroMC-App.
|
||||||
|
// Öffentliche Anwendungs-ID, kein Geheimnis (genau wie DEFAULT_CLIENT_ID bei
|
||||||
|
// der Microsoft-Anmeldung in auth.js) – gilt für alle AeroMC-Nutzer gleich.
|
||||||
|
const DEFAULT_CLIENT_ID = '1538173087730630686';
|
||||||
|
|
||||||
|
const LAUNCHER_WEBSITE = 'https://aeromc.viper.ipv64.net/';
|
||||||
|
|
||||||
|
let clientId = DEFAULT_CLIENT_ID;
|
||||||
|
let client = null;
|
||||||
|
let connecting = null;
|
||||||
|
|
||||||
|
function resolveClientId() {
|
||||||
|
return (clientId && String(clientId).trim()) || DEFAULT_CLIENT_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connect() {
|
||||||
|
if (client && client.isConnected) return client;
|
||||||
|
if (connecting) return connecting;
|
||||||
|
|
||||||
|
const id = resolveClientId();
|
||||||
|
const c = new Client({ clientId: id });
|
||||||
|
c.on('disconnected', () => { if (client === c) client = null; });
|
||||||
|
connecting = c.connect()
|
||||||
|
.then(() => { client = c; connecting = null; return c; })
|
||||||
|
.catch((err) => { connecting = null; throw err; });
|
||||||
|
return connecting;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* activity: { details, state, startTimestamp }
|
||||||
|
* details/state -> die beiden Zeilen unter dem Profilbild in Discord
|
||||||
|
* startTimestamp -> Discord zeigt die Spielzeit dann selbst laufend an
|
||||||
|
*/
|
||||||
|
async function setActivity(activity) {
|
||||||
|
try {
|
||||||
|
const c = await connect();
|
||||||
|
await c.request('SET_ACTIVITY', {
|
||||||
|
pid: process.pid,
|
||||||
|
activity: {
|
||||||
|
details: activity.details,
|
||||||
|
state: activity.state,
|
||||||
|
timestamps: activity.startTimestamp ? { start: activity.startTimestamp } : undefined,
|
||||||
|
assets: { large_image: 'aeromc_logo', large_text: 'AeroMC Launcher' },
|
||||||
|
// Discord zeigt Buttons Betrachtern des Profils an (nicht sich selbst) -
|
||||||
|
// maximal 2 sind erlaubt, hier reicht einer.
|
||||||
|
buttons: [{ label: 'AeroMC herunterladen', url: LAUNCHER_WEBSITE }],
|
||||||
|
instance: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Discord nicht installiert/nicht gestartet o. Ä. - bewusst lautlos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearActivity() {
|
||||||
|
try {
|
||||||
|
if (!client || !client.isConnected) return;
|
||||||
|
await client.request('SET_ACTIVITY', { pid: process.pid });
|
||||||
|
} catch { /* egal */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function shutdown() {
|
||||||
|
const c = client;
|
||||||
|
client = null;
|
||||||
|
connecting = null;
|
||||||
|
if (!c) return;
|
||||||
|
try { await c.destroy(); } catch { /* egal */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Verbindet direkt beim Start. settings.discordClientId erlaubt weiterhin
|
||||||
|
* (rein optional, per Hand in settings.json) eine eigene App-ID zu setzen -
|
||||||
|
* ohne eigenes Einstellungsfeld, ohne dass es dafür einen Schalter braucht.
|
||||||
|
*/
|
||||||
|
function configure(settings) {
|
||||||
|
const nextId = (settings && settings.discordClientId) || DEFAULT_CLIENT_ID;
|
||||||
|
const idChanged = nextId !== clientId;
|
||||||
|
clientId = nextId;
|
||||||
|
|
||||||
|
if (idChanged) { shutdown().then(() => connect().catch(() => {})); }
|
||||||
|
else if (!client) { connect().catch(() => {}); }
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { configure, setActivity, clearActivity, shutdown, DEFAULT_CLIENT_ID };
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* friends.js – Anbindung an das AeroMC-Freundesystem (WordPress-Plugin)
|
||||||
|
* -----------------------------------------------------------------------------
|
||||||
|
* Schlanke Fetch-Wrapper gegen die REST-Routen des "AeroMC Freunde"-Plugins
|
||||||
|
* (siehe wordpress-plugin/aeromc-friends/). Keine eigene Anmeldung - jeder
|
||||||
|
* Aufruf trägt einfach die Minecraft-UUID des aktiven Kontos.
|
||||||
|
*
|
||||||
|
* Genau wie bei discordpresence.js: jeder Fehler (Plugin nicht installiert,
|
||||||
|
* kein Internet, Seite down) wird abgefangen und nach oben als { ok:false,
|
||||||
|
* message } gereicht statt den Launcher zu stören - ein soziales Extra darf
|
||||||
|
* nie den Spielstart oder die Bedienung blockieren.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const BASIS_URL = 'https://viper-network.de/wp-json/aeromc/v1';
|
||||||
|
|
||||||
|
async function anfrage(pfad, { method = 'GET', body } = {}) {
|
||||||
|
try {
|
||||||
|
const url = method === 'GET' && body
|
||||||
|
? `${BASIS_URL}${pfad}?${new URLSearchParams(body).toString()}`
|
||||||
|
: `${BASIS_URL}${pfad}`;
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: method === 'GET' ? undefined : JSON.stringify(body || {}),
|
||||||
|
});
|
||||||
|
const daten = await res.json().catch(() => null);
|
||||||
|
if (!res.ok) return { ok: false, message: (daten && daten.message) || `Fehler ${res.status}` };
|
||||||
|
return { ok: true, data: daten };
|
||||||
|
} catch (err) {
|
||||||
|
return { ok: false, message: 'Freundesystem nicht erreichbar: ' + err.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function heartbeat({ uuid, username, instance, server }) {
|
||||||
|
return anfrage('/presence', { method: 'POST', body: { uuid, username, instance, server } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFriends(uuid) {
|
||||||
|
return anfrage('/friends', { method: 'GET', body: { uuid } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRequests(uuid) {
|
||||||
|
return anfrage('/friends/requests', { method: 'GET', body: { uuid } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSentRequests(uuid) {
|
||||||
|
return anfrage('/friends/sent', { method: 'GET', body: { uuid } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendRequest(uuid, username, targetUsername) {
|
||||||
|
return anfrage('/friends/request', { method: 'POST', body: { uuid, username, targetUsername } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function respond(uuid, otherUuid, accept) {
|
||||||
|
return anfrage('/friends/respond', { method: 'POST', body: { uuid, otherUuid, accept } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelRequest(uuid, otherUuid) {
|
||||||
|
return anfrage('/friends/cancel', { method: 'POST', body: { uuid, otherUuid } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeFriend(uuid, otherUuid) {
|
||||||
|
return anfrage('/friends/remove', { method: 'POST', body: { uuid, otherUuid } });
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
heartbeat, getFriends, getRequests, getSentRequests, sendRequest, respond, cancelRequest, removeFriend,
|
||||||
|
};
|
||||||
+237
-12
@@ -17,6 +17,7 @@
|
|||||||
<button id="btn-bedrock" class="tbtn" data-ic="blocks" title="Minecraft Bedrock (Windows-Ausgabe)">Bedrock</button>
|
<button id="btn-bedrock" class="tbtn" data-ic="blocks" title="Minecraft Bedrock (Windows-Ausgabe)">Bedrock</button>
|
||||||
<button id="btn-console" class="tbtn" data-ic="terminal" title="Konsole / Log">Konsole</button>
|
<button id="btn-console" class="tbtn" data-ic="terminal" title="Konsole / Log">Konsole</button>
|
||||||
<button id="btn-skin" class="tbtn" data-ic="user" title="Skin-Hintergrund ein/aus">Skin</button>
|
<button id="btn-skin" class="tbtn" data-ic="user" title="Skin-Hintergrund ein/aus">Skin</button>
|
||||||
|
<button id="btn-friends" class="tbtn" data-ic="users" title="Freunde (ingame: Strg+Alt+F zeigt, wer online ist)">Freunde<span id="friends-badge" class="tb-badge hidden"></span></button>
|
||||||
<span class="tbgrow"></span>
|
<span class="tbgrow"></span>
|
||||||
<div class="search-wrap">
|
<div class="search-wrap">
|
||||||
<input id="search" type="text" placeholder="Suchen… (Strg+F)" autocomplete="off" title="Strg+F fokussiert die Suche" />
|
<input id="search" type="text" placeholder="Suchen… (Strg+F)" autocomplete="off" title="Strg+F fokussiert die Suche" />
|
||||||
@@ -51,6 +52,7 @@
|
|||||||
<button id="p-mods" class="sbtn">Mods anzeigen</button>
|
<button id="p-mods" class="sbtn">Mods anzeigen</button>
|
||||||
<button id="p-worlds" class="sbtn">Welten verwalten</button>
|
<button id="p-worlds" class="sbtn">Welten verwalten</button>
|
||||||
<button id="p-screens" class="sbtn">Bildschirmfotos verwalten</button>
|
<button id="p-screens" class="sbtn">Bildschirmfotos verwalten</button>
|
||||||
|
<button id="p-record" class="sbtn">Spiel aufzeichnen</button>
|
||||||
<button id="p-modpack" class="sbtn hidden">Modpack aktualisieren</button>
|
<button id="p-modpack" class="sbtn hidden">Modpack aktualisieren</button>
|
||||||
<button id="p-logs" class="sbtn">Logdateien ansehen</button>
|
<button id="p-logs" class="sbtn">Logdateien ansehen</button>
|
||||||
<button id="p-servers" class="sbtn">Server verwalten</button>
|
<button id="p-servers" class="sbtn">Server verwalten</button>
|
||||||
@@ -130,6 +132,7 @@
|
|||||||
<option value="forge">Forge</option>
|
<option value="forge">Forge</option>
|
||||||
<option value="neoforge">NeoForge</option>
|
<option value="neoforge">NeoForge</option>
|
||||||
<option value="quilt">Quilt</option>
|
<option value="quilt">Quilt</option>
|
||||||
|
<option value="optifine">OptiFine</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
@@ -139,6 +142,23 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="f-optifine-tools" class="field-row hidden" style="align-items:flex-start">
|
||||||
|
<div class="field" style="flex:1">
|
||||||
|
<span>OptiFine einrichten</span>
|
||||||
|
<p class="hint" style="margin:2px 0 8px">
|
||||||
|
OptiFine bietet kein automatisches Download-API (Downloads laufen bei OptiFine bewusst über eine Werbeseite).
|
||||||
|
Lade den Installer zuerst selbst von <b>optifine.net</b> herunter, wähle ihn dann hier aus – AeroMC bereitet
|
||||||
|
die passende Minecraft-Version vor und startet den echten OptiFine-Installer. Wähle darin als Zielordner den
|
||||||
|
automatisch in die Zwischenablage kopierten Pfad, klicke „Install" und bestätige danach hier.
|
||||||
|
</p>
|
||||||
|
<div class="path-row">
|
||||||
|
<button type="button" id="f-optifine-install" class="btn">Installer wählen und starten…</button>
|
||||||
|
<button type="button" id="f-optifine-confirm" class="btn btn-primary hidden">Fertig – Version übernehmen</button>
|
||||||
|
</div>
|
||||||
|
<small id="f-optifine-status" class="hint"></small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Notizen <small class="opt">(optional)</small></span>
|
<span>Notizen <small class="opt">(optional)</small></span>
|
||||||
<textarea id="f-notes" rows="2" placeholder="Modliste, Server-IP, To-dos …"></textarea>
|
<textarea id="f-notes" rows="2" placeholder="Modliste, Server-IP, To-dos …"></textarea>
|
||||||
@@ -265,6 +285,22 @@
|
|||||||
</label>
|
</label>
|
||||||
<small class="hint">Erscheint nur, während das Launcher-Fenster nicht im Vordergrund ist.</small>
|
<small class="hint">Erscheint nur, während das Launcher-Fenster nicht im Vordergrund ist.</small>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="settings-card">
|
||||||
|
<h5>Freundesystem</h5>
|
||||||
|
<label class="checkbox-field">
|
||||||
|
<input id="s-friends" type="checkbox" /> <span>Freundesystem aktivieren</span>
|
||||||
|
</label>
|
||||||
|
<small class="hint">Ausgeschaltet: du bist für niemanden sichtbar und kannst von niemandem als Freund gefunden/hinzugefügt werden. Bestehende Freundschaften bleiben erhalten, zeigen dich aber als offline.</small>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="settings-card">
|
||||||
|
<h5>Autostart</h5>
|
||||||
|
<label class="checkbox-field">
|
||||||
|
<input id="s-autostart" type="checkbox" /> <span>Mit Windows starten (im Infobereich)</span>
|
||||||
|
</label>
|
||||||
|
<small class="hint">Startet AeroMC beim Anmelden automatisch im Hintergrund. Solange aktiv, landet der Launcher beim Schließen (X) nur im Infobereich neben der Uhr, statt sich zu beenden – über das Symbol dort lässt er sich jederzeit wieder öffnen oder wirklich beenden.</small>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -634,6 +670,7 @@
|
|||||||
<option value="mod">Mods</option>
|
<option value="mod">Mods</option>
|
||||||
<option value="resourcepack">Ressourcenpakete</option>
|
<option value="resourcepack">Ressourcenpakete</option>
|
||||||
<option value="shader">Shader</option>
|
<option value="shader">Shader</option>
|
||||||
|
<option value="datapack">Datenpakete</option>
|
||||||
</select>
|
</select>
|
||||||
<input id="m-query" type="text" placeholder="Mods durchsuchen … (z. B. Sodium, JEI, Create)" />
|
<input id="m-query" type="text" placeholder="Mods durchsuchen … (z. B. Sodium, JEI, Create)" />
|
||||||
<button id="m-search-btn" class="btn btn-primary">Suchen</button>
|
<button id="m-search-btn" class="btn btn-primary">Suchen</button>
|
||||||
@@ -682,6 +719,11 @@
|
|||||||
<button id="m-add-file" class="btn btn-mini" data-ic="plus">Datei hinzufügen…</button>
|
<button id="m-add-file" class="btn btn-mini" data-ic="plus">Datei hinzufügen…</button>
|
||||||
<button id="m-check-updates" class="btn btn-mini">Nach Updates suchen</button>
|
<button id="m-check-updates" class="btn btn-mini">Nach Updates suchen</button>
|
||||||
<button id="m-apply-updates" class="btn btn-primary btn-mini hidden">Alle aktualisieren</button>
|
<button id="m-apply-updates" class="btn btn-primary btn-mini hidden">Alle aktualisieren</button>
|
||||||
|
<select id="m-installed-sort" title="Sortierung">
|
||||||
|
<option value="name-asc">Name (A-Z)</option>
|
||||||
|
<option value="name-desc">Name (Z-A)</option>
|
||||||
|
<option value="status">Status (aktiv zuerst)</option>
|
||||||
|
</select>
|
||||||
<span id="m-update-status" class="hint"></span>
|
<span id="m-update-status" class="hint"></span>
|
||||||
</div>
|
</div>
|
||||||
<div id="m-installed" class="mods-list"><div class="mods-empty">Noch keine Mods.</div></div>
|
<div id="m-installed" class="mods-list"><div class="mods-empty">Noch keine Mods.</div></div>
|
||||||
@@ -773,6 +815,123 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ===================== Dialog: Spielaufzeichnung ===================== -->
|
||||||
|
<div id="modal-record" class="modal hidden">
|
||||||
|
<div class="modal-card modal-lg">
|
||||||
|
<div class="modal-head">
|
||||||
|
<h3 id="record-title">Spielaufzeichnung</h3>
|
||||||
|
<button class="modal-close" data-close>✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div id="record-search" class="record-search">
|
||||||
|
<p id="record-search-text">Suche Spielfenster …</p>
|
||||||
|
<button id="record-search-retry" class="btn hidden">Erneut suchen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="record-picker" class="screens-grid hidden"></div>
|
||||||
|
|
||||||
|
<div id="record-stage" class="record-stage hidden">
|
||||||
|
<video id="record-preview" class="record-video" autoplay muted></video>
|
||||||
|
<div class="record-audio-bar">
|
||||||
|
<span class="hint">Video: <span id="record-video-summary">Nativ · Mittel</span></span>
|
||||||
|
<button id="record-video-edit" class="btn btn-mini">Video-Einstellungen…</button>
|
||||||
|
</div>
|
||||||
|
<div class="record-audio-bar">
|
||||||
|
<span class="hint">Audio: <span id="record-audio-summary">Systemton</span></span>
|
||||||
|
<button id="record-audio-edit" class="btn btn-mini">Audioquellen wählen…</button>
|
||||||
|
</div>
|
||||||
|
<div class="record-toolbar">
|
||||||
|
<div id="record-live-badge" class="record-live-badge hidden">
|
||||||
|
<span class="rec-dot"></span>
|
||||||
|
<span>Aufnahme läuft · <span id="record-timer">00:00</span></span>
|
||||||
|
</div>
|
||||||
|
<button id="record-toggle" class="btn btn-primary">Aufnahme starten</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-foot">
|
||||||
|
<button id="record-open-folder" class="btn" data-ic="folder">Ordner öffnen</button>
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<button class="btn" data-close>Schließen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===================== Dialog: Video-Einstellungen der Aufzeichnung ===================== -->
|
||||||
|
<div id="modal-record-video" class="modal hidden">
|
||||||
|
<div class="modal-card modal-sm">
|
||||||
|
<div class="modal-head">
|
||||||
|
<h3>Video-Einstellungen</h3>
|
||||||
|
<button class="modal-close" data-close>✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="field-row">
|
||||||
|
<label class="field">
|
||||||
|
<span>Auflösung</span>
|
||||||
|
<select id="record-resolution">
|
||||||
|
<option value="native">Nativ (Fenstergröße)</option>
|
||||||
|
<option value="1080">1080p</option>
|
||||||
|
<option value="720">720p</option>
|
||||||
|
<option value="480">480p</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>Qualität</span>
|
||||||
|
<select id="record-quality">
|
||||||
|
<option value="low">Niedrig (kleinere Datei)</option>
|
||||||
|
<option value="medium" selected>Mittel</option>
|
||||||
|
<option value="high">Hoch (größere Datei)</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>Bildrate</span>
|
||||||
|
<select id="record-fps">
|
||||||
|
<option value="30" selected>30 fps</option>
|
||||||
|
<option value="60">60 fps (größere Datei)</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p id="record-video-hint" class="hint hidden">Gilt erst für die nächste Aufnahme – während einer laufenden Aufnahme nicht änderbar.</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-foot">
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<button class="btn btn-primary" data-close>Fertig</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===================== Dialog: Audioquellen der Aufzeichnung ===================== -->
|
||||||
|
<div id="modal-record-audio" class="modal hidden">
|
||||||
|
<div class="modal-card modal-sm">
|
||||||
|
<div class="modal-head">
|
||||||
|
<h3>Audioquellen wählen</h3>
|
||||||
|
<button class="modal-close" data-close>✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p class="hint">Mehrfachauswahl möglich – alle angehakten Quellen werden zu einer Tonspur gemischt, Lautstärke je Quelle einstellbar.</p>
|
||||||
|
<div class="record-source-row">
|
||||||
|
<label class="checkbox-field">
|
||||||
|
<input id="record-src-system" type="checkbox" checked /> <span>Systemton</span>
|
||||||
|
</label>
|
||||||
|
<input id="record-vol-system" class="record-vol" type="range" min="0" max="100" value="100" title="Lautstärke" />
|
||||||
|
</div>
|
||||||
|
<div class="record-source-row">
|
||||||
|
<label class="checkbox-field">
|
||||||
|
<input id="record-src-game" type="checkbox" disabled /> <span>Nur Minecraft (isoliert)</span>
|
||||||
|
</label>
|
||||||
|
<input id="record-vol-game" class="record-vol" type="range" min="0" max="100" value="100" title="Lautstärke" />
|
||||||
|
</div>
|
||||||
|
<p id="record-src-game-hint" class="hint hidden">Kein laufender Minecraft-Prozess erkannt – bitte über den Launcher starten.</p>
|
||||||
|
<div id="record-mic-list" class="record-mic-list"></div>
|
||||||
|
<p class="hint" style="margin-top:10px">Systemton ist immer der komplette Mix (Spiel, Discord, Musik, Benachrichtigungen usw.). "Nur Minecraft" nimmt ausschließlich den Ton des Spiels auf (z. B. wenn nebenbei Musik läuft, die nicht mit rein soll) – beides lässt sich auch kombiniert mit einem Mikrofon anhaken.</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-foot">
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<button class="btn btn-primary" data-close>Fertig</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ===================== Dialog: Screenshots ===================== -->
|
<!-- ===================== Dialog: Screenshots ===================== -->
|
||||||
<div id="modal-screens" class="modal hidden">
|
<div id="modal-screens" class="modal hidden">
|
||||||
<div class="modal-card modal-lg">
|
<div class="modal-card modal-lg">
|
||||||
@@ -781,13 +940,30 @@
|
|||||||
<button class="modal-close" data-close>✕</button>
|
<button class="modal-close" data-close>✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="screens-toolbar">
|
<div class="mods-tabs">
|
||||||
<button id="screens-select-all" class="btn btn-mini">Alle auswählen</button>
|
<button id="screens-tab-photos" class="mtab on">Bildschirmfotos</button>
|
||||||
<button id="screens-select-none" class="btn btn-mini">Auswahl aufheben</button>
|
<button id="screens-tab-records" class="mtab">Aufzeichnungen</button>
|
||||||
<button id="screens-delete-sel" class="btn btn-danger btn-mini" disabled>Auswahl löschen</button>
|
</div>
|
||||||
<span id="screens-sel-hint" class="hint">Klick auf Vorschaubild zum Auswählen</span>
|
|
||||||
|
<div id="screens-pane-photos">
|
||||||
|
<div class="screens-toolbar">
|
||||||
|
<button id="screens-select-all" class="btn btn-mini">Alle auswählen</button>
|
||||||
|
<button id="screens-select-none" class="btn btn-mini">Auswahl aufheben</button>
|
||||||
|
<button id="screens-delete-sel" class="btn btn-danger btn-mini" disabled>Auswahl löschen</button>
|
||||||
|
<span id="screens-sel-hint" class="hint">Klick auf Vorschaubild zum Auswählen</span>
|
||||||
|
</div>
|
||||||
|
<div id="screens-grid" class="screens-grid"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="screens-pane-records" class="hidden">
|
||||||
|
<div class="screens-toolbar">
|
||||||
|
<button id="records-select-all" class="btn btn-mini">Alle auswählen</button>
|
||||||
|
<button id="records-select-none" class="btn btn-mini">Auswahl aufheben</button>
|
||||||
|
<button id="records-delete-sel" class="btn btn-danger btn-mini" disabled>Auswahl löschen</button>
|
||||||
|
<span id="records-sel-hint" class="hint">Klick auf Vorschaubild zum Auswählen, Doppelklick zum Abspielen</span>
|
||||||
|
</div>
|
||||||
|
<div id="records-grid" class="screens-grid"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="screens-grid" class="screens-grid"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-foot">
|
<div class="modal-foot">
|
||||||
<button id="screens-open" class="btn" data-ic="folder">Ordner</button>
|
<button id="screens-open" class="btn" data-ic="folder">Ordner</button>
|
||||||
@@ -797,6 +973,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ===================== Dialog: Freunde ===================== -->
|
||||||
|
<div id="modal-friends" class="modal hidden">
|
||||||
|
<div class="modal-card modal-sm">
|
||||||
|
<div class="modal-head">
|
||||||
|
<h3>Freunde</h3>
|
||||||
|
<button class="modal-close" data-close>✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p id="friends-disabled-hint" class="hint hidden">Das Freundesystem ist ausgeschaltet – aktiviere es in den Einstellungen, um Freunde zu sehen oder hinzuzufügen.</p>
|
||||||
|
<div id="friends-content">
|
||||||
|
<div class="field-row">
|
||||||
|
<input id="friends-add-input" type="text" placeholder="Minecraft-Name…" maxlength="16" />
|
||||||
|
<button id="friends-add-btn" class="btn btn-mini">Anfrage senden</button>
|
||||||
|
</div>
|
||||||
|
<div id="friends-requests-box"></div>
|
||||||
|
<div id="friends-list-box"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-foot">
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<button class="btn" data-close>Schließen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ===================== Dialog: Modpack aktualisieren ===================== -->
|
<!-- ===================== Dialog: Modpack aktualisieren ===================== -->
|
||||||
<div id="modal-modpack" class="modal hidden">
|
<div id="modal-modpack" class="modal hidden">
|
||||||
<div class="modal-card modal-sm">
|
<div class="modal-card modal-sm">
|
||||||
@@ -829,7 +1030,7 @@
|
|||||||
<div class="field-row" style="margin-top:12px">
|
<div class="field-row" style="margin-top:12px">
|
||||||
<label class="field"><span>Name</span><input id="srv-name" type="text" placeholder="z. B. Mein Server" /></label>
|
<label class="field"><span>Name</span><input id="srv-name" type="text" placeholder="z. B. Mein Server" /></label>
|
||||||
<label class="field"><span>Adresse</span><input id="srv-ip" type="text" placeholder="mc.example.de oder 192.168.1.50:25566" /></label>
|
<label class="field"><span>Adresse</span><input id="srv-ip" type="text" placeholder="mc.example.de oder 192.168.1.50:25566" /></label>
|
||||||
<button id="srv-add" class="btn btn-primary" style="align-self:flex-end">Hinzufügen</button>
|
<div class="field" style="flex:0 0 auto"><span> </span><button id="srv-add" class="btn btn-primary">Hinzufügen</button></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="srv-list" class="mods-list" style="margin-top:12px"></div>
|
<div id="srv-list" class="mods-list" style="margin-top:12px"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -872,19 +1073,19 @@
|
|||||||
<div class="skin-spalte">
|
<div class="skin-spalte">
|
||||||
<div class="skin-label">Aktuell</div>
|
<div class="skin-label">Aktuell</div>
|
||||||
<div class="skin-bild">
|
<div class="skin-bild">
|
||||||
<img id="skin-aktuell" alt="" class="hidden" />
|
<canvas id="skin-aktuell" class="hidden"></canvas>
|
||||||
<span id="skin-aktuell-leer" class="hint">wird geladen …</span>
|
<span id="skin-aktuell-leer" class="hint">wird geladen …</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="skin-spalte">
|
<div class="skin-spalte">
|
||||||
<div class="skin-label">Neu</div>
|
<div class="skin-label">Neu</div>
|
||||||
<div class="skin-bild">
|
<div class="skin-bild">
|
||||||
<img id="skin-neu" alt="" class="hidden" />
|
<canvas id="skin-neu" class="hidden"></canvas>
|
||||||
<span id="skin-neu-leer" class="hint">noch keine Datei gewählt</span>
|
<span id="skin-neu-leer" class="hint">noch keine Datei gewählt</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<small class="hint">Die Vorschau zeigt die Figur so, wie sie im Spiel aussieht. Die Armform wirkt sich auf die Darstellung aus.</small>
|
<small class="hint">Mit der Maus ziehen dreht die Figur, Mausrad zoomt. Die Armform wirkt sich auf die Darstellung aus.</small>
|
||||||
<div class="path-row" style="margin-top:12px">
|
<div class="path-row" style="margin-top:12px">
|
||||||
<input id="skin-pfad" type="text" readonly placeholder="PNG mit 64×64 Pixeln" />
|
<input id="skin-pfad" type="text" readonly placeholder="PNG mit 64×64 Pixeln" />
|
||||||
<button id="skin-pick" class="btn">Datei wählen…</button>
|
<button id="skin-pick" class="btn">Datei wählen…</button>
|
||||||
@@ -958,6 +1159,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ===================== Dialog: Lizenz ===================== -->
|
||||||
|
<div id="modal-license" class="modal hidden">
|
||||||
|
<div class="modal-card modal-lg">
|
||||||
|
<div class="modal-head">
|
||||||
|
<h3>Lizenz</h3>
|
||||||
|
<button class="modal-close" data-close>✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<pre id="license-text" class="crash-voll">Lade …</pre>
|
||||||
|
</div>
|
||||||
|
<div class="modal-foot">
|
||||||
|
<button class="btn" data-close>Schließen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ===================== Dialog: Update ===================== -->
|
<!-- ===================== Dialog: Update ===================== -->
|
||||||
<div id="modal-update" class="modal hidden">
|
<div id="modal-update" class="modal hidden">
|
||||||
<div class="modal-card modal-sm">
|
<div class="modal-card modal-sm">
|
||||||
@@ -1011,11 +1228,18 @@
|
|||||||
|
|
||||||
<div id="import-pane-modpack" class="import-pane hidden">
|
<div id="import-pane-modpack" class="import-pane hidden">
|
||||||
<p class="import-copy">Modpack aus dem Katalog installieren oder als Datei importieren.</p>
|
<p class="import-copy">Modpack aus dem Katalog installieren oder als Datei importieren.</p>
|
||||||
<p class="hint">Suche läuft über Modrinth. Datei-Import unterstützt zusätzlich CurseForge-ZIP (manifest.json) – dafür ist ein API-Key oder eine Weiterleitung in den Netzwerk-Einstellungen nötig.</p>
|
<p class="hint">Katalog-Suche läuft über Modrinth oder CurseForge. Für CurseForge wird ein eigener API-Key in den Netzwerk-Einstellungen gebraucht (die Weiterleitung reicht nur für den Datei-Import). Datei-Import unterstützt zusätzlich CurseForge-ZIP (manifest.json).</p>
|
||||||
|
|
||||||
<div class="field-row" style="margin-top:12px;align-items:flex-end">
|
<div class="field-row" style="margin-top:12px;align-items:flex-end">
|
||||||
|
<label class="field" style="width:150px">
|
||||||
|
<span>Quelle</span>
|
||||||
|
<select id="mp-source">
|
||||||
|
<option value="modrinth">Modrinth</option>
|
||||||
|
<option value="curseforge">CurseForge</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
<label class="field" style="flex:1">
|
<label class="field" style="flex:1">
|
||||||
<span>Suche (Modrinth)</span>
|
<span>Suche</span>
|
||||||
<input id="mp-query" type="text" placeholder="z. B. Fabulously Optimized, Create …" autocomplete="off" />
|
<input id="mp-query" type="text" placeholder="z. B. Fabulously Optimized, Create …" autocomplete="off" />
|
||||||
</label>
|
</label>
|
||||||
<label class="field" style="width:140px">
|
<label class="field" style="width:140px">
|
||||||
@@ -1225,6 +1449,7 @@
|
|||||||
<button class="ctx-item" data-act="p-mods">Mods anzeigen</button>
|
<button class="ctx-item" data-act="p-mods">Mods anzeigen</button>
|
||||||
<button class="ctx-item" data-act="p-worlds">Welten verwalten</button>
|
<button class="ctx-item" data-act="p-worlds">Welten verwalten</button>
|
||||||
<button class="ctx-item" data-act="p-screens">Bildschirmfotos verwalten</button>
|
<button class="ctx-item" data-act="p-screens">Bildschirmfotos verwalten</button>
|
||||||
|
<button class="ctx-item" data-act="p-record">Spiel aufzeichnen</button>
|
||||||
<button class="ctx-item" data-act="p-logs">Logdateien ansehen</button>
|
<button class="ctx-item" data-act="p-logs">Logdateien ansehen</button>
|
||||||
<button class="ctx-item" data-act="p-servers">Server verwalten</button>
|
<button class="ctx-item" data-act="p-servers">Server verwalten</button>
|
||||||
<div class="ctx-sep"></div>
|
<div class="ctx-sep"></div>
|
||||||
|
|||||||
@@ -464,6 +464,37 @@ async function prepareAndLaunch(opts, onProgress) {
|
|||||||
classpath.unshift(path.join(libDir, rel.replace(/\//g, path.sep)));
|
classpath.unshift(path.join(libDir, rel.replace(/\//g, path.sep)));
|
||||||
}
|
}
|
||||||
effectiveVj = mergeLoader(vj, fvj);
|
effectiveVj = mergeLoader(vj, fvj);
|
||||||
|
|
||||||
|
} else if (opts.loader === 'optifine') {
|
||||||
|
/*
|
||||||
|
* OptiFine hat keine öffentliche Installations-API (Downloads laufen bewusst
|
||||||
|
* über eine Werbeseite). AeroMC lädt hier nichts selbst herunter – der Nutzer
|
||||||
|
* hat den offiziellen Installer bereits einmal sichtbar laufen lassen (siehe
|
||||||
|
* optifine:setup/optifine:confirm in main.js), der dabei ein normales
|
||||||
|
* Versions-Profil unter <sharedDir>/versions/<loaderVersion>/ abgelegt hat,
|
||||||
|
* genau wie Fabric/Forge es tun. Wir lesen es nur noch ein und mergen es.
|
||||||
|
*/
|
||||||
|
const ofId = opts.loaderVersion;
|
||||||
|
if (!ofId) {
|
||||||
|
return { ok: false, message: 'Für diese Instanz ist keine OptiFine-Version hinterlegt. Bitte im Bearbeiten-Dialog einrichten.' };
|
||||||
|
}
|
||||||
|
const ofJsonPath = path.join(opts.sharedDir, 'versions', ofId, ofId + '.json');
|
||||||
|
if (!fs.existsSync(ofJsonPath)) {
|
||||||
|
return { ok: false, message: `OptiFine-Profil "${ofId}" wurde nicht gefunden. Bitte die Einrichtung im Bearbeiten-Dialog wiederholen.` };
|
||||||
|
}
|
||||||
|
let ofvj;
|
||||||
|
try { ofvj = JSON.parse(fs.readFileSync(ofJsonPath, 'utf8')); }
|
||||||
|
catch (err) { return { ok: false, message: 'OptiFine-Profil ist beschädigt: ' + err.message }; }
|
||||||
|
for (const lib of ofvj.libraries || []) {
|
||||||
|
const art = lib.downloads && lib.downloads.artifact;
|
||||||
|
const rel = (art && art.path) || forge.mavenPath(lib.name);
|
||||||
|
const dest = path.join(libDir, rel.replace(/\//g, path.sep));
|
||||||
|
if (!fs.existsSync(dest) && art && art.url) {
|
||||||
|
await downloadIfNeeded(art.url, dest, art.sha1 || null);
|
||||||
|
}
|
||||||
|
classpath.unshift(dest);
|
||||||
|
}
|
||||||
|
effectiveVj = mergeLoader(vj, ofvj);
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalClasspath = dedupeClasspath(classpath, opts.sharedDir);
|
const finalClasspath = dedupeClasspath(classpath, opts.sharedDir);
|
||||||
|
|||||||
+1181
-106
File diff suppressed because it is too large
Load Diff
+216
-216
@@ -1,217 +1,217 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
const PROFILE_FILES = [
|
const PROFILE_FILES = [
|
||||||
'launcher_profiles.json',
|
'launcher_profiles.json',
|
||||||
'launcher_profiles_microsoft_store.json',
|
'launcher_profiles_microsoft_store.json',
|
||||||
'launcher_profiles_microsoft_store_2.json',
|
'launcher_profiles_microsoft_store_2.json',
|
||||||
];
|
];
|
||||||
|
|
||||||
const ROOT_COPY_DIRS = new Set([
|
const ROOT_COPY_DIRS = new Set([
|
||||||
'config', 'defaultconfigs', 'kubejs', 'mods', 'resourcepacks', 'screenshots', 'shaderpacks', 'saves',
|
'config', 'defaultconfigs', 'kubejs', 'mods', 'resourcepacks', 'screenshots', 'shaderpacks', 'saves',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const ROOT_COPY_FILE_PATTERNS = [
|
const ROOT_COPY_FILE_PATTERNS = [
|
||||||
/^options.*\.(txt|of)$/i,
|
/^options.*\.(txt|of)$/i,
|
||||||
/^servers\.dat(?:_old)?$/i,
|
/^servers\.dat(?:_old)?$/i,
|
||||||
/^usercache\.json$/i,
|
/^usercache\.json$/i,
|
||||||
/^tl_skin_cape\.json$/i,
|
/^tl_skin_cape\.json$/i,
|
||||||
/^journeymap.*\.(json|txt|cfg)$/i,
|
/^journeymap.*\.(json|txt|cfg)$/i,
|
||||||
];
|
];
|
||||||
|
|
||||||
function readJson(file, fallback) {
|
function readJson(file, fallback) {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
|
return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
|
||||||
} catch {
|
} catch {
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveLauncherRoot(inputPath) {
|
function resolveLauncherRoot(inputPath) {
|
||||||
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
||||||
const defaultRoot = path.join(appData, '.minecraft');
|
const defaultRoot = path.join(appData, '.minecraft');
|
||||||
const candidates = [];
|
const candidates = [];
|
||||||
|
|
||||||
if (inputPath && fs.existsSync(inputPath)) candidates.push(inputPath);
|
if (inputPath && fs.existsSync(inputPath)) candidates.push(inputPath);
|
||||||
candidates.push(defaultRoot);
|
candidates.push(defaultRoot);
|
||||||
|
|
||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
for (const profileFile of PROFILE_FILES) {
|
for (const profileFile of PROFILE_FILES) {
|
||||||
if (fs.existsSync(path.join(candidate, profileFile))) return candidate;
|
if (fs.existsSync(path.join(candidate, profileFile))) return candidate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveProfileFile(rootDir) {
|
function resolveProfileFile(rootDir) {
|
||||||
return PROFILE_FILES.map((name) => path.join(rootDir, name)).find((file) => fs.existsSync(file)) || null;
|
return PROFILE_FILES.map((name) => path.join(rootDir, name)).find((file) => fs.existsSync(file)) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeLoaderVersion(loader, rawValue, mcVersion) {
|
function normalizeLoaderVersion(loader, rawValue, mcVersion) {
|
||||||
let value = String(rawValue || '').trim();
|
let value = String(rawValue || '').trim();
|
||||||
if (!value) return '';
|
if (!value) return '';
|
||||||
|
|
||||||
if (loader === 'forge') {
|
if (loader === 'forge') {
|
||||||
value = value.replace(/^forge-/i, '');
|
value = value.replace(/^forge-/i, '');
|
||||||
if (mcVersion && value.startsWith(mcVersion + '-')) value = value.slice(mcVersion.length + 1);
|
if (mcVersion && value.startsWith(mcVersion + '-')) value = value.slice(mcVersion.length + 1);
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
if (loader === 'neoforge') return value.replace(/^neoforge-/i, '');
|
if (loader === 'neoforge') return value.replace(/^neoforge-/i, '');
|
||||||
if (loader === 'fabric') {
|
if (loader === 'fabric') {
|
||||||
value = value.replace(/^fabric(?:-loader)?-/i, '');
|
value = value.replace(/^fabric(?:-loader)?-/i, '');
|
||||||
if (mcVersion && value.endsWith('-' + mcVersion)) value = value.slice(0, -1 * (mcVersion.length + 1));
|
if (mcVersion && value.endsWith('-' + mcVersion)) value = value.slice(0, -1 * (mcVersion.length + 1));
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
if (loader === 'quilt') {
|
if (loader === 'quilt') {
|
||||||
value = value.replace(/^quilt(?:-loader)?-/i, '');
|
value = value.replace(/^quilt(?:-loader)?-/i, '');
|
||||||
if (mcVersion && value.endsWith('-' + mcVersion)) value = value.slice(0, -1 * (mcVersion.length + 1));
|
if (mcVersion && value.endsWith('-' + mcVersion)) value = value.slice(0, -1 * (mcVersion.length + 1));
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseLastVersionId(lastVersionId) {
|
function parseLastVersionId(lastVersionId) {
|
||||||
const raw = String(lastVersionId || '').trim();
|
const raw = String(lastVersionId || '').trim();
|
||||||
if (!raw) return { version: '', loader: 'vanilla', loaderVersion: '' };
|
if (!raw) return { version: '', loader: 'vanilla', loaderVersion: '' };
|
||||||
|
|
||||||
let match = raw.match(/^fabric-loader-([^-]+(?:\.[^-]+)*)-(.+)$/i);
|
let match = raw.match(/^fabric-loader-([^-]+(?:\.[^-]+)*)-(.+)$/i);
|
||||||
if (match) return { version: match[2], loader: 'fabric', loaderVersion: normalizeLoaderVersion('fabric', match[1], match[2]) };
|
if (match) return { version: match[2], loader: 'fabric', loaderVersion: normalizeLoaderVersion('fabric', match[1], match[2]) };
|
||||||
|
|
||||||
match = raw.match(/^quilt-loader-([^-]+(?:\.[^-]+)*)-(.+)$/i);
|
match = raw.match(/^quilt-loader-([^-]+(?:\.[^-]+)*)-(.+)$/i);
|
||||||
if (match) return { version: match[2], loader: 'quilt', loaderVersion: normalizeLoaderVersion('quilt', match[1], match[2]) };
|
if (match) return { version: match[2], loader: 'quilt', loaderVersion: normalizeLoaderVersion('quilt', match[1], match[2]) };
|
||||||
|
|
||||||
match = raw.match(/^(.+)-forge-([\w.-]+)$/i);
|
match = raw.match(/^(.+)-forge-([\w.-]+)$/i);
|
||||||
if (match) return { version: match[1], loader: 'forge', loaderVersion: normalizeLoaderVersion('forge', match[2], match[1]) };
|
if (match) return { version: match[1], loader: 'forge', loaderVersion: normalizeLoaderVersion('forge', match[2], match[1]) };
|
||||||
|
|
||||||
match = raw.match(/^forge-(.+)-([\w.-]+)$/i);
|
match = raw.match(/^forge-(.+)-([\w.-]+)$/i);
|
||||||
if (match) return { version: match[1], loader: 'forge', loaderVersion: normalizeLoaderVersion('forge', match[2], match[1]) };
|
if (match) return { version: match[1], loader: 'forge', loaderVersion: normalizeLoaderVersion('forge', match[2], match[1]) };
|
||||||
|
|
||||||
match = raw.match(/^(.+)-neoforge-([\w.-]+)$/i);
|
match = raw.match(/^(.+)-neoforge-([\w.-]+)$/i);
|
||||||
if (match) return { version: match[1], loader: 'neoforge', loaderVersion: normalizeLoaderVersion('neoforge', match[2], match[1]) };
|
if (match) return { version: match[1], loader: 'neoforge', loaderVersion: normalizeLoaderVersion('neoforge', match[2], match[1]) };
|
||||||
|
|
||||||
if (/^\d+(?:\.\d+)+(?:-[\w.]+)?$/i.test(raw)) return { version: raw, loader: 'vanilla', loaderVersion: '' };
|
if (/^\d+(?:\.\d+)+(?:-[\w.]+)?$/i.test(raw)) return { version: raw, loader: 'vanilla', loaderVersion: '' };
|
||||||
return { version: raw, loader: 'vanilla', loaderVersion: '' };
|
return { version: raw, loader: 'vanilla', loaderVersion: '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseJavaSettings(javaDir, javaArgs) {
|
function parseJavaSettings(javaDir, javaArgs) {
|
||||||
const args = String(javaArgs || '').trim();
|
const args = String(javaArgs || '').trim();
|
||||||
const minMatch = args.match(/(?:^|\s)-Xms(\d+)([mMgG])/);
|
const minMatch = args.match(/(?:^|\s)-Xms(\d+)([mMgG])/);
|
||||||
const maxMatch = args.match(/(?:^|\s)-Xmx(\d+)([mMgG])/);
|
const maxMatch = args.match(/(?:^|\s)-Xmx(\d+)([mMgG])/);
|
||||||
const toMb = (match) => {
|
const toMb = (match) => {
|
||||||
if (!match) return null;
|
if (!match) return null;
|
||||||
const num = Number(match[1]);
|
const num = Number(match[1]);
|
||||||
if (!Number.isFinite(num)) return null;
|
if (!Number.isFinite(num)) return null;
|
||||||
return match[2].toLowerCase() === 'g' ? num * 1024 : num;
|
return match[2].toLowerCase() === 'g' ? num * 1024 : num;
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
path: String(javaDir || '').trim(),
|
path: String(javaDir || '').trim(),
|
||||||
minMemMb: toMb(minMatch),
|
minMemMb: toMb(minMatch),
|
||||||
maxMemMb: toMb(maxMatch),
|
maxMemMb: toMb(maxMatch),
|
||||||
extraArgs: args
|
extraArgs: args
|
||||||
.replace(/(?:^|\s)-Xms\d+[mMgG]/g, ' ')
|
.replace(/(?:^|\s)-Xms\d+[mMgG]/g, ' ')
|
||||||
.replace(/(?:^|\s)-Xmx\d+[mMgG]/g, ' ')
|
.replace(/(?:^|\s)-Xmx\d+[mMgG]/g, ' ')
|
||||||
.replace(/\s+/g, ' ')
|
.replace(/\s+/g, ' ')
|
||||||
.trim(),
|
.trim(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function isImportableProfile(profile) {
|
function isImportableProfile(profile) {
|
||||||
const type = String((profile && profile.type) || '').trim().toLowerCase();
|
const type = String((profile && profile.type) || '').trim().toLowerCase();
|
||||||
if (type === 'latest-release' || type === 'latest-snapshot') return false;
|
if (type === 'latest-release' || type === 'latest-snapshot') return false;
|
||||||
const name = String((profile && profile.name) || '').trim();
|
const name = String((profile && profile.name) || '').trim();
|
||||||
const lastVersionId = String((profile && profile.lastVersionId) || '').trim();
|
const lastVersionId = String((profile && profile.lastVersionId) || '').trim();
|
||||||
return !!(name || lastVersionId);
|
return !!(name || lastVersionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldCopyRootEntry(name) {
|
function shouldCopyRootEntry(name) {
|
||||||
if (ROOT_COPY_DIRS.has(name)) return true;
|
if (ROOT_COPY_DIRS.has(name)) return true;
|
||||||
return ROOT_COPY_FILE_PATTERNS.some((pattern) => pattern.test(name));
|
return ROOT_COPY_FILE_PATTERNS.some((pattern) => pattern.test(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
function scan(inputPath) {
|
function scan(inputPath) {
|
||||||
const rootDir = resolveLauncherRoot(inputPath);
|
const rootDir = resolveLauncherRoot(inputPath);
|
||||||
if (!rootDir) return { ok: false, reason: 'not-found' };
|
if (!rootDir) return { ok: false, reason: 'not-found' };
|
||||||
|
|
||||||
const profileFile = resolveProfileFile(rootDir);
|
const profileFile = resolveProfileFile(rootDir);
|
||||||
const json = readJson(profileFile, {});
|
const json = readJson(profileFile, {});
|
||||||
const profiles = json.profiles || {};
|
const profiles = json.profiles || {};
|
||||||
const list = [];
|
const list = [];
|
||||||
const defaultRoot = path.normalize(rootDir).toLowerCase();
|
const defaultRoot = path.normalize(rootDir).toLowerCase();
|
||||||
|
|
||||||
for (const [id, profile] of Object.entries(profiles)) {
|
for (const [id, profile] of Object.entries(profiles)) {
|
||||||
if (!isImportableProfile(profile)) continue;
|
if (!isImportableProfile(profile)) continue;
|
||||||
|
|
||||||
const name = String(profile.name || '').trim() || String(profile.lastVersionId || '').trim() || id;
|
const name = String(profile.name || '').trim() || String(profile.lastVersionId || '').trim() || id;
|
||||||
const parsed = parseLastVersionId(profile.lastVersionId);
|
const parsed = parseLastVersionId(profile.lastVersionId);
|
||||||
const gameDir = path.normalize(String(profile.gameDir || rootDir));
|
const gameDir = path.normalize(String(profile.gameDir || rootDir));
|
||||||
const java = parseJavaSettings(profile.javaDir, profile.javaArgs);
|
const java = parseJavaSettings(profile.javaDir, profile.javaArgs);
|
||||||
|
|
||||||
list.push({
|
list.push({
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
group: '',
|
group: '',
|
||||||
notes: 'Importiert aus dem Minecraft Launcher',
|
notes: 'Importiert aus dem Minecraft Launcher',
|
||||||
version: parsed.version,
|
version: parsed.version,
|
||||||
loader: parsed.loader,
|
loader: parsed.loader,
|
||||||
loaderVersion: parsed.loaderVersion,
|
loaderVersion: parsed.loaderVersion,
|
||||||
javaPath: java.path,
|
javaPath: java.path,
|
||||||
minMemMb: java.minMemMb,
|
minMemMb: java.minMemMb,
|
||||||
maxMemMb: java.maxMemMb,
|
maxMemMb: java.maxMemMb,
|
||||||
extraJavaArgs: java.extraArgs,
|
extraJavaArgs: java.extraArgs,
|
||||||
gameDir,
|
gameDir,
|
||||||
hasGameData: fs.existsSync(gameDir),
|
hasGameData: fs.existsSync(gameDir),
|
||||||
usesDefaultGameDir: gameDir.toLowerCase() === defaultRoot,
|
usesDefaultGameDir: gameDir.toLowerCase() === defaultRoot,
|
||||||
rootDir,
|
rootDir,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
list.sort((a, b) => a.name.localeCompare(b.name));
|
list.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
return { ok: true, rootDir, count: list.length, instances: list };
|
return { ok: true, rootDir, count: list.length, instances: list };
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyInto(sourceDir, targetDir, usesDefaultGameDir) {
|
function copyInto(sourceDir, targetDir, usesDefaultGameDir) {
|
||||||
fs.mkdirSync(targetDir, { recursive: true });
|
fs.mkdirSync(targetDir, { recursive: true });
|
||||||
for (const name of fs.readdirSync(sourceDir)) {
|
for (const name of fs.readdirSync(sourceDir)) {
|
||||||
if (/^launcher_profiles.*\.json$/i.test(name)) continue;
|
if (/^launcher_profiles.*\.json$/i.test(name)) continue;
|
||||||
if (name === 'versions' || name === 'libraries' || name === 'assets' || name === 'runtime' || name === 'webcache2') continue;
|
if (name === 'versions' || name === 'libraries' || name === 'assets' || name === 'runtime' || name === 'webcache2') continue;
|
||||||
if (usesDefaultGameDir && !shouldCopyRootEntry(name)) continue;
|
if (usesDefaultGameDir && !shouldCopyRootEntry(name)) continue;
|
||||||
fs.cpSync(path.join(sourceDir, name), path.join(targetDir, name), { recursive: true });
|
fs.cpSync(path.join(sourceDir, name), path.join(targetDir, name), { recursive: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function importOne(store, entry, copyData = true) {
|
function importOne(store, entry, copyData = true) {
|
||||||
const created = store.createOrReplaceImportedInstance({
|
const created = store.createOrReplaceImportedInstance({
|
||||||
name: entry.name,
|
name: entry.name,
|
||||||
group: entry.group,
|
group: entry.group,
|
||||||
notes: entry.notes,
|
notes: entry.notes,
|
||||||
minecraft: {
|
minecraft: {
|
||||||
version: entry.version,
|
version: entry.version,
|
||||||
loader: entry.loader,
|
loader: entry.loader,
|
||||||
loaderVersion: entry.loaderVersion,
|
loaderVersion: entry.loaderVersion,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const patch = {};
|
const patch = {};
|
||||||
if (entry.javaPath || entry.minMemMb || entry.maxMemMb || entry.extraJavaArgs) {
|
if (entry.javaPath || entry.minMemMb || entry.maxMemMb || entry.extraJavaArgs) {
|
||||||
patch.java = {
|
patch.java = {
|
||||||
path: entry.javaPath || '',
|
path: entry.javaPath || '',
|
||||||
minMemMb: entry.minMemMb || null,
|
minMemMb: entry.minMemMb || null,
|
||||||
maxMemMb: entry.maxMemMb || null,
|
maxMemMb: entry.maxMemMb || null,
|
||||||
extraArgs: entry.extraJavaArgs || '',
|
extraArgs: entry.extraJavaArgs || '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (Object.keys(patch).length) store.updateInstance(created.id, patch);
|
if (Object.keys(patch).length) store.updateInstance(created.id, patch);
|
||||||
|
|
||||||
let copied = false;
|
let copied = false;
|
||||||
if (copyData && entry.gameDir && fs.existsSync(entry.gameDir)) {
|
if (copyData && entry.gameDir && fs.existsSync(entry.gameDir)) {
|
||||||
copyInto(entry.gameDir, store.gameDir(created.id), !!entry.usesDefaultGameDir);
|
copyInto(entry.gameDir, store.gameDir(created.id), !!entry.usesDefaultGameDir);
|
||||||
copied = true;
|
copied = true;
|
||||||
}
|
}
|
||||||
return { id: created.id, name: created.name, copied };
|
return { id: created.id, name: created.name, copied };
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { scan, importOne, parseLastVersionId, resolveLauncherRoot };
|
module.exports = { scan, importOne, parseLastVersionId, resolveLauncherRoot };
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
// AudioWorklet-Prozessor für die isolierte "Nur Minecraft"-Tonaufnahme.
|
||||||
|
//
|
||||||
|
// loopback-capture liefert PCM-Häppchen unregelmäßig getaktet über IPC (mal
|
||||||
|
// etwas früher, mal etwas später). Würde man sie direkt nacheinander als
|
||||||
|
// einzelne AudioBufferSourceNode-Aufrufe abspielen, würde jede kleine
|
||||||
|
// Verzögerung als hörbares Knacksen/Aussetzer auffallen. Stattdessen landen
|
||||||
|
// ankommende Häppchen hier in einem Ringpuffer; der Audio-Rendering-Thread
|
||||||
|
// liest in gleichmäßigem Takt (process() läuft alle 128 Samples) daraus.
|
||||||
|
// Kleine Zeitschwankungen bei der Anlieferung werden so unhörbar
|
||||||
|
// abgefedert. Läuft der Puffer doch mal leer, gibt es kurz Stille statt
|
||||||
|
// eines Knacksens.
|
||||||
|
class PcmRingBufferProcessor extends AudioWorkletProcessor {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.capacity = sampleRate * 2; // 2 Sekunden Puffer je Kanal
|
||||||
|
this.ring = [new Float32Array(this.capacity), new Float32Array(this.capacity)];
|
||||||
|
this.writeIdx = 0;
|
||||||
|
this.readIdx = 0;
|
||||||
|
this.available = 0; // gepufferte Frames (nicht Samples)
|
||||||
|
this.port.onmessage = (e) => this._enqueue(e.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
_enqueue([left, right]) {
|
||||||
|
const n = left.length;
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const idx = (this.writeIdx + i) % this.capacity;
|
||||||
|
this.ring[0][idx] = left[i];
|
||||||
|
this.ring[1][idx] = right[i];
|
||||||
|
}
|
||||||
|
this.writeIdx = (this.writeIdx + n) % this.capacity;
|
||||||
|
this.available = Math.min(this.capacity, this.available + n);
|
||||||
|
}
|
||||||
|
|
||||||
|
process(_inputs, outputs) {
|
||||||
|
const output = outputs[0];
|
||||||
|
const frames = output[0].length;
|
||||||
|
const playable = Math.min(frames, this.available);
|
||||||
|
for (let i = 0; i < playable; i++) {
|
||||||
|
const idx = (this.readIdx + i) % this.capacity;
|
||||||
|
output[0][i] = this.ring[0][idx];
|
||||||
|
output[1][i] = this.ring[1][idx];
|
||||||
|
}
|
||||||
|
for (let i = playable; i < frames; i++) { output[0][i] = 0; output[1][i] = 0; }
|
||||||
|
this.readIdx = (this.readIdx + playable) % this.capacity;
|
||||||
|
this.available -= playable;
|
||||||
|
return true; // Prozessor am Leben halten, auch wenn gerade nichts ankommt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerProcessor('pcm-player', PcmRingBufferProcessor);
|
||||||
+96
-3
@@ -7,6 +7,15 @@
|
|||||||
|
|
||||||
const { contextBridge, ipcRenderer } = require('electron');
|
const { contextBridge, ipcRenderer } = require('electron');
|
||||||
|
|
||||||
|
// Debug-Hilfe: JS-Fehler im Renderer landen im Terminal des Hauptprozesses,
|
||||||
|
// damit man sie auch ohne offene DevTools sieht
|
||||||
|
window.addEventListener('error', (e) => {
|
||||||
|
ipcRenderer.send('debug:rendererError', (e.error && e.error.stack) || e.message);
|
||||||
|
});
|
||||||
|
window.addEventListener('unhandledrejection', (e) => {
|
||||||
|
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'),
|
||||||
@@ -19,6 +28,9 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
// 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),
|
||||||
|
optifineSetup: (id) => ipcRenderer.invoke('optifine:setup', 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),
|
||||||
@@ -87,18 +99,24 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
return () => ipcRenderer.removeListener('backup:progress', l);
|
return () => ipcRenderer.removeListener('backup:progress', l);
|
||||||
},
|
},
|
||||||
importInstance: () => ipcRenderer.invoke('instances:import'),
|
importInstance: () => ipcRenderer.invoke('instances:import'),
|
||||||
createShortcut: (id) => ipcRenderer.invoke('instances:shortcut', id),
|
createShortcut: (id, customName) => ipcRenderer.invoke('instances:shortcut', id, customName),
|
||||||
// Start über eine Desktop-Verknüpfung ("--launch <id>")
|
serversShortcut: (id, ip, name, customName) => ipcRenderer.invoke('servers:shortcut', id, ip, name, customName),
|
||||||
|
// Start über eine Desktop-Verknüpfung ("--launch=<id>", optional "--server=<ip>")
|
||||||
onLaunchRequest: (cb) => {
|
onLaunchRequest: (cb) => {
|
||||||
const l = (_e, id) => cb(id);
|
const l = (_e, payload) => cb(payload);
|
||||||
ipcRenderer.on('instance:launch-request', l);
|
ipcRenderer.on('instance:launch-request', l);
|
||||||
return () => ipcRenderer.removeListener('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,
|
||||||
|
// weil er ankommt, bevor die Oberfläche fertig eingerichtet ist
|
||||||
|
notifyReady: () => ipcRenderer.send('renderer:ready'),
|
||||||
importModpack: () => ipcRenderer.invoke('modpack:import'),
|
importModpack: () => ipcRenderer.invoke('modpack:import'),
|
||||||
// Datei per Drag&Drop aufs Fenster gezogen (.jar/.mrpack/.zip)
|
// Datei per Drag&Drop aufs Fenster gezogen (.jar/.mrpack/.zip)
|
||||||
filesDrop: (filePath, targetId, targetArt) => ipcRenderer.invoke('files:drop', filePath, targetId, targetArt),
|
filesDrop: (filePath, targetId, targetArt) => ipcRenderer.invoke('files:drop', filePath, targetId, targetArt),
|
||||||
modpackSearch: (opts) => ipcRenderer.invoke('modpack:search', opts),
|
modpackSearch: (opts) => ipcRenderer.invoke('modpack:search', opts),
|
||||||
modpackInstallFromModrinth: (project) => ipcRenderer.invoke('modpack:installFromModrinth', project),
|
modpackInstallFromModrinth: (project) => ipcRenderer.invoke('modpack:installFromModrinth', project),
|
||||||
|
modpackInstallFromCurseForge: (project) => ipcRenderer.invoke('modpack:installFromCurseForge', project),
|
||||||
modpackCheck: (id) => ipcRenderer.invoke('modpack:check', id),
|
modpackCheck: (id) => ipcRenderer.invoke('modpack:check', id),
|
||||||
modpackUpdate: (id, versionId) => ipcRenderer.invoke('modpack:update', id, versionId),
|
modpackUpdate: (id, versionId) => ipcRenderer.invoke('modpack:update', id, versionId),
|
||||||
onLaunchProgress: (cb) => {
|
onLaunchProgress: (cb) => {
|
||||||
@@ -231,4 +249,79 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
pickFolder: (title) => ipcRenderer.invoke('dialog:pickFolder', title),
|
pickFolder: (title) => ipcRenderer.invoke('dialog:pickFolder', title),
|
||||||
pickJava: () => ipcRenderer.invoke('dialog:pickJava'),
|
pickJava: () => ipcRenderer.invoke('dialog:pickJava'),
|
||||||
appInfo: () => ipcRenderer.invoke('app:info'),
|
appInfo: () => ipcRenderer.invoke('app:info'),
|
||||||
|
appLicense: () => ipcRenderer.invoke('app:license'),
|
||||||
|
|
||||||
|
// Einstellungen/Konsole als eigenes, frei verschiebbares Fenster
|
||||||
|
popoutOpen: (modalId, params) => ipcRenderer.send('popout:open', modalId, params),
|
||||||
|
popoutHide: () => ipcRenderer.send('popout:hide'),
|
||||||
|
onPopoutReopen: (cb) => {
|
||||||
|
const l = (_e, params) => cb(params);
|
||||||
|
ipcRenderer.on('popout:reopen', l);
|
||||||
|
return () => ipcRenderer.removeListener('popout:reopen', l);
|
||||||
|
},
|
||||||
|
onSettingsChanged: (cb) => {
|
||||||
|
const l = (_e, s) => cb(s);
|
||||||
|
ipcRenderer.on('settings:changed', l);
|
||||||
|
return () => ipcRenderer.removeListener('settings:changed', l);
|
||||||
|
},
|
||||||
|
onInstancesChanged: (cb) => {
|
||||||
|
const l = () => cb();
|
||||||
|
ipcRenderer.on('instances:changed', l);
|
||||||
|
return () => ipcRenderer.removeListener('instances:changed', l);
|
||||||
|
},
|
||||||
|
onSkinChanged: (cb) => {
|
||||||
|
const l = () => cb();
|
||||||
|
ipcRenderer.on('skin:changed', l);
|
||||||
|
return () => ipcRenderer.removeListener('skin:changed', l);
|
||||||
|
},
|
||||||
|
updateGetPending: () => ipcRenderer.invoke('update:getPending'),
|
||||||
|
|
||||||
|
// Spielaufzeichnung
|
||||||
|
recordingListWindows: () => ipcRenderer.invoke('recording:listWindows'),
|
||||||
|
recordingGetAudioSourceId: () => ipcRenderer.invoke('recording:getAudioSourceId'),
|
||||||
|
recordingSave: (instanceName, buffer, serverLabel, fps) => ipcRenderer.invoke('recording:save', instanceName, buffer, serverLabel, fps),
|
||||||
|
recordingOpenFolder: (instanceName) => ipcRenderer.invoke('recording:openFolder', instanceName),
|
||||||
|
recordingList: (instanceName) => ipcRenderer.invoke('recording:list', instanceName),
|
||||||
|
recordingThumbnail: (instanceName, dateiname) => ipcRenderer.invoke('recording:thumbnail', instanceName, dateiname),
|
||||||
|
recordingDelete: (instanceName, dateiname) => ipcRenderer.invoke('recording:delete', instanceName, dateiname),
|
||||||
|
recordingOpen: (instanceName, dateiname) => ipcRenderer.invoke('recording:open', instanceName, dateiname),
|
||||||
|
|
||||||
|
// Freundesystem
|
||||||
|
friendsEnabled: () => ipcRenderer.invoke('friends:enabled'),
|
||||||
|
friendsList: () => ipcRenderer.invoke('friends:list'),
|
||||||
|
friendsRequests: () => ipcRenderer.invoke('friends:requests'),
|
||||||
|
friendsAdd: (targetUsername) => ipcRenderer.invoke('friends:add', targetUsername),
|
||||||
|
friendsRespond: (otherUuid, accept) => ipcRenderer.invoke('friends:respond', otherUuid, accept),
|
||||||
|
friendsRemove: (otherUuid) => ipcRenderer.invoke('friends:remove', otherUuid),
|
||||||
|
friendsSentRequests: () => ipcRenderer.invoke('friends:sentRequests'),
|
||||||
|
friendsCancel: (otherUuid) => ipcRenderer.invoke('friends:cancel', otherUuid),
|
||||||
|
friendsSetNickname: (uuid, nickname) => ipcRenderer.invoke('friends:setNickname', uuid, nickname),
|
||||||
|
friendsRequestCount: () => ipcRenderer.invoke('friends:requestCount'),
|
||||||
|
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);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
+101
-101
@@ -1,101 +1,101 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const DEFAULT_GRACE_MS = 15000;
|
const DEFAULT_GRACE_MS = 15000;
|
||||||
|
|
||||||
const CLEAN_SHUTDOWN = /\[(?:Render thread|Client thread)\/INFO\](?:\s+\([^)]*\))?:\s*Stopping!\s*$/;
|
const CLEAN_SHUTDOWN = /\[(?:Render thread|Client thread)\/INFO\](?:\s+\([^)]*\))?:\s*Stopping!\s*$/;
|
||||||
const FATAL_OUTPUT = [
|
const FATAL_OUTPUT = [
|
||||||
/---- Minecraft Crash Report ----/i,
|
/---- Minecraft Crash Report ----/i,
|
||||||
/\b(?:Encountered an unexpected exception|Unreported exception thrown|Reported exception thrown)\b/i,
|
/\b(?:Encountered an unexpected exception|Unreported exception thrown|Reported exception thrown)\b/i,
|
||||||
/\b(?:This crash report has been saved to|Saving crash report to)\b/i,
|
/\b(?:This crash report has been saved to|Saving crash report to)\b/i,
|
||||||
/\bA fatal error has been detected by the Java Runtime Environment\b/i,
|
/\bA fatal error has been detected by the Java Runtime Environment\b/i,
|
||||||
/Exception in thread "(?:Render thread|Client thread|main)"/i,
|
/Exception in thread "(?:Render thread|Client thread|main)"/i,
|
||||||
/\[(?:Render thread|Client thread|main)\/FATAL\]/i,
|
/\[(?:Render thread|Client thread|main)\/FATAL\]/i,
|
||||||
];
|
];
|
||||||
|
|
||||||
function createProcessWatchdog(options) {
|
function createProcessWatchdog(options) {
|
||||||
const graceMs = options.graceMs || DEFAULT_GRACE_MS;
|
const graceMs = options.graceMs || DEFAULT_GRACE_MS;
|
||||||
const schedule = options.setTimer || setTimeout;
|
const schedule = options.setTimer || setTimeout;
|
||||||
const cancel = options.clearTimer || clearTimeout;
|
const cancel = options.clearTimer || clearTimeout;
|
||||||
const buffers = { stdout: '', stderr: '' };
|
const buffers = { stdout: '', stderr: '' };
|
||||||
let timer = null;
|
let timer = null;
|
||||||
let armed = false;
|
let armed = false;
|
||||||
let crashed = false;
|
let crashed = false;
|
||||||
let ended = false;
|
let ended = false;
|
||||||
let forced = false;
|
let forced = false;
|
||||||
// bleibt true, sobald einmal ein regulärer Shutdown ("Stopping!") gesehen
|
// bleibt true, sobald einmal ein regulärer Shutdown ("Stopping!") gesehen
|
||||||
// wurde – im Gegensatz zu `armed` wird das NICHT durch markCrash()
|
// wurde – im Gegensatz zu `armed` wird das NICHT durch markCrash()
|
||||||
// zurückgesetzt, damit ein später vom internen Watchdog erzwungener Halt
|
// zurückgesetzt, damit ein später vom internen Watchdog erzwungener Halt
|
||||||
// weiterhin als "war ein regulärer Shutdown" erkennbar bleibt
|
// weiterhin als "war ein regulärer Shutdown" erkennbar bleibt
|
||||||
let sawCleanShutdown = false;
|
let sawCleanShutdown = false;
|
||||||
|
|
||||||
function clear() {
|
function clear() {
|
||||||
if (timer !== null) cancel(timer);
|
if (timer !== null) cancel(timer);
|
||||||
timer = null;
|
timer = null;
|
||||||
armed = false;
|
armed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function markCrash() {
|
function markCrash() {
|
||||||
crashed = true;
|
crashed = true;
|
||||||
clear();
|
clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleKill() {
|
function scheduleKill() {
|
||||||
if (timer !== null) cancel(timer);
|
if (timer !== null) cancel(timer);
|
||||||
timer = schedule(() => {
|
timer = schedule(() => {
|
||||||
timer = null;
|
timer = null;
|
||||||
if (ended || crashed || !armed || !options.isRunning()) return;
|
if (ended || crashed || !armed || !options.isRunning()) return;
|
||||||
try {
|
try {
|
||||||
forced = options.kill() === true;
|
forced = options.kill() === true;
|
||||||
} catch {
|
} catch {
|
||||||
forced = false;
|
forced = false;
|
||||||
}
|
}
|
||||||
if (forced && options.onForce) options.onForce(graceMs);
|
if (forced && options.onForce) options.onForce(graceMs);
|
||||||
else if (!forced && options.isRunning() && options.onError) options.onError();
|
else if (!forced && options.isRunning() && options.onError) options.onError();
|
||||||
}, graceMs);
|
}, graceMs);
|
||||||
if (timer && typeof timer.unref === 'function') timer.unref();
|
if (timer && typeof timer.unref === 'function') timer.unref();
|
||||||
}
|
}
|
||||||
|
|
||||||
function inspectLine(line) {
|
function inspectLine(line) {
|
||||||
const isCleanShutdown = CLEAN_SHUTDOWN.test(line);
|
const isCleanShutdown = CLEAN_SHUTDOWN.test(line);
|
||||||
if (isCleanShutdown) sawCleanShutdown = true;
|
if (isCleanShutdown) sawCleanShutdown = true;
|
||||||
if (FATAL_OUTPUT.some((pattern) => pattern.test(line))) {
|
if (FATAL_OUTPUT.some((pattern) => pattern.test(line))) {
|
||||||
markCrash();
|
markCrash();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ended || crashed || armed || !isCleanShutdown) return;
|
if (ended || crashed || armed || !isCleanShutdown) return;
|
||||||
|
|
||||||
armed = true;
|
armed = true;
|
||||||
scheduleKill();
|
scheduleKill();
|
||||||
}
|
}
|
||||||
|
|
||||||
function feed(chunk, stream) {
|
function feed(chunk, stream) {
|
||||||
if (ended) return;
|
if (ended) return;
|
||||||
const key = stream === 'stderr' ? 'stderr' : 'stdout';
|
const key = stream === 'stderr' ? 'stderr' : 'stdout';
|
||||||
const text = buffers[key] + String(chunk || '');
|
const text = buffers[key] + String(chunk || '');
|
||||||
const lines = text.split(/\r?\n/);
|
const lines = text.split(/\r?\n/);
|
||||||
buffers[key] = lines.pop() || '';
|
buffers[key] = lines.pop() || '';
|
||||||
for (const line of lines) inspectLine(line);
|
for (const line of lines) inspectLine(line);
|
||||||
if (armed && !crashed && !ended && lines.length) scheduleKill();
|
if (armed && !crashed && !ended && lines.length) scheduleKill();
|
||||||
}
|
}
|
||||||
|
|
||||||
function end() {
|
function end() {
|
||||||
ended = true;
|
ended = true;
|
||||||
clear();
|
clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
feed,
|
feed,
|
||||||
end,
|
end,
|
||||||
get armed() { return armed; },
|
get armed() { return armed; },
|
||||||
get crashed() { return crashed; },
|
get crashed() { return crashed; },
|
||||||
get forced() { return forced; },
|
get forced() { return forced; },
|
||||||
get sawCleanShutdown() { return sawCleanShutdown; },
|
get sawCleanShutdown() { return sawCleanShutdown; },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
DEFAULT_GRACE_MS,
|
DEFAULT_GRACE_MS,
|
||||||
CLEAN_SHUTDOWN,
|
CLEAN_SHUTDOWN,
|
||||||
FATAL_OUTPUT,
|
FATAL_OUTPUT,
|
||||||
createProcessWatchdog,
|
createProcessWatchdog,
|
||||||
};
|
};
|
||||||
|
|||||||
+1410
-80
File diff suppressed because it is too large
Load Diff
+17
-2
@@ -236,6 +236,7 @@ const INHALTSARTEN = {
|
|||||||
mod: { projectType: 'mod', ordner: 'mods', label: 'Mods', loaderNoetig: true },
|
mod: { projectType: 'mod', ordner: 'mods', label: 'Mods', loaderNoetig: true },
|
||||||
resourcepack: { projectType: 'resourcepack', ordner: 'resourcepacks', label: 'Ressourcenpakete', loaderNoetig: false },
|
resourcepack: { projectType: 'resourcepack', ordner: 'resourcepacks', label: 'Ressourcenpakete', loaderNoetig: false },
|
||||||
shader: { projectType: 'shader', ordner: 'shaderpacks', label: 'Shader', loaderNoetig: false },
|
shader: { projectType: 'shader', ordner: 'shaderpacks', label: 'Shader', loaderNoetig: false },
|
||||||
|
datapack: { projectType: 'datapack', ordner: 'datapacks', label: 'Datenpakete', loaderNoetig: false },
|
||||||
modpack: { projectType: 'modpack', ordner: null, label: 'Modpacks', loaderNoetig: false },
|
modpack: { projectType: 'modpack', ordner: null, label: 'Modpacks', loaderNoetig: false },
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -515,7 +516,9 @@ function forgeCdnUrl(fileId, fileName) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CF_LOADER = { forge: 1, fabric: 4, quilt: 5, neoforge: 6 };
|
const CF_LOADER = { forge: 1, fabric: 4, quilt: 5, neoforge: 6 };
|
||||||
const CF_CLASS = { mod: 6, resourcepack: 12, shader: 6552 };
|
// classId je Inhaltsart bei gameId 432 (Minecraft). modpack=4471 ist die auf
|
||||||
|
// CurseForge selbst verwendete Kategorie-ID (siehe curseforge.com/minecraft/modpacks).
|
||||||
|
const CF_CLASS = { mod: 6, resourcepack: 12, shader: 6552, modpack: 4471 };
|
||||||
|
|
||||||
function cfLoaderType(loader) {
|
function cfLoaderType(loader) {
|
||||||
return CF_LOADER[String(loader || '').toLowerCase()] || 0;
|
return CF_LOADER[String(loader || '').toLowerCase()] || 0;
|
||||||
@@ -598,7 +601,12 @@ async function curseforgeSearch(zugang, {
|
|||||||
+ 'Die Weiterleitung reicht für Downloads, nicht für den Katalog.',
|
+ 'Die Weiterleitung reicht für Downloads, nicht für den Katalog.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const classId = CF_CLASS[art] || CF_CLASS.mod;
|
if (!CF_CLASS[art]) {
|
||||||
|
// Kein zugeordneter CurseForge-Klassencode für diese Inhaltsart -> lieber gar
|
||||||
|
// keine Treffer als falsch einsortierte (z. B. Mods unter "Datenpakete").
|
||||||
|
return { total: 0, hits: [], error: 'CurseForge unterstützt diese Inhaltsart hier nicht – bitte Modrinth nutzen.' };
|
||||||
|
}
|
||||||
|
const classId = CF_CLASS[art];
|
||||||
const url = new URL(CURSEFORGE + '/mods/search');
|
const url = new URL(CURSEFORGE + '/mods/search');
|
||||||
url.searchParams.set('gameId', '432');
|
url.searchParams.set('gameId', '432');
|
||||||
url.searchParams.set('classId', String(classId));
|
url.searchParams.set('classId', String(classId));
|
||||||
@@ -713,6 +721,12 @@ async function curseforgePickFile(zugang, modId, gameVersion, loader, art) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Beste Modpack-Datei eines CurseForge-Projekts auflösen (für den Katalog-Import,
|
||||||
|
// analog zu modrinthResolveModpack).
|
||||||
|
async function curseforgeResolveModpackFile(zugang, modId, gameVersion) {
|
||||||
|
return curseforgePickFile(zugang, modId, gameVersion, '', 'modpack');
|
||||||
|
}
|
||||||
|
|
||||||
async function curseforgeInstall({ zugang, projectId, title, gameVersion, loader, modsDir, art = 'mod' }) {
|
async function curseforgeInstall({ zugang, projectId, title, gameVersion, loader, modsDir, art = 'mod' }) {
|
||||||
if (!zugang) return { ok: false, message: 'Kein CurseForge-Zugang eingerichtet.' };
|
if (!zugang) return { ok: false, message: 'Kein CurseForge-Zugang eingerichtet.' };
|
||||||
const rawId = String(projectId || '').replace(/^cf-/, '');
|
const rawId = String(projectId || '').replace(/^cf-/, '');
|
||||||
@@ -814,4 +828,5 @@ module.exports = {
|
|||||||
modrinthSearch, modrinthResolveFile, modrinthResolveModpack, modrinthInstall, modrinthProjectTitle,
|
modrinthSearch, modrinthResolveFile, modrinthResolveModpack, modrinthInstall, modrinthProjectTitle,
|
||||||
modrinthFindUpdates, downloadFile, downloadBuffer, inhaltsArt, INHALTSARTEN,
|
modrinthFindUpdates, downloadFile, downloadBuffer, inhaltsArt, INHALTSARTEN,
|
||||||
curseforgeResolveFiles, curseforgeSearch, curseforgeInstall, curseforgeTestKey, forgeCdnUrl, curseforgeZugang,
|
curseforgeResolveFiles, curseforgeSearch, curseforgeInstall, curseforgeTestKey, forgeCdnUrl, curseforgeZugang,
|
||||||
|
curseforgeResolveModpackFile,
|
||||||
};
|
};
|
||||||
|
|||||||
+20
-2
@@ -76,6 +76,10 @@ const DEFAULT_SETTINGS = {
|
|||||||
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
|
||||||
|
friendNicknames: {}, // rein lokal: uuid -> eigener Spitzname für einen Freund
|
||||||
|
autostart: false, // mit Windows starten, im Infobereich statt als Fenster
|
||||||
|
discordClientId: '', // optional, nur per Hand in settings.json; leer => eingebaute AeroMC-App-ID
|
||||||
bgSkin: '', // '' = aus, 'active' = aktives Konto, sonst Konto-UUID
|
bgSkin: '', // '' = aus, 'active' = aktives Konto, sonst Konto-UUID
|
||||||
bgSkinOpacity: 55, // Deckkraft des Hintergrund-Skins in %
|
bgSkinOpacity: 55, // Deckkraft des Hintergrund-Skins in %
|
||||||
bgSkinPose: 'walking', // Pose des 3D-Renders (falls der Pose-Dienst erreichbar ist)
|
bgSkinPose: 'walking', // Pose des 3D-Renders (falls der Pose-Dienst erreichbar ist)
|
||||||
@@ -88,6 +92,16 @@ const DEFAULT_SETTINGS = {
|
|||||||
proxyUser: '',
|
proxyUser: '',
|
||||||
proxyPass: '',
|
proxyPass: '',
|
||||||
theme: 'dark',
|
theme: 'dark',
|
||||||
|
// Spielaufzeichnung - bleibt über Neustarts erhalten (letzte Wahl je Feld)
|
||||||
|
recordingResolution: 'native',
|
||||||
|
recordingQuality: 'medium',
|
||||||
|
recordingFramerate: 30,
|
||||||
|
recordingAudioSystem: true,
|
||||||
|
recordingAudioGame: false,
|
||||||
|
recordingVolSystem: 100,
|
||||||
|
recordingVolGame: 100,
|
||||||
|
recordingMicSelected: [], // Liste gewählter Mikrofon-deviceIds
|
||||||
|
recordingMicVolumes: {}, // deviceId -> Lautstärke (0-100)
|
||||||
};
|
};
|
||||||
|
|
||||||
function settingsFile() {
|
function settingsFile() {
|
||||||
@@ -133,8 +147,12 @@ function gameDir(id) {
|
|||||||
* "mod" behält die bisherigen Namen, damit vorhandene Instanzen unverändert
|
* "mod" behält die bisherigen Namen, damit vorhandene Instanzen unverändert
|
||||||
* weiterlaufen.
|
* weiterlaufen.
|
||||||
*/
|
*/
|
||||||
const INHALT_ORDNER = { mod: 'mods', resourcepack: 'resourcepacks', shader: 'shaderpacks' };
|
const INHALT_ORDNER = {
|
||||||
const INHALT_INDEX = { mod: 'mods.json', resourcepack: 'resourcepacks.json', shader: 'shaders.json' };
|
mod: 'mods', resourcepack: 'resourcepacks', shader: 'shaderpacks', datapack: 'datapacks',
|
||||||
|
};
|
||||||
|
const INHALT_INDEX = {
|
||||||
|
mod: 'mods.json', resourcepack: 'resourcepacks.json', shader: 'shaders.json', datapack: 'datapacks.json',
|
||||||
|
};
|
||||||
|
|
||||||
function inhaltsDir(id, art = 'mod') {
|
function inhaltsDir(id, art = 'mod') {
|
||||||
return path.join(gameDir(id), INHALT_ORDNER[art] || INHALT_ORDNER.mod);
|
return path.join(gameDir(id), INHALT_ORDNER[art] || INHALT_ORDNER.mod);
|
||||||
|
|||||||
+85
-2
@@ -32,6 +32,7 @@ body{display:grid;grid-template-rows:auto 1fr auto;height:100vh;overflow:hidden}
|
|||||||
:root[data-theme="dark"] .field select,
|
:root[data-theme="dark"] .field select,
|
||||||
:root[data-theme="dark"] .field textarea,
|
:root[data-theme="dark"] .field textarea,
|
||||||
:root[data-theme="dark"] .mods-search input,
|
:root[data-theme="dark"] .mods-search input,
|
||||||
|
:root[data-theme="dark"] .path-row input,
|
||||||
:root[data-theme="dark"] .login-usercode{background:#1e1e1e;color:var(--text);border-color:var(--border)}
|
:root[data-theme="dark"] .login-usercode{background:#1e1e1e;color:var(--text);border-color:var(--border)}
|
||||||
:root[data-theme="dark"] .modal-card{background:#2d2d30;border-color:#161618}
|
:root[data-theme="dark"] .modal-card{background:#2d2d30;border-color:#161618}
|
||||||
:root[data-theme="dark"] .modal-head{background:#333337}
|
:root[data-theme="dark"] .modal-head{background:#333337}
|
||||||
@@ -67,7 +68,8 @@ body{display:grid;grid-template-rows:auto 1fr auto;height:100vh;overflow:hidden}
|
|||||||
:root[data-theme="dark"] #btn-console svg{color:#c8c8cc}
|
:root[data-theme="dark"] #btn-console svg{color:#c8c8cc}
|
||||||
:root[data-theme="dark"] .acct-av svg{color:#c8c8cc}
|
:root[data-theme="dark"] .acct-av svg{color:#c8c8cc}
|
||||||
:root[data-theme="dark"] .mods-filters select,
|
:root[data-theme="dark"] .mods-filters select,
|
||||||
:root[data-theme="dark"] .mods-search select{background:#1e1e1e;color:var(--text);border-color:var(--border)}
|
:root[data-theme="dark"] .mods-search select,
|
||||||
|
:root[data-theme="dark"] .mmc-bar select{background:#1e1e1e;color:var(--text);border-color:var(--border)}
|
||||||
:root[data-theme="dark"] .upd-notes{background:#1e1e1e;color:var(--text)}
|
:root[data-theme="dark"] .upd-notes{background:#1e1e1e;color:var(--text)}
|
||||||
:root[data-theme="dark"] .modal-close{color:#aaa}
|
:root[data-theme="dark"] .modal-close{color:#aaa}
|
||||||
:root[data-theme="dark"] .modal-close:hover{color:#fff}
|
:root[data-theme="dark"] .modal-close:hover{color:#fff}
|
||||||
@@ -185,6 +187,42 @@ body.file-drop-active::after{
|
|||||||
.modal-body{padding:16px;overflow:auto;background:#f6f6f6}
|
.modal-body{padding:16px;overflow:auto;background:#f6f6f6}
|
||||||
.modal-foot{display:flex;justify-content:flex-end;gap:8px;padding:10px 14px;border-top:1px solid var(--border);background:#efefef}
|
.modal-foot{display:flex;justify-content:flex-end;gap:8px;padding:10px 14px;border-top:1px solid var(--border);background:#efefef}
|
||||||
|
|
||||||
|
/* Popup-Fenster (Einstellungen/Konsole): Haupt-UI ausblenden, das eine Modal
|
||||||
|
füllt das ganze (rahmenlose) Fenster; der eigene Kopfbereich ersetzt die
|
||||||
|
native Titelleiste – per -webkit-app-region per Maus verschiebbar */
|
||||||
|
body.is-popout>header.toolbar,body.is-popout>main.workspace,body.is-popout>footer.statusbar{display:none}
|
||||||
|
.popout-host{position:static;inset:auto;background:none;display:block}
|
||||||
|
/* !important: schlägt auch die spezifischere #modal-settings-Breitenregel oben */
|
||||||
|
.popout-host .modal-card{width:100%!important;max-width:none!important;height:100vh!important;max-height:none!important;border:none;border-radius:0;box-shadow:none;margin:0}
|
||||||
|
/* Inhalt soll die restliche Fensterhöhe füllen (scrollen statt Leerraum unter dem Fuß) */
|
||||||
|
.popout-host .modal-body{flex:1 1 auto;min-height:0}
|
||||||
|
/* Konsole/Logs/Screenshots haben einen einzelnen scrollbaren Kasten mit
|
||||||
|
fester Höhe (für den normalen Dialog gedacht) – im Popup-Fenster soll der
|
||||||
|
stattdessen die volle verbleibende Höhe ausfüllen statt Leerraum darunter
|
||||||
|
zu lassen */
|
||||||
|
#modal-console.popout-host .modal-body,
|
||||||
|
#modal-logs.popout-host .modal-body,
|
||||||
|
#modal-screens.popout-host .modal-body,
|
||||||
|
#modal-servers.popout-host .modal-body,
|
||||||
|
#modal-mods.popout-host .modal-body,
|
||||||
|
#modal-record.popout-host .modal-body{display:flex;flex-direction:column}
|
||||||
|
.popout-host .console-out,
|
||||||
|
#modal-logs.popout-host .crash-voll,
|
||||||
|
.popout-host .mods-list,
|
||||||
|
.popout-host .record-stage{flex:1 1 auto;height:auto;max-height:none;min-height:0}
|
||||||
|
/* Screenshots/Aufzeichnungen NICHT auf die volle Fensterhöhe zwingen (Grid-
|
||||||
|
Zeilen strecken sich sonst nicht mit, es bliebe bei wenigen Einträgen ein
|
||||||
|
großer sichtbar leerer Kasten stehen) - nur die feste 460px-Deckelung
|
||||||
|
aufheben, damit bei VIELEN Einträgen trotzdem mehr Platz genutzt wird */
|
||||||
|
.popout-host .screens-grid{max-height:none}
|
||||||
|
/* Mods-Fenster: die aktive Ansicht (Katalog/Installiert) muss selbst ein
|
||||||
|
Flex-Container werden, damit ihre eigene Liste (.mods-list) die Regel oben
|
||||||
|
nutzen und den restlichen Platz füllen kann */
|
||||||
|
#modal-mods.popout-host #m-pane-catalog,
|
||||||
|
#modal-mods.popout-host #m-pane-installed{display:flex;flex-direction:column;flex:1 1 auto;min-height:0}
|
||||||
|
.popout-host .modal-head{-webkit-app-region:drag;cursor:default}
|
||||||
|
.popout-host .modal-head button,.popout-host .modal-head input,.popout-host .modal-head select{-webkit-app-region:no-drag}
|
||||||
|
|
||||||
/* Formular */
|
/* Formular */
|
||||||
.field{display:flex;flex-direction:column;gap:5px;margin-bottom:12px}
|
.field{display:flex;flex-direction:column;gap:5px;margin-bottom:12px}
|
||||||
.field>span{font-size:12px;color:#333}
|
.field>span{font-size:12px;color:#333}
|
||||||
@@ -196,7 +234,8 @@ body.file-drop-active::after{
|
|||||||
.backup-select-field{margin-top:14px;margin-bottom:0}
|
.backup-select-field{margin-top:14px;margin-bottom:0}
|
||||||
.backup-select-field select{width:100%}
|
.backup-select-field select{width:100%}
|
||||||
.hint{font-size:12px}
|
.hint{font-size:12px}
|
||||||
.path-row{display:flex;gap:8px}.path-row input{flex:1}
|
.path-row{display:flex;gap:8px}
|
||||||
|
.path-row input{flex:1;padding:6px 8px;border:1px solid var(--border);border-radius:3px;background:#fff;color:var(--text);font-family:inherit;font-size:13px}
|
||||||
.advanced{border:1px solid var(--border);border-radius:3px;padding:12px 14px 2px;margin-top:4px;background:#fbfbfb}
|
.advanced{border:1px solid var(--border);border-radius:3px;padding:12px 14px 2px;margin-top:4px;background:#fbfbfb}
|
||||||
.advanced legend{color:var(--dim);font-size:12px;padding:0 6px}
|
.advanced legend{color:var(--dim);font-size:12px;padding:0 6px}
|
||||||
.version-row{display:flex;gap:8px}.version-row #f-version-type{max-width:120px}.version-row #f-version{flex:1}
|
.version-row{display:flex;gap:8px}.version-row #f-version-type{max-width:120px}.version-row #f-version{flex:1}
|
||||||
@@ -286,6 +325,8 @@ body.file-drop-active::after{
|
|||||||
/* Die gerenderte Figur soll das Feld ausfüllen; eine rohe 64x64-Textur wird
|
/* Die gerenderte Figur soll das Feld ausfüllen; eine rohe 64x64-Textur wird
|
||||||
ohne Weichzeichnen vergrößert, damit sie nicht verschwommen wirkt. */
|
ohne Weichzeichnen vergrößert, damit sie nicht verschwommen wirkt. */
|
||||||
.skin-bild img{max-width:100%;max-height:100%;image-rendering:pixelated}
|
.skin-bild img{max-width:100%;max-height:100%;image-rendering:pixelated}
|
||||||
|
.skin-bild canvas{max-width:100%;max-height:100%;cursor:grab}
|
||||||
|
.skin-bild canvas:active{cursor:grabbing}
|
||||||
.crash-details{margin-top:12px}
|
.crash-details{margin-top:12px}
|
||||||
.crash-details summary{cursor:pointer;font-size:12px;color:var(--dim)}
|
.crash-details summary{cursor:pointer;font-size:12px;color:var(--dim)}
|
||||||
.crash-voll{max-height:320px;overflow:auto;margin-top:8px;padding:10px;background:rgba(0,0,0,.25);border-radius:4px;font-family:var(--mono);font-size:11px;line-height:1.45;white-space:pre-wrap;word-break:break-word}
|
.crash-voll{max-height:320px;overflow:auto;margin-top:8px;padding:10px;background:rgba(0,0,0,.25);border-radius:4px;font-family:var(--mono);font-size:11px;line-height:1.45;white-space:pre-wrap;word-break:break-word}
|
||||||
@@ -299,6 +340,8 @@ body.file-drop-active::after{
|
|||||||
|
|
||||||
/* MultiMC-Import */
|
/* MultiMC-Import */
|
||||||
.mmc-bar{display:flex;gap:18px;align-items:center;flex-wrap:wrap;margin-bottom:10px}
|
.mmc-bar{display:flex;gap:18px;align-items:center;flex-wrap:wrap;margin-bottom:10px}
|
||||||
|
.mmc-bar select{padding:6px 8px;border:1px solid var(--border);border-radius:3px;background:var(--input,#fff);color:var(--text);font-family:inherit;font-size:12px}
|
||||||
|
.mmc-bar select:focus{outline:none;border-color:var(--accent)}
|
||||||
.mmc-item{display:flex;gap:10px;align-items:center;padding:7px 8px;border-radius:3px}
|
.mmc-item{display:flex;gap:10px;align-items:center;padding:7px 8px;border-radius:3px}
|
||||||
.mmc-item+.mmc-item{border-top:1px solid var(--border2)}
|
.mmc-item+.mmc-item{border-top:1px solid var(--border2)}
|
||||||
.mmc-item:hover{background:rgba(128,128,128,.10)}
|
.mmc-item:hover{background:rgba(128,128,128,.10)}
|
||||||
@@ -339,6 +382,40 @@ img.account-av{object-fit:cover;image-rendering:auto}
|
|||||||
.screens-toolbar{display:flex;align-items:center;gap:8px;margin-bottom:10px;flex-wrap:wrap}
|
.screens-toolbar{display:flex;align-items:center;gap:8px;margin-bottom:10px;flex-wrap:wrap}
|
||||||
.screens-toolbar .hint{font-size:11px}
|
.screens-toolbar .hint{font-size:11px}
|
||||||
.screens-empty{color:var(--dim);text-align:center;padding:30px;grid-column:1/-1}
|
.screens-empty{color:var(--dim);text-align:center;padding:30px;grid-column:1/-1}
|
||||||
|
.record-thumb-img-wrap{position:relative}
|
||||||
|
.record-thumb-img-wrap img{background:#000}
|
||||||
|
.record-thumb-play{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#fff;font-size:22px;text-shadow:0 1px 4px rgba(0,0,0,.7);pointer-events:none}
|
||||||
|
.record-thumb-size{font-size:10px;color:var(--dim);padding:0 6px 4px}
|
||||||
|
|
||||||
|
/* Freunde */
|
||||||
|
.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 input{flex:1}
|
||||||
|
.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:hover{background:var(--sel,rgba(0,0,0,.04))}
|
||||||
|
.friend-dot{width:9px;height:9px;border-radius:50%;background:#8a8a8a;flex:0 0 auto}
|
||||||
|
.friend-dot.online{background:#3bb54a}
|
||||||
|
.friend-avatar{width:28px;height:28px;border-radius:4px;flex:0 0 auto;background:#1e1e1e;object-fit:contain}
|
||||||
|
.friend-info{flex:1;min-width:0}
|
||||||
|
.friend-name{font-size:13px}
|
||||||
|
.friend-status{font-size:11px;color:var(--dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
.friends-empty{color:var(--dim);font-size:12px;padding:8px 4px}
|
||||||
|
|
||||||
|
/* Spielaufzeichnung */
|
||||||
|
.record-search{display:flex;flex-direction:column;align-items:center;gap:10px;padding:48px 0;color:var(--dim)}
|
||||||
|
.record-stage{display:flex;flex-direction:column;gap:10px}
|
||||||
|
.record-video{width:100%;flex:1 1 auto;min-height:360px;background:#000;border-radius:4px;object-fit:contain}
|
||||||
|
.record-toolbar{display:flex;flex-direction:column;align-items:flex-end;gap:6px}
|
||||||
|
.record-live-badge{display:flex;align-items:center;gap:8px;color:var(--danger);font-size:13px;font-weight:600}
|
||||||
|
#record-timer{font-variant-numeric:tabular-nums}
|
||||||
|
.rec-dot{width:10px;height:10px;border-radius:50%;background:var(--danger);animation:rec-pulse 1s infinite}
|
||||||
|
@keyframes rec-pulse{0%,100%{opacity:1}50%{opacity:.25}}
|
||||||
|
.record-audio-bar{display:flex;align-items:center;gap:10px;padding:2px 2px}
|
||||||
|
.record-mic-list{display:flex;flex-direction:column;gap:8px;margin-top:8px}
|
||||||
|
.record-source-row{display:flex;align-items:center;gap:10px;margin-bottom:8px}
|
||||||
|
.record-source-row .checkbox-field{margin-bottom:0;flex:1;min-width:0;font-size:13px;gap:6px}
|
||||||
|
.record-vol{width:110px}
|
||||||
|
|
||||||
/* Konsole (dunkel wie ein Log) */
|
/* Konsole (dunkel wie ein Log) */
|
||||||
.console-out{height:440px;overflow:auto;margin:0;background:#1e1e1e;border:1px solid var(--border);border-radius:3px;padding:10px;font-family:var(--mono);font-size:12px;color:#d4d4d4;white-space:pre-wrap;word-break:break-word;line-height:1.5}
|
.console-out{height:440px;overflow:auto;margin:0;background:#1e1e1e;border:1px solid var(--border);border-radius:3px;padding:10px;font-family:var(--mono);font-size:12px;color:#d4d4d4;white-space:pre-wrap;word-break:break-word;line-height:1.5}
|
||||||
@@ -413,6 +490,12 @@ img.account-av{object-fit:cover;image-rendering:auto}
|
|||||||
.path-row{flex-direction:column}
|
.path-row{flex-direction:column}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Popup-Fenster sind eigenständige, meist schmalere Fenster -- die
|
||||||
|
Zeilenumbruch-Regel oben ist für die (deutlich breitere) Haupt-App gedacht
|
||||||
|
und soll hier nicht greifen (sonst z. B. Version/Icon im Instanz-Fenster
|
||||||
|
untereinander statt nebeneinander) */
|
||||||
|
.is-popout .field-row{flex-direction:row}
|
||||||
|
|
||||||
/* Konto-Button mit 3D-Skin-Kopf */
|
/* Konto-Button mit 3D-Skin-Kopf */
|
||||||
.acct-btn{gap:8px}
|
.acct-btn{gap:8px}
|
||||||
.acct-av{display:inline-flex;align-items:center;justify-content:center}
|
.acct-av{display:inline-flex;align-items:center;justify-content:center}
|
||||||
|
|||||||
+11
-2
@@ -230,8 +230,17 @@ async function downloadAndInstall(asset, onProgress) {
|
|||||||
|
|
||||||
const dir = updateDir();
|
const dir = updateDir();
|
||||||
fs.mkdirSync(dir, { recursive: true });
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
const dest = path.join(dir, asset.name.replace(/[^\w.\-]/g, '_'));
|
let dest = path.join(dir, asset.name.replace(/[^\w.\-]/g, '_'));
|
||||||
fs.rmSync(dest, { force: true });
|
try {
|
||||||
|
fs.rmSync(dest, { force: true });
|
||||||
|
} catch {
|
||||||
|
// Alte Datei ist gesperrt (Virenscanner prüft sie gerade, oder ein vorheriger
|
||||||
|
// Installer-Versuch läuft noch) - dann eben unter neuem Namen laden, statt mit
|
||||||
|
// EPERM abzubrechen. Der Rest wird bei nächster Gelegenheit von
|
||||||
|
// cleanupUpdateCache() aufgeräumt (ab 10 Minuten Alter).
|
||||||
|
const parsed = path.parse(dest);
|
||||||
|
dest = path.join(parsed.dir, `${parsed.name}-${Date.now()}${parsed.ext}`);
|
||||||
|
}
|
||||||
|
|
||||||
const res = await fetch(asset.url, { headers: { 'User-Agent': UA } });
|
const res = await fetch(asset.url, { headers: { 'User-Agent': UA } });
|
||||||
if (!res.ok) throw new Error('Download fehlgeschlagen: HTTP ' + res.status);
|
if (!res.ok) throw new Error('Download fehlgeschlagen: HTTP ' + res.status);
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
* Version: 1.0.0
|
||||||
|
* Author: M_Viper
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit; // kein direkter Aufruf der Datei
|
||||||
|
}
|
||||||
|
|
||||||
|
define('AEROMC_FRIENDS_VERSION', '1.0.0');
|
||||||
|
define('AEROMC_FRIENDS_PRESENCE_TABLE', 'aeromc_presence');
|
||||||
|
define('AEROMC_FRIENDS_FRIENDS_TABLE', 'aeromc_friends');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legt die beiden Tabellen an (bzw. bringt sie bei einem Update auf den
|
||||||
|
* neuesten Stand - dbDelta erkennt bestehende Tabellen/Spalten selbst).
|
||||||
|
*/
|
||||||
|
function aeromc_friends_install() {
|
||||||
|
global $wpdb;
|
||||||
|
$charset_collate = $wpdb->get_charset_collate();
|
||||||
|
$presence_table = $wpdb->prefix . AEROMC_FRIENDS_PRESENCE_TABLE;
|
||||||
|
$friends_table = $wpdb->prefix . AEROMC_FRIENDS_FRIENDS_TABLE;
|
||||||
|
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||||
|
|
||||||
|
// uuid ohne Bindestriche gespeichert (siehe aeromc_friends_normalize_uuid
|
||||||
|
// in includes/routes.php) - vermeidet doppelte Zeilen durch
|
||||||
|
// unterschiedliche Schreibweisen derselben UUID
|
||||||
|
$sql_presence = "CREATE TABLE $presence_table (
|
||||||
|
uuid varchar(32) NOT NULL,
|
||||||
|
username varchar(32) NOT NULL,
|
||||||
|
instance_name varchar(128) DEFAULT NULL,
|
||||||
|
server_label varchar(128) DEFAULT NULL,
|
||||||
|
last_seen datetime NOT NULL,
|
||||||
|
PRIMARY KEY (uuid)
|
||||||
|
) $charset_collate;";
|
||||||
|
dbDelta($sql_presence);
|
||||||
|
|
||||||
|
$sql_friends = "CREATE TABLE $friends_table (
|
||||||
|
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
uuid_a varchar(32) NOT NULL,
|
||||||
|
uuid_b varchar(32) NOT NULL,
|
||||||
|
status varchar(20) NOT NULL DEFAULT 'pending',
|
||||||
|
created_at datetime NOT NULL,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY uuid_a (uuid_a),
|
||||||
|
KEY uuid_b (uuid_b)
|
||||||
|
) $charset_collate;";
|
||||||
|
dbDelta($sql_friends);
|
||||||
|
}
|
||||||
|
register_activation_hook(__FILE__, 'aeromc_friends_install');
|
||||||
|
|
||||||
|
require_once plugin_dir_path(__FILE__) . 'includes/routes.php';
|
||||||
@@ -0,0 +1,303 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* REST-Routen unter /wp-json/aeromc/v1/... für das Freundesystem.
|
||||||
|
*
|
||||||
|
* Bewusst OHNE echte Authentifizierung (Absprache mit dem Nutzer): der
|
||||||
|
* Launcher meldet sich einfach mit der Minecraft-UUID des aktiven Kontos an.
|
||||||
|
* Wer eine fremde UUID kennt, könnte sich theoretisch als dieser Spieler
|
||||||
|
* ausgeben - für den privaten Rahmen dieses Projekts akzeptiert. Eingaben
|
||||||
|
* werden trotzdem serverseitig geprüft (Format von UUID/Name) und jede
|
||||||
|
* Datenbankabfrage läuft ausschließlich über $wpdb->prepare, um SQL-Injection
|
||||||
|
* auszuschließen.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Konten gelten als "online", solange ihr letzter Herzschlag (der Launcher
|
||||||
|
// sendet alle 60s einen) nicht älter als das hier ist.
|
||||||
|
define('AEROMC_FRIENDS_ONLINE_SECONDS', 180);
|
||||||
|
// Häufigere Herzschläge als das werden ignoriert (Schutz gegen Missbrauch/
|
||||||
|
// versehentliche Anfrageflut) - der Launcher selbst sendet ohnehin nur alle 60s.
|
||||||
|
define('AEROMC_FRIENDS_HEARTBEAT_MIN_GAP', 20);
|
||||||
|
|
||||||
|
function aeromc_friends_presence_table() {
|
||||||
|
global $wpdb;
|
||||||
|
return $wpdb->prefix . AEROMC_FRIENDS_PRESENCE_TABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
function aeromc_friends_friends_table() {
|
||||||
|
global $wpdb;
|
||||||
|
return $wpdb->prefix . AEROMC_FRIENDS_FRIENDS_TABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Minecraft-UUIDs kommen mal mit, mal ohne Bindestriche an - einheitlich
|
||||||
|
* ohne Bindestriche, klein geschrieben speichern/vergleichen. Gibt false
|
||||||
|
* zurück, wenn es keine gültige UUID (32 Hex-Zeichen) ist. */
|
||||||
|
function aeromc_friends_normalize_uuid($uuid) {
|
||||||
|
$stripped = strtolower(str_replace('-', '', (string) $uuid));
|
||||||
|
if (!preg_match('/^[a-f0-9]{32}$/', $stripped)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return $stripped;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Minecraft-Namensregeln: 1-16 Zeichen, Buchstaben/Ziffern/Unterstrich. */
|
||||||
|
function aeromc_friends_valid_username($name) {
|
||||||
|
return is_string($name) && preg_match('/^[A-Za-z0-9_]{1,16}$/', $name);
|
||||||
|
}
|
||||||
|
|
||||||
|
add_action('rest_api_init', function () {
|
||||||
|
register_rest_route('aeromc/v1', '/presence', [
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => 'aeromc_friends_route_presence',
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
]);
|
||||||
|
register_rest_route('aeromc/v1', '/friends', [
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => 'aeromc_friends_route_list',
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
]);
|
||||||
|
register_rest_route('aeromc/v1', '/friends/requests', [
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => 'aeromc_friends_route_requests',
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
]);
|
||||||
|
register_rest_route('aeromc/v1', '/friends/sent', [
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => 'aeromc_friends_route_sent',
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
]);
|
||||||
|
register_rest_route('aeromc/v1', '/friends/request', [
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => 'aeromc_friends_route_request',
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
]);
|
||||||
|
register_rest_route('aeromc/v1', '/friends/respond', [
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => 'aeromc_friends_route_respond',
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
]);
|
||||||
|
register_rest_route('aeromc/v1', '/friends/cancel', [
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => 'aeromc_friends_route_cancel',
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
]);
|
||||||
|
register_rest_route('aeromc/v1', '/friends/remove', [
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => 'aeromc_friends_route_remove',
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** POST /presence {uuid, username, instance, server} */
|
||||||
|
function aeromc_friends_route_presence($request) {
|
||||||
|
global $wpdb;
|
||||||
|
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
||||||
|
$username = $request->get_param('username');
|
||||||
|
if (!$uuid || !aeromc_friends_valid_username($username)) {
|
||||||
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid/username.', ['status' => 400]);
|
||||||
|
}
|
||||||
|
$instance = sanitize_text_field((string) $request->get_param('instance'));
|
||||||
|
$server = sanitize_text_field((string) $request->get_param('server'));
|
||||||
|
$table = aeromc_friends_presence_table();
|
||||||
|
|
||||||
|
$letzte = $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT last_seen FROM $table WHERE uuid = %s", $uuid
|
||||||
|
));
|
||||||
|
if ($letzte && (time() - strtotime($letzte)) < AEROMC_FRIENDS_HEARTBEAT_MIN_GAP) {
|
||||||
|
return ['ok' => true]; // zu häufig - stillschweigend ignoriert
|
||||||
|
}
|
||||||
|
|
||||||
|
// current_time('mysql', true) statt current_time('mysql'): das zweite
|
||||||
|
// Argument erzwingt GMT/UTC. Ohne das liefert current_time() die an die
|
||||||
|
// WordPress-Zeitzone angepasste ORTSZEIT (Einstellungen > Allgemein) -
|
||||||
|
// das online/offline-Fenster unten vergleicht last_seen aber per
|
||||||
|
// strtotime()/time() als UTC, wodurch ein Freund bei abweichender
|
||||||
|
// Zeitzone praktisch nie als offline erkannt worden wäre.
|
||||||
|
$jetzt_utc = current_time('mysql', true);
|
||||||
|
$wpdb->query($wpdb->prepare(
|
||||||
|
"INSERT INTO $table (uuid, username, instance_name, server_label, last_seen)
|
||||||
|
VALUES (%s, %s, %s, %s, %s)
|
||||||
|
ON DUPLICATE KEY UPDATE username = %s, instance_name = %s, server_label = %s, last_seen = %s",
|
||||||
|
$uuid, $username, $instance ?: null, $server ?: null, $jetzt_utc,
|
||||||
|
$username, $instance ?: null, $server ?: null, $jetzt_utc
|
||||||
|
));
|
||||||
|
return ['ok' => true];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GET /friends?uuid= - akzeptierte Freunde inkl. Online-Status */
|
||||||
|
function aeromc_friends_route_list($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]);
|
||||||
|
}
|
||||||
|
$friends_table = aeromc_friends_friends_table();
|
||||||
|
$presence_table = aeromc_friends_presence_table();
|
||||||
|
|
||||||
|
$rows = $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT IF(f.uuid_a = %s, f.uuid_b, f.uuid_a) AS other_uuid
|
||||||
|
FROM $friends_table f
|
||||||
|
WHERE (f.uuid_a = %s OR f.uuid_b = %s) AND f.status = 'accepted'",
|
||||||
|
$uuid, $uuid, $uuid
|
||||||
|
));
|
||||||
|
|
||||||
|
$ergebnis = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$p = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT username, instance_name, server_label, last_seen FROM $presence_table WHERE uuid = %s",
|
||||||
|
$row->other_uuid
|
||||||
|
));
|
||||||
|
$online = $p && (time() - strtotime($p->last_seen)) < AEROMC_FRIENDS_ONLINE_SECONDS;
|
||||||
|
$ergebnis[] = [
|
||||||
|
'uuid' => $row->other_uuid,
|
||||||
|
'username' => $p ? $p->username : null,
|
||||||
|
'online' => (bool) $online,
|
||||||
|
'instance' => $online ? $p->instance_name : null,
|
||||||
|
'server' => $online ? $p->server_label : null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $ergebnis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GET /friends/requests?uuid= - offene, an mich gerichtete Anfragen */
|
||||||
|
function aeromc_friends_route_requests($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]);
|
||||||
|
}
|
||||||
|
$friends_table = aeromc_friends_friends_table();
|
||||||
|
$presence_table = aeromc_friends_presence_table();
|
||||||
|
|
||||||
|
$rows = $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT f.uuid_a AS from_uuid, p.username AS from_username
|
||||||
|
FROM $friends_table f
|
||||||
|
LEFT JOIN $presence_table p ON p.uuid = f.uuid_a
|
||||||
|
WHERE f.uuid_b = %s AND f.status = 'pending'",
|
||||||
|
$uuid
|
||||||
|
));
|
||||||
|
return array_map(function ($r) {
|
||||||
|
return ['uuid' => $r->from_uuid, 'username' => $r->from_username];
|
||||||
|
}, $rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GET /friends/sent?uuid= - offene, von mir SELBST gesendete Anfragen */
|
||||||
|
function aeromc_friends_route_sent($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]);
|
||||||
|
}
|
||||||
|
$friends_table = aeromc_friends_friends_table();
|
||||||
|
$presence_table = aeromc_friends_presence_table();
|
||||||
|
|
||||||
|
$rows = $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT f.uuid_b AS to_uuid, p.username AS to_username
|
||||||
|
FROM $friends_table f
|
||||||
|
LEFT JOIN $presence_table p ON p.uuid = f.uuid_b
|
||||||
|
WHERE f.uuid_a = %s AND f.status = 'pending'",
|
||||||
|
$uuid
|
||||||
|
));
|
||||||
|
return array_map(function ($r) {
|
||||||
|
return ['uuid' => $r->to_uuid, 'username' => $r->to_username];
|
||||||
|
}, $rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST /friends/request {uuid, username, targetUsername} */
|
||||||
|
function aeromc_friends_route_request($request) {
|
||||||
|
global $wpdb;
|
||||||
|
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
||||||
|
$targetUsername = $request->get_param('targetUsername');
|
||||||
|
if (!$uuid || !aeromc_friends_valid_username($targetUsername)) {
|
||||||
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid/targetUsername.', ['status' => 400]);
|
||||||
|
}
|
||||||
|
$presence_table = aeromc_friends_presence_table();
|
||||||
|
$friends_table = aeromc_friends_friends_table();
|
||||||
|
|
||||||
|
$ziel = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT uuid FROM $presence_table WHERE username = %s", $targetUsername
|
||||||
|
));
|
||||||
|
if (!$ziel) {
|
||||||
|
return new WP_Error('aeromc_not_found', 'Dieser Name wurde noch nie im Launcher gesehen.', ['status' => 404]);
|
||||||
|
}
|
||||||
|
$ziel_uuid = $ziel->uuid;
|
||||||
|
if ($ziel_uuid === $uuid) {
|
||||||
|
return new WP_Error('aeromc_self', 'Du kannst dich nicht selbst hinzufügen.', ['status' => 400]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$vorhanden = $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)",
|
||||||
|
$uuid, $ziel_uuid, $ziel_uuid, $uuid
|
||||||
|
));
|
||||||
|
if ($vorhanden) {
|
||||||
|
return new WP_Error('aeromc_exists', 'Es gibt schon eine Anfrage oder Freundschaft.', ['status' => 409]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$wpdb->insert($friends_table, [
|
||||||
|
'uuid_a' => $uuid,
|
||||||
|
'uuid_b' => $ziel_uuid,
|
||||||
|
'status' => 'pending',
|
||||||
|
'created_at' => current_time('mysql'),
|
||||||
|
]);
|
||||||
|
return ['ok' => true];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST /friends/respond {uuid, otherUuid, accept} */
|
||||||
|
function aeromc_friends_route_respond($request) {
|
||||||
|
global $wpdb;
|
||||||
|
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
||||||
|
$other = aeromc_friends_normalize_uuid($request->get_param('otherUuid'));
|
||||||
|
$accept = (bool) $request->get_param('accept');
|
||||||
|
if (!$uuid || !$other) {
|
||||||
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid/otherUuid.', ['status' => 400]);
|
||||||
|
}
|
||||||
|
$friends_table = aeromc_friends_friends_table();
|
||||||
|
|
||||||
|
if ($accept) {
|
||||||
|
$wpdb->query($wpdb->prepare(
|
||||||
|
"UPDATE $friends_table SET status = 'accepted' WHERE uuid_a = %s AND uuid_b = %s AND status = 'pending'",
|
||||||
|
$other, $uuid
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
$wpdb->query($wpdb->prepare(
|
||||||
|
"DELETE FROM $friends_table WHERE uuid_a = %s AND uuid_b = %s AND status = 'pending'",
|
||||||
|
$other, $uuid
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return ['ok' => true];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST /friends/cancel {uuid, otherUuid} - eine SELBST gesendete Anfrage zurückziehen */
|
||||||
|
function aeromc_friends_route_cancel($request) {
|
||||||
|
global $wpdb;
|
||||||
|
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
||||||
|
$other = aeromc_friends_normalize_uuid($request->get_param('otherUuid'));
|
||||||
|
if (!$uuid || !$other) {
|
||||||
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid/otherUuid.', ['status' => 400]);
|
||||||
|
}
|
||||||
|
$friends_table = aeromc_friends_friends_table();
|
||||||
|
$wpdb->query($wpdb->prepare(
|
||||||
|
"DELETE FROM $friends_table WHERE uuid_a = %s AND uuid_b = %s AND status = 'pending'",
|
||||||
|
$uuid, $other
|
||||||
|
));
|
||||||
|
return ['ok' => true];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST /friends/remove {uuid, otherUuid} */
|
||||||
|
function aeromc_friends_route_remove($request) {
|
||||||
|
global $wpdb;
|
||||||
|
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
||||||
|
$other = aeromc_friends_normalize_uuid($request->get_param('otherUuid'));
|
||||||
|
if (!$uuid || !$other) {
|
||||||
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid/otherUuid.', ['status' => 400]);
|
||||||
|
}
|
||||||
|
$friends_table = aeromc_friends_friends_table();
|
||||||
|
$wpdb->query($wpdb->prepare(
|
||||||
|
"DELETE FROM $friends_table WHERE ((uuid_a = %s AND uuid_b = %s) OR (uuid_a = %s AND uuid_b = %s)) AND status = 'accepted'",
|
||||||
|
$uuid, $other, $other, $uuid
|
||||||
|
));
|
||||||
|
return ['ok' => true];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user