Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41c63ef0ed | ||
|
|
0595b7ab1a | ||
|
|
cbe7a6d81d | ||
|
|
75be217546 | ||
|
|
78246284d6 | ||
|
|
1d432d08ae | ||
|
|
47b8b88f6e | ||
|
|
906ac75443 |
+136
@@ -0,0 +1,136 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
Alle nennenswerten Änderungen an AeroMC werden hier festgehalten.
|
||||||
|
|
||||||
|
## [1.1.5] - 2026-10-02
|
||||||
|
|
||||||
|
### Neu
|
||||||
|
- Instanz teilen per Code: „Instanz teilen“ in der Seitenleiste erzeugt einen Code (30 Tage gültig) – Freunde lösen ihn unter Importieren → „Per Code“ ein und bekommen Minecraft-Version, Loader sowie alle Mods, Ressourcenpakete und Shader in denselben Versionen
|
||||||
|
- Absturz-Assistent: erklärt die Ursache eines Absturzes in einfachen Worten – fehlende Abhängigkeiten, Mods für eine andere Minecraft-Version, unverträgliche oder doppelte Mods, falsches Java, zu viel oder zu wenig Arbeitsspeicher, Grafiktreiber – und behebt sie auf Wunsch mit einem Klick
|
||||||
|
- Der Absturz-Assistent meldet sich auch, wenn Minecraft schon beim Start scheitert und keinen Absturzbericht schreibt
|
||||||
|
- Nach einer Lösung lässt sich die Instanz direkt aus dem Absturz-Fenster erneut starten
|
||||||
|
- Fehlerberichte per Klick: „An M_Viper senden“ im Absturz-Assistenten schickt Bericht und Mod-Liste an den Entwickler – Windows-Benutzername und Anmeldedaten werden vorher entfernt, ohne Häkchen wird anonym gesendet
|
||||||
|
- Kurznachrichten an Freunde: kleiner Chat im Freunde-Fenster mit Schnellantworten wie „Bin in 10 Min. online“ – neue Nachrichten erscheinen im Launcher und als Meldung im Spiel
|
||||||
|
- Screenshots verschicken: mit 📤 in der Screenshot-Galerie ein Bild direkt an einen Freund schicken, er sieht es im Chat
|
||||||
|
- Schnellstart-Tasten: Lieblings-Instanzen per Rechtsklick auf Strg+1 bis Strg+5 legen und mit der Taste direkt starten
|
||||||
|
- Rechtsklick auf das AeroMC-Symbol neben der Uhr startet Schnellstart- und zuletzt gespielte Instanzen, ohne das Fenster zu öffnen
|
||||||
|
- Welten kopieren: „Kopieren nach …“ übernimmt eine Welt mit einem Klick in eine andere Instanz
|
||||||
|
- Automatische Welt-Sicherung: nach jeder Spielsitzung wird die zuletzt gespielte Welt gesichert, die letzten 5 Stände bleiben erhalten (abschaltbar unter Optionen → Backup)
|
||||||
|
- Rechtsklick-Menü in allen Textfeldern (Ausschneiden, Kopieren, Einfügen, Alles auswählen)
|
||||||
|
|
||||||
|
### Geändert
|
||||||
|
- Freundesystem abgesichert: Anmeldung über Mojangs Sitzungsprüfung wie bei Minecraft-Servern – niemand kann sich mehr als ein anderer Spieler ausgeben, ganz ohne eigenes Passwort
|
||||||
|
- Das Symbol im Infobereich neben der Uhr ist jetzt immer da, nicht nur bei Autostart
|
||||||
|
- Der Zähler am „Freunde“-Knopf zählt jetzt offene Anfragen und ungelesene Nachrichten
|
||||||
|
- Installer: Lizenztext übersichtlich mit Überschriften und Aufzählungen, Rückfragen im Setup-Look statt Windows-Meldungsfenster, der nicht abwählbare Startmenü-Eintrag wird nicht mehr angezeigt
|
||||||
|
- Benötigt das Plugin „AeroMC Freunde“ 1.4.0 auf dem Server (Kurznachrichten, Screenshots, Fehlerberichte, Instanz teilen)
|
||||||
|
|
||||||
|
### Behoben
|
||||||
|
- Windows-Benachrichtigungen zeigten „electron.app.AeroMC“ ohne Logo statt „AeroMC“
|
||||||
|
- Im Import-Fenster standen „Wählen…“, „Suchen“ und „Prüfen“ unter statt neben dem Eingabefeld
|
||||||
|
|
||||||
|
## [1.1.4] - 2026-10-02
|
||||||
|
|
||||||
|
### Neu
|
||||||
|
- Neuer Installer im modernen Look: Schritt-für-Schritt-Assistent mit Lizenz, Ordnerwahl und Verknüpfungen, eigene Deinstallation (Instanzen und Welten bleiben auf Wunsch erhalten) – eine vorhandene Installation wird automatisch übernommen
|
||||||
|
- Optionaler Import im Installer: erkennt Minecraft Launcher, CurseForge, MultiMC, Prism Launcher, PolyMC und Technic und bietet deren Instanzen beim ersten Start zum Übernehmen an
|
||||||
|
- Updates aus dem Launcher heraus laufen jetzt ohne Rückfragen durch und starten AeroMC danach von selbst neu
|
||||||
|
- Freunde: „Beitreten“ startet eine passende Instanz und verbindet direkt mit dem Server, auf dem ein Freund spielt – gibt es keine passende Instanz, wird eine angelegt
|
||||||
|
- Freunde: „Einladen“ schickt einem Freund deinen aktuellen Server – er bekommt die Einladung im Launcher und als Meldung im Spiel (Strg+Alt+J kopiert dort die Adresse)
|
||||||
|
- Freunde: Der eigene Server lässt sich in den Einstellungen vor Freunden verbergen
|
||||||
|
- Clip-Modus in der Spielaufzeichnung: läuft im Hintergrund mit, Alt+F10 speichert die letzten 30, 60 oder 90 Sekunden als MP4
|
||||||
|
- Performance-Paket beim Anlegen einer Fabric-, Quilt- oder NeoForge-Instanz: Sodium, Lithium, FerriteCore, ImmediatelyFast, Entity Culling und ModernFix per Häkchen mitinstallieren
|
||||||
|
- Automatische Mod-Updates vor dem Spielstart (Optionen → Spiel → Mods)
|
||||||
|
- Spielzeit-Statistik: Gesamtspielzeit, Spielstarts, meistgespielte Instanz und Spielzeit pro Instanz auf einen Blick
|
||||||
|
|
||||||
|
### Geändert
|
||||||
|
- Die Suche nach Updates startet jetzt sofort beim Öffnen des Launchers, die Info-Seite zeigt das Ergebnis direkt an
|
||||||
|
- Direkt einem Server beitreten funktioniert jetzt auch bei Minecraft-Versionen vor 1.20
|
||||||
|
|
||||||
|
### Behoben
|
||||||
|
- Die Info-Seite zeigte „Kein Update-Check ausgeführt“, obwohl der Launcher beim Start bereits gesucht hatte
|
||||||
|
|
||||||
|
## [1.1.3] - 2026-09-29
|
||||||
|
|
||||||
|
### Neu
|
||||||
|
- Freundesystem: Freunde per Minecraft-Name hinzufügen, Anfragen annehmen/ablehnen, gesendete Anfragen zurückziehen, Spitznamen vergeben – mit 3D-Spielerkopf und Online-Status
|
||||||
|
- In-Game-Meldung mit 3D-Spielerkopf, sobald ein Freund online kommt – bei mehreren gleichzeitig laufenden Konten erscheint sie im jeweils richtigen Spielfenster
|
||||||
|
- Tastenkombination Strg+Alt+F zeigt im Spiel an, welche Freunde gerade online sind (inklusive Server)
|
||||||
|
- Zähler für offene Freundschaftsanfragen am "Freunde"-Knopf
|
||||||
|
- Freundesystem in den Einstellungen abschaltbar – man ist dann weder sichtbar noch als Freund hinzufügbar, der "Freunde"-Knopf wird ausgeblendet
|
||||||
|
- Neuer Reiter "Aufzeichnungen" im Screenshot-Fenster mit Vorschaubildern, Abspielen und Löschen
|
||||||
|
|
||||||
|
### Geändert
|
||||||
|
- Aufzeichnungen werden jetzt als MP4 gespeichert und laufen damit in jedem gängigen Player
|
||||||
|
- Der Mauszeiger ist in Aufzeichnungen nicht mehr zu sehen
|
||||||
|
|
||||||
|
### Behoben
|
||||||
|
- Aufzeichnungen ruckelten bzw. hatten eine falsche Bildrate
|
||||||
|
- Wechsel des aktiven Microsoft-Kontos wurde nicht in allen Fenstern übernommen
|
||||||
|
|
||||||
|
## [1.1.2] - 2026-09-27
|
||||||
|
|
||||||
|
### Neu
|
||||||
|
- Auflösung, Qualität und Bildrate der Spielaufzeichnung einstellbar (Nativ/1080p/720p/480p, Niedrig/Mittel/Hoch, 30/60 fps) – inklusive Anzeige der tatsächlich aufgenommenen Pixelgröße
|
||||||
|
- Lautstärke jeder einzelnen Tonspur der Aufzeichnung (Systemton, Nur Minecraft, jedes Mikrofon einzeln) regelbar, auch während eine Aufnahme bereits läuft
|
||||||
|
- Video- und Audioeinstellungen der Spielaufzeichnung bleiben jetzt über einen Neustart des Launchers hinweg erhalten
|
||||||
|
|
||||||
|
### Geändert
|
||||||
|
- Anzeige "Aufnahme läuft" samt Timer sitzt jetzt über dem Start/Stopp-Knopf statt auf dem Videobild
|
||||||
|
- Mikrofonnamen in der Audioquellen-Auswahl zeigen keine technische USB-Kennung mehr (z. B. "(9886:0038)")
|
||||||
|
|
||||||
|
### Behoben
|
||||||
|
- Servername in der Spielaufzeichnung fehlte, wenn man sich schon vor dem Öffnen des Aufzeichnungsfensters mit dem Server verbunden hatte – wird jetzt zentral erkannt und trägt sich auch nachträglich in den Titel ein
|
||||||
|
- Vorschaubild der Aufzeichnung blieb beim ersten Öffnen manchmal schwarz – wird jetzt automatisch erkannt und neu erfasst
|
||||||
|
|
||||||
|
## [1.0.9] - 2026-08-09
|
||||||
|
|
||||||
|
### Behoben
|
||||||
|
- Update-Installation brach mit "EPERM: operation not permitted, lstat …" ab, wenn vom vorherigen Versuch noch eine gesperrte Installer-Datei im Temp-Ordner lag (z. B. durch einen Virenscanner) – der Download weicht jetzt automatisch auf einen neuen Dateinamen aus
|
||||||
|
- Autostart mit Windows öffnete im Entwicklungsmodus ein leeres Electron-Fenster statt AeroMC (gleiche Ursache wie zuvor bei den Desktop-Verknüpfungen), zusätzlich zeigte der Autostart-Eintrag bei der Portable-Fassung potenziell auf einen flüchtigen Temp-Pfad
|
||||||
|
- Wiederherstellen aus einem ungültigen oder beschädigten Backup-Archiv löschte die aktuellen, unveränderten Instanzen ersatzlos, obwohl der Vorgang mit einer Fehlermeldung abgebrochen wurde – der Rollback greift jetzt nur noch, wenn am Originalbestand überhaupt schon etwas verändert wurde
|
||||||
|
|
||||||
|
## [1.0.8] - 2026-08-08
|
||||||
|
|
||||||
|
### Neu
|
||||||
|
- Datenpakete lassen sich jetzt wie Mods, Ressourcenpakete und Shader direkt aus dem Modrinth-Katalog durchsuchen, installieren, aktivieren/deaktivieren und aktualisieren
|
||||||
|
- Modpacks lassen sich jetzt auch aus dem CurseForge-Katalog durchsuchen und mit einem Klick installieren (bisher nur Modrinth und Technic; CurseForge ging nur per manuellem ZIP-Import) – braucht wie die bestehende CurseForge-Suche einen eigenen API-Key
|
||||||
|
- OptiFine als eigenständige Loader-Option: Installer-Datei auswählen, AeroMC bereitet die passende Minecraft-Version vor und startet den offiziellen OptiFine-Installer sichtbar; nach Abschluss wird die neue Version automatisch übernommen
|
||||||
|
- Von Hand hinzugefügte OptiFine-Dateien im Mods-Dialog werden jetzt erkannt und eigens gekennzeichnet (funktioniert bei jedem Loader, u. a. für Fabric + OptiFabric)
|
||||||
|
- Lizenztext direkt in den Einstellungen unter Info einsehbar
|
||||||
|
|
||||||
|
### Geändert
|
||||||
|
- Lizenzangabe von MIT auf die tatsächlich geltenden Nutzungsbedingungen umgestellt (nur privater, nicht-kommerzieller Gebrauch – siehe LICENSE)
|
||||||
|
|
||||||
|
## [1.0.7] - 2026-08-06
|
||||||
|
|
||||||
|
### Neu
|
||||||
|
- Desktop-Verknüpfung pro Server: startet die Instanz und verbindet direkt mit dem hinterlegten Server (Quick Play, ab MC 1.20)
|
||||||
|
- Name der Desktop-Verknüpfung (Instanz oder Server) lässt sich beim Erstellen frei selbst vergeben
|
||||||
|
|
||||||
|
### Behoben
|
||||||
|
- Desktop-Verknüpfungen zeigten im Entwicklungsmodus einen leeren Electron-Startbildschirm statt AeroMC zu öffnen
|
||||||
|
- Desktop-Verknüpfungen zeigten im Entwicklungsmodus das Electron-Symbol statt des AeroMC-Logos
|
||||||
|
- Desktop-Verknüpfungen (Instanz und Server) starteten bei kaltem Start unzuverlässig gar keine Instanz – der Startwunsch traf manchmal ein, bevor die Oberfläche bereit war, ihn zu empfangen, und ging dann verloren
|
||||||
|
|
||||||
|
## [1.0.6] - 2026-08-05
|
||||||
|
|
||||||
|
### Neu
|
||||||
|
- Technic-Modpacks direkt durchsuchen und installieren (Katalog), oder eine vorhandene Technic-Installation importieren
|
||||||
|
- Mods lassen sich jetzt auch als eigene Datei hinzufügen, nicht nur über die Suche
|
||||||
|
- Dateien (Mods, Modpacks, Instanzen) per Drag & Drop aufs Fenster ziehen
|
||||||
|
- Tastenkürzel: Strg+F (Suche), Strg+N (neue Instanz), Entf (Instanz löschen)
|
||||||
|
- Windows-Benachrichtigung, wenn ein großer Download oder ein Backup im Hintergrund fertig wird
|
||||||
|
- Serverliste zeigt jetzt Live-Status (Spieleranzahl, MOTD, Version)
|
||||||
|
- Ein-Klick-Knopf für empfohlene GC-Flags bei den JVM-Argumenten
|
||||||
|
- Einzelne Welten lassen sich separat schnellsichern
|
||||||
|
- Warnung, wenn ein Mod entfernt wird, den ein anderer installierter Mod noch braucht
|
||||||
|
- Skin-Hintergrund kann automatisch zwischen mehreren angemeldeten Konten durchwechseln
|
||||||
|
- Optionaler Autostart mit Windows (im Infobereich statt als Fenster)
|
||||||
|
|
||||||
|
### Geändert
|
||||||
|
- Update-Suche läuft automatisch beim Start, Anzeige wird grün wenn alles aktuell ist
|
||||||
|
|
||||||
|
### Behoben
|
||||||
|
- Bedrock-Start repariert (ging nach einem Windows-Update nicht mehr) und startete zwischenzeitlich doppelt
|
||||||
|
- Absturz beim Schließen des Spiels durch hängende Mod-Threads (z. B. JourneyMap-Webmap) behoben
|
||||||
@@ -1,245 +1,354 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
<img src="src/assets/logo.png" alt="AeroMC Logo" width="140" />
|
<img src="src/assets/logo.png" alt="AeroMC Logo" width="140" />
|
||||||
|
|
||||||
# AeroMC
|
# AeroMC
|
||||||
|
|
||||||
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, Spielaufzeichnung, Freundesystem und Microsoft-Anmeldung.
|
||||||
|
|
||||||
Aktueller Stand: Version 1.0.9
|
Aktueller Stand: Version 1.1.5
|
||||||
|
|
||||||
</div>
|
<img src="docs/aeromc.png" alt="AeroMC – Instanzliste mit Seitenleiste und Skin-Vorschau" width="820" />
|
||||||
|
|
||||||
## Überblick
|
</div>
|
||||||
|
|
||||||
AeroMC richtet sich an Spieler, die mehrere Minecraft-Setups sauber voneinander trennen wollen. Jede Instanz kann ihre eigene Version, ihren eigenen Mod-Loader, eigene Mods, eigene Welten und eigene Java-Einstellungen haben.
|
## Überblick
|
||||||
|
|
||||||
Der Launcher konzentriert sich auf den Alltag: Instanz anlegen, Konto auswählen, Mods installieren, starten.
|
AeroMC richtet sich an Spieler, die mehrere Minecraft-Setups sauber voneinander trennen wollen. Jede Instanz kann ihre eigene Version, ihren eigenen Mod-Loader, eigene Mods, eigene Welten und eigene Java-Einstellungen haben.
|
||||||
|
|
||||||
## Funktionen
|
Der Launcher konzentriert sich auf den Alltag: Instanz anlegen, Konto auswählen, Mods installieren, starten – und alles, was drumherum gebraucht wird, vom Aufnehmen bis zum Zusammenspielen mit Freunden.
|
||||||
|
|
||||||
- Beliebig viele getrennte Minecraft-Instanzen
|
## Funktionen
|
||||||
- Unterstützung für Vanilla, Fabric, Quilt, Forge und NeoForge
|
|
||||||
- Versionsliste mit Filter: Releases, Snapshots oder alle
|
**Instanzen**
|
||||||
- Modrinth- **und CurseForge**-Integration für **Mods, Ressourcenpakete und Shader** (Suche wahlweise über beide Quellen, Installation inkl. rekursiver Pflicht-Abhängigkeiten, Update-Prüfung samt Sammel-Update)
|
- Beliebig viele getrennte Minecraft-Instanzen
|
||||||
- Modpacks direkt von Modrinth **oder von der Technic-Plattform** durchsuchen und mit einem Klick als neue Instanz installieren – ohne vorherigen manuellen Download
|
- Unterstützung für Vanilla, Fabric, Quilt, Forge, NeoForge und OptiFine
|
||||||
- Microsoft-Login mit mehreren Konten
|
- Versionsliste mit Filter: Releases, Snapshots oder alle
|
||||||
- Gemeinsamer Import für AeroMC-ZIP, Modrinth-Modpacks (`.mrpack`), Minecraft Launcher, CurseForge, MultiMC/Prism sowie Technic
|
- Performance-Paket beim Anlegen: Sodium, Lithium, FerriteCore, ImmediatelyFast, Entity Culling und ModernFix per Häkchen
|
||||||
- Reimport überschreibt vorhandene Instanzen mit gleichem Namen
|
- Export, Kopieren, Gruppieren, Löschen und Desktop-Verknüpfung pro Instanz
|
||||||
- Export, Kopieren, Gruppieren, Löschen und Desktop-Verknüpfung pro Instanz
|
- Eigene Icons pro Instanz – vorgefertigte Symbole, aus Minecraft extrahierte Block-/Item-Icons oder ein eigenes Bild – plus Notizen
|
||||||
- Mehrfachauswahl mit Strg-Klick für Sammellöschen, Tastenkürzel für die wichtigsten Aktionen (Strg+F, Strg+N, Entf)
|
- Mehrfachauswahl mit Strg-Klick, Tastenkürzel für die wichtigsten Aktionen
|
||||||
- Mods/Modpacks/Instanzen auch per Drag & Drop aufs Fenster ziehen
|
- Schnellstart-Tasten: Lieblings-Instanzen auf **Strg+1** bis **Strg+5** legen
|
||||||
- Mods zusätzlich als eigene Datei hinzufügen, wenn sie nicht über Modrinth/CurseForge zu finden sind
|
- Instanzen per Rechtsklick auf das Symbol neben der Uhr starten, ohne das Fenster zu öffnen
|
||||||
- Warnt beim Entfernen eines Mods, falls ein anderer installierter Mod ihn noch als Pflicht-Abhängigkeit braucht
|
- **Instanz teilen per Code**: Freunde bekommen Version, Loader und alle Mods in denselben Versionen
|
||||||
- Globales Backup inkl. automatischer Zeitplan-Sicherungen, dazu Schnellsicherung einzelner Welten
|
- Spielzeit-Statistik: Gesamtspielzeit, Spielstarts und Spielzeit pro Instanz
|
||||||
- Welten, Ressourcenpakete, Datenpakete, Screenshots, Serverliste und Logdateien verwalten
|
|
||||||
- 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)
|
**Mods, Modpacks und Inhalte**
|
||||||
- Online- und Offline-Start
|
- Modrinth- **und CurseForge**-Integration für Mods, Ressourcenpakete, Shader und Datenpakete (Installation inkl. rekursiver Pflicht-Abhängigkeiten)
|
||||||
- Eingebaute Konsole/Log-Ansicht inkl. Absturzberichte pro Instanz
|
- Update-Prüfung samt Sammel-Update, auf Wunsch automatisch vor jedem Spielstart
|
||||||
- Windows-Benachrichtigung, wenn ein großer Download oder ein Backup im Hintergrund fertig wird
|
- Modpacks direkt von Modrinth, CurseForge oder der Technic-Plattform mit einem Klick installieren
|
||||||
- Eigenen Minecraft-Skin direkt im Launcher hochladen, mit Live-Vorschau anpassen (klassisch/schlank) und zurücksetzen
|
- Mods als eigene Datei hinzufügen oder per Drag & Drop aufs Fenster ziehen
|
||||||
- Skin-Hintergrund des aktiven Kontos (mehrere Posen, einstellbare Deckkraft) – wechselt bei mehreren angemeldeten Konten optional automatisch durch
|
- Warnung beim Entfernen eines Mods, der noch als Pflicht-Abhängigkeit gebraucht wird
|
||||||
- Eigene Icons pro Instanz – vorgefertigte Symbole, direkt aus Minecraft extrahierte Block-/Item-Icons oder ein eigenes Bild – plus Notizen
|
|
||||||
- Java pro Instanz oder global, inkl. automatischem Download fehlender Versionen (Eclipse Temurin)
|
**Import**
|
||||||
- RAM-, JVM-, Pre-Launch- und Post-Exit-Einstellungen (global und pro Instanz), inkl. Ein-Klick-Knopf für empfohlene GC-Flags
|
- Gemeinsamer Import für AeroMC-ZIP, Modrinth-Modpacks (`.mrpack`), Minecraft Launcher, CurseForge, MultiMC/Prism sowie Technic
|
||||||
- Optionaler Wrapper-Befehl und HTTP-Proxy für Downloads
|
- Schon bei der Installation: der Installer erkennt andere Launcher und AeroMC bietet deren Instanzen beim ersten Start zum Übernehmen an
|
||||||
- Bedrock-Bereich für vorhandene Bedrock-Installation unter Windows, inkl. Installation zusätzlicher Add-ons
|
- Von Freunden geteilte Instanzen per Code einlösen
|
||||||
- Update-Suche läuft automatisch beim Start, dazu manuelle Prüfung in den Einstellungen
|
|
||||||
- Optionaler Autostart mit Windows, dann im Infobereich statt als Fenster
|
**Spielaufzeichnung**
|
||||||
- Design Hell/Dunkel und Sortierung der Instanzen
|
- Nimmt nur das Minecraft-Fenster auf – ohne Desktop-Ränder und ohne Mauszeiger – und speichert als MP4
|
||||||
|
- Auflösung, Qualität und Bildrate einstellbar, mehrere Tonspuren (Systemton, nur Minecraft, Mikrofone) mit eigener Lautstärke
|
||||||
## Installation
|
- Clip-Modus: läuft im Hintergrund mit, **Alt+F10** speichert die letzten 30, 60 oder 90 Sekunden
|
||||||
|
- Servername im Aufnahmetitel, Aufzeichnungen-Galerie mit Vorschaubildern
|
||||||
Es gibt zwei Varianten:
|
|
||||||
|
**Freunde**
|
||||||
- **AeroMC-Setup.exe** – installiert den Launcher, schneller Start im Alltag
|
- Freunde per Minecraft-Name hinzufügen, mit 3D-Spielerkopf, Online-Status und Spitznamen
|
||||||
- **AeroMC-Portable.exe** – startet ohne Installation, ideal zum Mitnehmen (etwas längerer Start)
|
- Meldung im Spiel, sobald ein Freund online kommt – bei mehreren Konten im jeweils richtigen Fenster
|
||||||
|
- **Beitreten**: startet eine passende Instanz und verbindet direkt mit dem Server eines Freundes
|
||||||
Automatische Updates bleiben getrennt: Setup erhält nur Setup-Updates, Portable nur Portable-Updates. Ein automatischer Wechsel zwischen beiden Varianten findet nicht statt.
|
- **Einladen**: schickt Freunden deinen aktuellen Server – als Hinweis im Launcher und als Meldung im Spiel
|
||||||
|
- **Kurznachrichten** mit Schnellantworten wie „Bin in 10 Min. online“, auch als Meldung im Spiel
|
||||||
## Voraussetzungen
|
- **Screenshots** direkt aus der Galerie an Freunde schicken
|
||||||
|
- Anmeldung über Mojangs Sitzungsprüfung – niemand kann sich als ein anderer Spieler ausgeben
|
||||||
- Windows x64
|
- Eigener Server auf Wunsch verborgen, Freundesystem komplett abschaltbar
|
||||||
- Legale Minecraft Java Edition mit Microsoft-Konto
|
|
||||||
- Internetverbindung beim ersten Download von Minecraft-Dateien, Loadern oder Inhalten
|
**Konten, Welten und Backups**
|
||||||
- Für Bedrock: installierte Minecraft Bedrock Edition aus dem Microsoft Store
|
- Microsoft-Login mit mehreren Konten
|
||||||
|
- Eigenen Skin im Launcher hochladen, mit Live-Vorschau anpassen (klassisch/schlank) und zurücksetzen
|
||||||
## Erste Schritte
|
- Skin-Hintergrund des aktiven Kontos (mehrere Posen, einstellbare Deckkraft, optional wechselnd)
|
||||||
|
- Welten, Ressourcenpakete, Datenpakete, Screenshots, Serverliste und Logdateien verwalten
|
||||||
1. AeroMC installieren oder die portable Datei starten.
|
- Serverliste mit Live-Status und Desktop-Verknüpfung pro Server (startet die Instanz und verbindet direkt)
|
||||||
2. Über **Konten** ein Microsoft-Konto hinzufügen.
|
- Welten mit einem Klick in eine andere Instanz kopieren
|
||||||
3. Eine neue Instanz anlegen.
|
- Globales Backup inkl. automatischer Zeitplan-Sicherungen, dazu Schnellsicherung einzelner Welten
|
||||||
4. Minecraft-Version und optional einen Loader wählen.
|
- Automatische Welt-Sicherung nach jeder Spielsitzung (die letzten 5 Stände je Welt)
|
||||||
5. Optional Mods/Ressourcenpakete/Shader hinzufügen oder bestehende Setups über **Importieren** übernehmen.
|
|
||||||
6. Instanz starten (online oder offline).
|
**Technik und Komfort**
|
||||||
|
- Java pro Instanz oder global, inkl. automatischem Download fehlender Versionen (Eclipse Temurin)
|
||||||
## Instanzen
|
- RAM-, JVM-, Pre-Launch- und Post-Exit-Einstellungen (global und pro Instanz), Ein-Klick-Knopf für empfohlene GC-Flags
|
||||||
|
- **Absturz-Assistent**: erklärt Abstürze in einfachen Worten und behebt sie oft mit einem Klick; auf Wunsch geht der Bericht an den Entwickler
|
||||||
Jede Instanz hat:
|
- Eingebaute Konsole/Log-Ansicht inkl. Absturzberichte pro Instanz
|
||||||
|
- Bedrock-Bereich für eine vorhandene Bedrock-Installation, inkl. Add-on-Installation
|
||||||
- Namen und optionale Gruppe
|
- Update-Suche sofort beim Start, Updates installieren sich ohne Rückfragen
|
||||||
- Minecraft-Version und optionalen Mod-Loader
|
- Optionaler Autostart mit Windows (im Infobereich statt als Fenster)
|
||||||
- optionales Icon (vorgegeben, aus Minecraft extrahiert oder eigenes Bild) und Notizen
|
- Rechtsklick-Menü in allen Textfeldern (Ausschneiden, Kopieren, Einfügen)
|
||||||
- eigene Mods, Ressourcenpakete, Shader, Welten, Datenpakete, Screenshots und Serverliste
|
- Design Hell/Dunkel, optionaler HTTP-Proxy und Wrapper-Befehl
|
||||||
- eigene Java-, RAM-, JVM-, Pre-Launch- und Post-Exit-Einstellungen (leer = globale Vorgabe), inkl. Knopf für empfohlene GC-Flags bei den JVM-Argumenten
|
|
||||||
|
## Installation
|
||||||
Über die Seitenleiste einer ausgewählten Instanz erreichst du u. a.:
|
|
||||||
|
Es gibt zwei Varianten:
|
||||||
- Starten / Offline starten
|
|
||||||
- Instanz und Notizen bearbeiten
|
- **AeroMC-Setup.exe** – installiert den Launcher, schneller Start im Alltag
|
||||||
- Mods, Ressourcenpakete und Shader
|
- **AeroMC-Portable.exe** – startet ohne Installation, ideal zum Mitnehmen (etwas längerer Start)
|
||||||
- Welten (inkl. Schnellsicherung einzelner Welten), Datenpakete und Bildschirmfotos
|
|
||||||
- Server verwalten (Live-Status, Übernahme eines Eintrags in andere Instanzen, Desktop-Verknüpfung pro Server)
|
Der Installer führt Schritt für Schritt durch Willkommen, Lizenz, Optionen (Ordner, Desktop-Verknüpfung) und – falls andere Launcher gefunden werden – einen optionalen Import. Administratorrechte sind nicht nötig, installiert wird nur für das eigene Benutzerkonto. Eine bereits vorhandene Installation wird automatisch übernommen; Instanzen, Welten und Einstellungen bleiben erhalten.
|
||||||
- Logdateien und Absturzberichte ansehen
|
|
||||||
- Minecraft-, Konfigurations- und Instanz-Ordner öffnen
|
Deinstallieren über **Windows-Einstellungen → Apps → AeroMC**. Instanzen und Welten bleiben dabei standardmäßig erhalten und lassen sich auf Wunsch mit entfernen (Papierkorb).
|
||||||
- Desktop-Verknüpfung erstellen
|
|
||||||
- Exportieren, Kopieren, Gruppe ändern, Löschen
|
Automatische Updates bleiben getrennt: Setup erhält nur Setup-Updates, Portable nur Portable-Updates. Ein automatischer Wechsel zwischen beiden Varianten findet nicht statt.
|
||||||
- Bei importierten Modpacks: Modpack aktualisieren
|
|
||||||
|
## Voraussetzungen
|
||||||
In der Instanzliste: Suche, Sortierung und **Strg + Klick** für Mehrfachauswahl.
|
|
||||||
|
- Windows 10 oder 11 (x64)
|
||||||
### Tastenkürzel
|
- Legale Minecraft Java Edition mit Microsoft-Konto
|
||||||
|
- Internetverbindung beim ersten Download von Minecraft-Dateien, Loadern oder Inhalten
|
||||||
| Taste | Wirkung |
|
- Für Bedrock: installierte Minecraft Bedrock Edition aus dem Microsoft Store
|
||||||
|---|---|
|
|
||||||
| Strg+F | Suche fokussieren |
|
## Erste Schritte
|
||||||
| Strg+N | Neue Instanz anlegen |
|
|
||||||
| Entf | Markierte Instanz(en) löschen |
|
1. AeroMC installieren oder die portable Datei starten.
|
||||||
| Esc | Aktuellen Dialog schließen |
|
2. Über **Konten** ein Microsoft-Konto hinzufügen.
|
||||||
|
3. Eine neue Instanz anlegen, Minecraft-Version und optional einen Loader wählen – bei Fabric, Quilt oder NeoForge auf Wunsch gleich mit Performance-Paket.
|
||||||
Greifen nicht, während ein Textfeld fokussiert ist oder ein Dialog mit eigener Suche offen ist.
|
4. Optional Mods/Ressourcenpakete/Shader hinzufügen oder bestehende Setups über **Importieren** übernehmen.
|
||||||
|
5. Instanz starten (online oder offline).
|
||||||
## Import
|
|
||||||
|
## Instanzen
|
||||||
Über **Importieren**:
|
|
||||||
|
Jede Instanz hat:
|
||||||
| Quelle | Typische Nutzung |
|
|
||||||
|---|---|
|
- Namen und optionale Gruppe
|
||||||
| ZIP-Archiv | AeroMC-Export (`instance.json`) sowie einzelne MultiMC/Prism-Instanzen (auch gezippt) |
|
- Minecraft-Version und optionalen Mod-Loader
|
||||||
| Modpack | Modrinth-Katalog direkt durchsuchen und installieren, oder Datei importieren: `.mrpack` sowie CurseForge-ZIP (`manifest.json`) |
|
- optionales Icon (vorgegeben, aus Minecraft extrahiert oder eigenes Bild) und Notizen
|
||||||
| Minecraft Launcher | Profile aus dem offiziellen Launcher |
|
- eigene Mods, Ressourcenpakete, Shader, Welten, Datenpakete, Screenshots, Aufzeichnungen und Serverliste
|
||||||
| CurseForge | Instanzen aus dem CurseForge-Ordner |
|
- eigene Java-, RAM-, JVM-, Pre-Launch- und Post-Exit-Einstellungen (leer = globale Vorgabe)
|
||||||
| MultiMC / Prism | Vorhandene MultiMC- oder Prism-Instanzen |
|
|
||||||
| Technic | Technic-Katalog direkt durchsuchen und installieren, oder eine vorhandene Technic-Installation importieren (`%APPDATA%\.technic\modpacks`) |
|
Über die Seitenleiste einer ausgewählten Instanz erreichst du u. a.:
|
||||||
|
|
||||||
Existiert bereits eine Instanz mit demselben Namen, wird sie ersetzt.
|
- Starten / Offline starten
|
||||||
|
- Instanz und Notizen bearbeiten
|
||||||
Beim offiziellen Minecraft Launcher werden nur benutzerdefinierte Profile angeboten (keine „Latest Release“/„Latest Snapshot“). Bei gemeinsamen Standard-Spielordnern übernimmt AeroMC spielrelevante Daten (Mods, Welten, Konfiguration), nicht den kompletten Cache.
|
- Mods, Ressourcenpakete und Shader
|
||||||
|
- Welten (inkl. Schnellsicherung und „Kopieren nach …“ in eine andere Instanz), Datenpakete, Bildschirmfotos und Aufzeichnungen
|
||||||
Im Reiter **Modpack** desselben Dialogs lässt sich zusätzlich direkt im Modrinth-Katalog suchen und ein Modpack mit einem Klick installieren – ganz ohne vorherigen manuellen Download der `.mrpack`-Datei. Im Reiter **Technic** gibt es dasselbe für den Technic-Katalog.
|
- Spiel aufzeichnen
|
||||||
|
- Server verwalten (Live-Status, Übernahme eines Eintrags in andere Instanzen, Desktop-Verknüpfung pro Server)
|
||||||
Alternativ: `.jar`-, `.mrpack`- oder `.zip`-Dateien direkt aufs Launcher-Fenster ziehen. AeroMC erkennt anhand des Inhalts, ob es sich um einen Mod (landet in der gerade offenen/ausgewählten Instanz), ein Modpack oder einen Instanz-Export handelt.
|
- Logdateien und Absturzberichte ansehen
|
||||||
|
- Minecraft-, Konfigurations- und Instanz-Ordner öffnen
|
||||||
## Mods, Ressourcenpakete und Shader
|
- Desktop-Verknüpfung erstellen
|
||||||
|
- Exportieren, **Instanz teilen** (Code für Freunde), Kopieren, Gruppe ändern, Löschen
|
||||||
Im Inhalts-Dialog einer Instanz:
|
- Bei importierten Modpacks: Modpack aktualisieren
|
||||||
|
|
||||||
- Katalog durchsuchen (Quelle Modrinth oder CurseForge, Sortierung, Kategorien)
|
In der Instanzliste: Suche, Sortierung und **Strg + Klick** für Mehrfachauswahl. Über **Statistik** in der oberen Leiste gibt es die Spielzeit aller Instanzen im Überblick.
|
||||||
- Installation inkl. **automatischer** Installation aller Pflicht-Abhängigkeiten (rekursiv, auch quellübergreifend)
|
|
||||||
- Eigene Datei hinzufügen (`.jar`/`.zip`) – für Mods, die nicht über Modrinth/CurseForge zu finden sind, oder per Drag & Drop aufs Fenster ziehen
|
**Schnellstart:** Rechtsklick auf eine Instanz → **Schnellstart-Taste …** legt sie auf Strg+1 bis Strg+5; die Kachel zeigt dann ein kleines Abzeichen. Schnellstart- und zuletzt gespielte Instanzen stehen außerdem im Rechtsklick-Menü des AeroMC-Symbols neben der Uhr – ein Klick startet das Spiel, ohne das Launcher-Fenster zu öffnen.
|
||||||
- Installierte Einträge aktivieren, deaktivieren, entfernen – beim Entfernen warnt AeroMC, falls ein anderer installierter Mod das noch als Pflicht-Abhängigkeit braucht
|
|
||||||
- Nach Updates suchen und alle aktualisieren
|
**Instanz teilen:** „Instanz teilen“ in der Seitenleiste erzeugt einen Code wie `K7QD-M2XP` (30 Tage gültig). Freunde lösen ihn unter **Importieren → Per Code** ein und bekommen Minecraft-Version, Loader sowie alle Mods, Ressourcenpakete und Shader in denselben Versionen – geladen direkt von Modrinth bzw. CurseForge. Von Hand hinzugefügte Dateien sind nicht dabei; AeroMC nennt sie.
|
||||||
- Ordner der jeweiligen Inhaltsart öffnen
|
|
||||||
|
### Tastenkürzel
|
||||||
Ressourcenpakete und Shader funktionieren auch in Vanilla-Instanzen. Mods benötigen einen Mod-Loader. Für CurseForge-Inhalte wird ein eigener API-Key oder eine Weiterleitungs-Adresse benötigt (siehe **Einstellungen → Netzwerk**).
|
|
||||||
|
Im Launcher:
|
||||||
## Skin ändern
|
|
||||||
|
| Taste | Wirkung |
|
||||||
Über **Konten → Skin ändern…** lässt sich der Skin des aktiven Microsoft-Kontos direkt im Launcher ändern:
|
|---|---|
|
||||||
|
| Strg+F | Suche fokussieren |
|
||||||
- PNG-Datei auswählen, sofortige Prüfung auf gültige Maße
|
| Strg+N | Neue Instanz anlegen |
|
||||||
- Live-Vorschau am 3D-Modell vor dem Anwenden
|
| Strg+1 … Strg+5 | Schnellstart-Instanz starten |
|
||||||
- Modell wählen: klassisch oder schlank (slim)
|
| Entf | Markierte Instanz(en) löschen |
|
||||||
- Auf einen früheren Standard-Skin zurücksetzen
|
| Esc | Aktuellen Dialog schließen |
|
||||||
|
|
||||||
Die Änderung wird direkt bei Mojang für das angemeldete Konto hinterlegt und gilt somit auch außerhalb von AeroMC.
|
Greifen nicht, während ein Textfeld fokussiert ist oder ein Dialog mit eigener Suche offen ist.
|
||||||
|
|
||||||
## Backup und Wiederherstellung
|
Im Spiel (solange ein über AeroMC gestartetes Minecraft läuft):
|
||||||
|
|
||||||
Unter **Optionen → Backup**:
|
| Taste | Wirkung |
|
||||||
|
|---|---|
|
||||||
- Manuelles globales Backup (alle Instanzen + Launcher-Einstellungen)
|
| Strg+Alt+F | Zeigt, welche Freunde gerade online sind |
|
||||||
- Automatische Sicherungen (täglich oder wöchentlich, einstellbare Anzahl)
|
| Strg+Alt+J | Kopiert die Server-Adresse der letzten Einladung |
|
||||||
- Optional zusätzlich vor jedem Spielstart sichern
|
| Alt+F10 | Speichert im Clip-Modus die letzten Sekunden (ersatzweise Strg+Alt+C) |
|
||||||
- Wiederherstellung aus vorhandenem Backup
|
|
||||||
|
## Import
|
||||||
Speicherort: `Dokumente\AeroMC Launcher\Backups`.
|
|
||||||
|
Über **Importieren**:
|
||||||
**Wichtig:** Konten und Tokens werden nicht mitgesichert. Beim Wiederherstellen werden aktuelle Instanzen und Launcher-Einstellungen durch den Backup-Inhalt ersetzt.
|
|
||||||
|
| Quelle | Typische Nutzung |
|
||||||
## Einstellungen
|
|---|---|
|
||||||
|
| ZIP-Archiv | AeroMC-Export (`instance.json`) sowie einzelne MultiMC/Prism-Instanzen (auch gezippt) |
|
||||||
### Allgemein
|
| Modpack | Modrinth- oder CurseForge-Katalog direkt durchsuchen und installieren, oder Datei importieren: `.mrpack` sowie CurseForge-ZIP (`manifest.json`) |
|
||||||
- Design (Dunkel / Hell)
|
| Minecraft Launcher | Profile aus dem offiziellen Launcher |
|
||||||
- Instanzen sortieren (nach letztem Start / nach Name)
|
| CurseForge | Instanzen aus dem CurseForge-Ordner |
|
||||||
- Instanz-Verzeichnis
|
| MultiMC / Prism | Vorhandene MultiMC- oder Prism-Instanzen |
|
||||||
- Bedrock-Bereich anzeigen
|
| Technic | Technic-Katalog direkt durchsuchen und installieren, oder eine vorhandene Technic-Installation importieren (`%APPDATA%\.technic\modpacks`) |
|
||||||
- Windows-Benachrichtigung bei fertigem Hintergrund-Download/-Backup (nur wenn das Fenster nicht im Vordergrund ist)
|
| Per Code | Von einem Freund geteilte Instanz einlösen (siehe **Instanzen → Instanz teilen**) |
|
||||||
- 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)
|
|
||||||
|
Existiert bereits eine Instanz mit demselben Namen, wird sie ersetzt.
|
||||||
### Darstellung
|
|
||||||
- Block-Symbole räumlich (3D)
|
Beim offiziellen Minecraft Launcher werden nur benutzerdefinierte Profile angeboten (keine „Latest Release“/„Latest Snapshot“). Bei gemeinsamen Standard-Spielordnern übernimmt AeroMC spielrelevante Daten (Mods, Welten, Konfiguration), nicht den kompletten Cache.
|
||||||
- Skin-Hintergrund: Quelle, Pose, Deckkraft, optional automatischer Wechsel zwischen mehreren angemeldeten Konten (mit einstellbarem Zeitabstand)
|
|
||||||
|
**Schon bei der Installation:** Der Installer sucht nach Minecraft Launcher, CurseForge, MultiMC, Prism Launcher, PolyMC und Technic – ein portables MultiMC auch über seine Verknüpfung auf dem Desktop oder im Startmenü. Was dort angekreuzt wird, öffnet AeroMC beim ersten Start fertig durchsucht im Import; ausgewählt wird dann nur noch, was übernommen werden soll. In den anderen Launchern wird nichts verändert.
|
||||||
### Spiel
|
|
||||||
- Standard-RAM (min./max.)
|
Alternativ: `.jar`-, `.mrpack`- oder `.zip`-Dateien direkt aufs Launcher-Fenster ziehen. AeroMC erkennt anhand des Inhalts, ob es sich um einen Mod (landet in der gerade ausgewählten Instanz), ein Modpack oder einen Instanz-Export handelt.
|
||||||
- Fenstergröße bzw. maximiert starten
|
|
||||||
|
## Mods, Ressourcenpakete und Shader
|
||||||
### Start & Java
|
|
||||||
- Globale Java-Auswahl und Erkennung
|
Im Inhalts-Dialog einer Instanz:
|
||||||
- Fehlende Java-Version automatisch herunterladen
|
|
||||||
- Konsole: während des Spiels, nach Beenden schließen, bei Absturz anzeigen, Historie-Limit
|
- Katalog durchsuchen (Quelle Modrinth oder CurseForge, Sortierung, Kategorien)
|
||||||
- Globale Pre-Launch- / Post-Exit-Befehle
|
- Installation inkl. **automatischer** Installation aller Pflicht-Abhängigkeiten (rekursiv, auch quellübergreifend)
|
||||||
- Optionaler Wrapper-Befehl
|
- Eigene Datei hinzufügen (`.jar`/`.zip`) – für Mods, die nicht über Modrinth/CurseForge zu finden sind, oder per Drag & Drop
|
||||||
|
- Installierte Einträge aktivieren, deaktivieren, entfernen – mit Warnung, falls ein anderer Mod sie noch braucht
|
||||||
### Netzwerk
|
- Nach Updates suchen und alle aktualisieren
|
||||||
- HTTP-Proxy (Host, Port, optional Benutzer/Passwort)
|
- Ordner der jeweiligen Inhaltsart öffnen
|
||||||
- CurseForge: eigener API-Key und/oder Weiterleitungs-Adresse inkl. Token
|
|
||||||
|
Ressourcenpakete und Shader funktionieren auch in Vanilla-Instanzen. Mods benötigen einen Mod-Loader. Für CurseForge-Inhalte wird ein eigener API-Key oder eine Weiterleitungs-Adresse benötigt (siehe **Einstellungen → Netzwerk**).
|
||||||
Ohne eigenen Key und ohne Weiterleitung funktionieren Modrinth-Inhalte weiterhin. Für CurseForge-Downloads wird einer der beiden Wege benötigt.
|
|
||||||
|
**Performance-Paket:** Beim Anlegen einer Fabric-, Quilt- oder NeoForge-Instanz installiert ein Häkchen Sodium, Lithium, FerriteCore, ImmediatelyFast, Entity Culling und ModernFix gleich mit. Was es für die gewählte Version noch nicht gibt, wird übersprungen.
|
||||||
### Konten
|
|
||||||
- Mehrere Microsoft-Konten verwalten, aktiv setzen, entfernen
|
**Automatische Mod-Updates:** Unter **Optionen → Spiel → Mods** sucht AeroMC auf Wunsch vor jedem Start nach neueren Fassungen der Modrinth-Mods und installiert sie. Dauert die Suche länger als 20 Sekunden, startet Minecraft ohne Update.
|
||||||
|
|
||||||
### Backup
|
## Spielaufzeichnung
|
||||||
- Manuell, Zeitplan und Wiederherstellung
|
|
||||||
|
Über **Spiel aufzeichnen** in der Seitenleiste:
|
||||||
### Info
|
|
||||||
- Version, Autor und Links
|
- Aufgenommen wird nur das Minecraft-Fenster – ohne Desktop-Ränder und ohne Mauszeiger
|
||||||
- Update-Suche läuft automatisch kurz nach dem Start (Status erscheint grün, wenn alles aktuell ist) sowie jederzeit manuell per Knopf
|
- Auflösung (Nativ, 1080p, 720p, 480p), Qualität (Niedrig/Mittel/Hoch) und Bildrate (30/60 fps) einstellbar
|
||||||
|
- Tonspuren: Systemton, nur Minecraft (isoliert) und beliebig viele Mikrofone, jede mit eigenem Lautstärkeregler – auch während der Aufnahme
|
||||||
## Bedrock
|
- Der Server, auf dem gerade gespielt wird, erscheint automatisch im Titel
|
||||||
|
- Gespeichert wird als MP4 unter `Videos\AeroMC Launcher\<Instanz>`, zu finden auch unter **Bildschirmfotos verwalten → Aufzeichnungen**
|
||||||
Der Bedrock-Bereich startet die vorhandene Windows-Store-Version und erleichtert den Zugriff auf Welten, Pakete, Add-ons und Ordner. Zusätzliche Add-on-Dateien lassen sich direkt über AeroMC installieren. AeroMC installiert Bedrock selbst (die App aus dem Microsoft Store) nicht. Fehlt die Installation, wird der Bereich automatisch ausgeblendet.
|
|
||||||
|
**Clip-Modus:** Läuft im Hintergrund mit, auch bei geschlossenem Aufzeichnungsfenster. **Alt+F10** (ist das belegt, z. B. durch ShadowPlay: Strg+Alt+C) speichert die letzten 30, 60 oder 90 Sekunden als `Clip_….mp4`, mit kurzer Meldung im Spiel. Endet Minecraft, endet auch der Clip-Modus.
|
||||||
## Konsole, Logs und Crash-Reports
|
|
||||||
|
## Freunde
|
||||||
- Konsole über die obere Leiste oder bei Absturz
|
|
||||||
- Pro Instanz: gespeicherte Logdateien und Crash-Reports einsehen, kopieren und Ordner öffnen
|
Über **Freunde** in der oberen Leiste:
|
||||||
|
|
||||||
## Datenort
|
- Freunde per Minecraft-Name hinzufügen, Anfragen annehmen, ablehnen oder zurückziehen – offene Anfragen zählt der Knopf mit
|
||||||
|
- Spitznamen vergeben, Online-Status und Server der Freunde sehen
|
||||||
Nutzerdaten liegen lokal unter `%APPDATA%\aeromc`.
|
- Meldung im Spiel mit 3D-Spielerkopf, sobald ein Freund online kommt
|
||||||
|
- **Beitreten** startet eine passende eigene Instanz (gleiche Minecraft-Version, bevorzugt mit dem Server in der Liste) und verbindet direkt; gibt es keine, wird eine Vanilla-Instanz angelegt
|
||||||
## Links
|
- **Einladen** schickt Freunden den eigenen aktuellen Server. Im Launcher erscheint die Einladung mit „Beitreten“, im Spiel als Meldung – dort kopiert **Strg+Alt+J** die Adresse für „Direkt verbinden“
|
||||||
|
|
||||||
- Webseite: https://m-viper.de
|
- **Kurznachrichten**: 💬 beim Freund öffnet eine kleine Unterhaltung mit Schnellantworten („Bin in 10 Min. online“, „Komm online!“ …) oder eigenem Text bis 200 Zeichen. Neue Nachrichten erscheinen als Hinweis im Launcher und – beim Spielen – als Meldung im Spiel; der Zähler am „Freunde“-Knopf zählt offene Anfragen und ungelesene Nachrichten. Nachrichten werden nach 7 Tagen gelöscht.
|
||||||
- Launcher-Webseite: https://aeromc.viper.ipv64.net
|
- **Screenshots verschicken**: In **Bildschirmfotos verwalten** schickt 📤 ein Bild (verkleinert auf höchstens 1600 Pixel Breite) samt optionalem Text an einen Freund; er sieht es im Chat.
|
||||||
- Autor: M_Viper
|
|
||||||
|
**Sicherheit:** AeroMC meldet sich beim Freundesystem wie ein Minecraft-Server über Mojangs Sitzungsprüfung an – ohne eigenes Passwort, und niemand kann sich mit einer fremden Spieler-ID als jemand anderes ausgeben.
|
||||||
## Rechtliches
|
|
||||||
|
Unter **Optionen → Allgemein → Freundesystem** lässt sich der eigene Server vor Freunden verbergen oder das Freundesystem ganz abschalten – dann ist man für niemanden sichtbar und kann nicht hinzugefügt werden.
|
||||||
AeroMC ist kein offizielles Produkt von Mojang oder Microsoft.
|
|
||||||
Minecraft ist eine Marke von Mojang AB.
|
Das Freundesystem funktioniert nur zwischen Spielern, die AeroMC nutzen und mindestens einmal mit eingeschaltetem Freundesystem darüber gespielt haben.
|
||||||
|
|
||||||
### Urheberrecht & Nutzungsbedingungen
|
## Skin ändern
|
||||||
|
|
||||||
AeroMC ist urheberrechtlich geschützt. © M_Viper – Alle Rechte vorbehalten.
|
Über **Konten → Skin ändern…** lässt sich der Skin des aktiven Microsoft-Kontos direkt im Launcher ändern:
|
||||||
|
|
||||||
- ❌ Die Weitergabe dieses Launchers an Dritte ist strengstens untersagt
|
- PNG-Datei auswählen, sofortige Prüfung auf gültige Maße
|
||||||
- ❌ Das Verändern, Dekompilieren oder Bearbeiten des Codes ist nicht erlaubt
|
- Live-Vorschau am 3D-Modell vor dem Anwenden
|
||||||
- ❌ Eine Veröffentlichung – auch in veränderter Form – ist verboten
|
- Modell wählen: klassisch oder schlank (slim)
|
||||||
- ❌ Der Verkauf oder die kommerzielle Nutzung ist nicht gestattet
|
- Auf einen früheren Standard-Skin zurücksetzen
|
||||||
- ✅ Die Nutzung ist ausschließlich für den privaten, nicht-kommerziellen Gebrauch erlaubt
|
|
||||||
|
Die Änderung wird direkt bei Mojang für das angemeldete Konto hinterlegt und gilt somit auch außerhalb von AeroMC.
|
||||||
Wer gegen diese Bedingungen verstößt, macht sich strafbar und kann rechtlich belangt werden.
|
|
||||||
|
## Backup und Wiederherstellung
|
||||||
|
|
||||||
|
Unter **Optionen → Backup**:
|
||||||
|
|
||||||
|
- Manuelles globales Backup (alle Instanzen + Launcher-Einstellungen)
|
||||||
|
- Automatische Sicherungen (täglich oder wöchentlich, einstellbare Anzahl)
|
||||||
|
- Optional zusätzlich vor jedem Spielstart sichern
|
||||||
|
- Wiederherstellung aus vorhandenem Backup
|
||||||
|
|
||||||
|
Speicherort: `Dokumente\AeroMC Launcher\Backups`.
|
||||||
|
|
||||||
|
**Welten einzeln:** In **Welten verwalten** sichert „Sichern“ eine einzelne Welt als ZIP. Zusätzlich sichert AeroMC nach jeder Spielsitzung automatisch die zuletzt gespielte Welt; pro Welt bleiben die letzten 5 Stände erhalten (`Dokumente\AeroMC Launcher\Welten-Sicherungen\<Instanz>\Automatisch`). Abschaltbar unter **Optionen → Backup → Welten**.
|
||||||
|
|
||||||
|
**Wichtig:** Konten und Tokens werden nicht mitgesichert. Beim Wiederherstellen werden aktuelle Instanzen und Launcher-Einstellungen durch den Backup-Inhalt ersetzt.
|
||||||
|
|
||||||
|
## Einstellungen
|
||||||
|
|
||||||
|
### Allgemein
|
||||||
|
- Design (Dunkel / Hell)
|
||||||
|
- Instanzen sortieren (nach letztem Start / nach Name)
|
||||||
|
- Instanz-Verzeichnis
|
||||||
|
- Bereiche: Bedrock-Bereich anzeigen
|
||||||
|
- Windows-Benachrichtigung bei fertigem Hintergrund-Download/-Backup (nur wenn das Fenster nicht im Vordergrund ist)
|
||||||
|
- Freundesystem ein-/ausschalten, eigenen Server für Freunde sichtbar oder verborgen
|
||||||
|
- Autostart: mit Windows starten, im Infobereich statt als Fenster – solange aktiv, versteckt „Schließen“ (X) den Launcher nur. Das Symbol im Infobereich gibt es immer; per Rechtsklick startest du dort Instanzen.
|
||||||
|
|
||||||
|
### Darstellung
|
||||||
|
- Block-Symbole räumlich (3D)
|
||||||
|
- Skin-Hintergrund: Quelle, Pose, Deckkraft, optional automatischer Wechsel zwischen mehreren Konten
|
||||||
|
|
||||||
|
### Spiel
|
||||||
|
- Standard-RAM (min./max.)
|
||||||
|
- Fenstergröße bzw. maximiert starten
|
||||||
|
- Mods: vor jedem Start automatisch nach Updates suchen und installieren
|
||||||
|
|
||||||
|
### Start & Java
|
||||||
|
- Globale Java-Auswahl und Erkennung
|
||||||
|
- Fehlende Java-Version automatisch herunterladen
|
||||||
|
- Konsole: während des Spiels, nach Beenden schließen, bei Absturz anzeigen, Historie-Limit
|
||||||
|
- Globale Pre-Launch- / Post-Exit-Befehle
|
||||||
|
- Optionaler Wrapper-Befehl
|
||||||
|
|
||||||
|
### Netzwerk
|
||||||
|
- HTTP-Proxy (Host, Port, optional Benutzer/Passwort)
|
||||||
|
- CurseForge: eigener API-Key und/oder Weiterleitungs-Adresse inkl. Token
|
||||||
|
|
||||||
|
Ohne eigenen Key und ohne Weiterleitung funktionieren Modrinth-Inhalte weiterhin. Für CurseForge-Downloads wird einer der beiden Wege benötigt.
|
||||||
|
|
||||||
|
### Konten
|
||||||
|
- Mehrere Microsoft-Konten verwalten, aktiv setzen, entfernen
|
||||||
|
|
||||||
|
### Backup
|
||||||
|
- Manuell, Zeitplan und Wiederherstellung
|
||||||
|
- Welten: nach jeder Spielsitzung die zuletzt gespielte Welt sichern (Standard: an)
|
||||||
|
|
||||||
|
### Info
|
||||||
|
- Version, Autor, Links und Lizenz
|
||||||
|
- Update-Suche läuft sofort beim Start, das Ergebnis steht direkt hier (grün, wenn alles aktuell ist); dazu jederzeit manuell per Knopf. Updates installieren sich ohne Rückfragen und starten AeroMC danach neu.
|
||||||
|
|
||||||
|
## Bedrock
|
||||||
|
|
||||||
|
Der Bedrock-Bereich startet die vorhandene Windows-Store-Version und erleichtert den Zugriff auf Welten, Pakete, Add-ons und Ordner. Zusätzliche Add-on-Dateien lassen sich direkt über AeroMC installieren. AeroMC installiert Bedrock selbst (die App aus dem Microsoft Store) nicht. Fehlt die Installation, wird der Bereich automatisch ausgeblendet.
|
||||||
|
|
||||||
|
## Absturz-Assistent
|
||||||
|
|
||||||
|
Stürzt Minecraft ab – oder scheitert es schon beim Start –, wertet AeroMC Absturzbericht, Spielprotokoll und Konsolenausgabe aus und erklärt unter „Das ist wahrscheinlich passiert“ in einfachen Worten, was los ist:
|
||||||
|
|
||||||
|
- fehlende Abhängigkeiten (Fabric, Quilt, Forge, NeoForge) – mit Knopf zum Nachinstallieren
|
||||||
|
- Mods für eine andere Minecraft-Version, unverträgliche oder doppelte Mods – mit Knopf zum Abschalten
|
||||||
|
- falsche Java-Version – passendes Java wird automatisch gewählt und bei Bedarf geladen
|
||||||
|
- zu wenig oder zu viel Arbeitsspeicher – mit Knopf zum Anpassen
|
||||||
|
- Grafiktreiber-Probleme, OptiFine zusammen mit Sodium
|
||||||
|
|
||||||
|
Nach einer Lösung startet „Erneut starten“ die Instanz direkt. Bleibt die Ursache unklar, schickt **„An M_Viper senden“** Bericht und Mod-Liste an den Entwickler – vorher zeigt AeroMC genau, was gesendet wird. Windows-Benutzername und Anmeldedaten werden entfernt; ohne Häkchen wird anonym gesendet.
|
||||||
|
|
||||||
|
## Konsole, Logs und Crash-Reports
|
||||||
|
|
||||||
|
- Konsole über die obere Leiste oder bei Absturz
|
||||||
|
- Pro Instanz: gespeicherte Logdateien und Crash-Reports einsehen, kopieren und Ordner öffnen
|
||||||
|
|
||||||
|
## Datenort
|
||||||
|
|
||||||
|
- Nutzerdaten (Instanzen, Konten-Verknüpfung, Einstellungen): `%APPDATA%\aeromc`
|
||||||
|
- Aufzeichnungen und Clips: `Videos\AeroMC Launcher`
|
||||||
|
- Backups: `Dokumente\AeroMC Launcher\Backups`
|
||||||
|
- Welt-Sicherungen: `Dokumente\AeroMC Launcher\Welten-Sicherungen`
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- Webseite: https://m-viper.de
|
||||||
|
- Launcher-Webseite: https://aeromc.viper.ipv64.net
|
||||||
|
- Autor: M_Viper
|
||||||
|
|
||||||
|
## Rechtliches
|
||||||
|
|
||||||
|
AeroMC ist kein offizielles Produkt von Mojang oder Microsoft.
|
||||||
|
Minecraft ist eine Marke von Mojang AB.
|
||||||
|
|
||||||
|
### Urheberrecht & Nutzungsbedingungen
|
||||||
|
|
||||||
|
AeroMC ist urheberrechtlich geschützt. © M_Viper – Alle Rechte vorbehalten.
|
||||||
|
|
||||||
|
- ❌ Die Weitergabe dieses Launchers an Dritte ist strengstens untersagt
|
||||||
|
- ❌ Das Verändern, Dekompilieren oder Bearbeiten des Codes ist nicht erlaubt
|
||||||
|
- ❌ Eine Veröffentlichung – auch in veränderter Form – ist verboten
|
||||||
|
- ❌ Der Verkauf oder die kommerzielle Nutzung ist nicht gestattet
|
||||||
|
- ✅ Die Nutzung ist ausschließlich für den privaten, nicht-kommerziellen Gebrauch erlaubt
|
||||||
|
|
||||||
|
Wer gegen diese Bedingungen verstößt, macht sich strafbar und kann rechtlich belangt werden.
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
@@ -0,0 +1,49 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<!-- .NET Framework 4.8 ist in jedem Windows 10/11 enthalten → der Installer läuft ohne Zusatzinstallation -->
|
||||||
|
<TargetFramework>net48</TargetFramework>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
<UseWindowsForms>true</UseWindowsForms>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
|
<Prefer32Bit>false</Prefer32Bit>
|
||||||
|
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||||
|
<AssemblyName>AeroMC-Setup</AssemblyName>
|
||||||
|
<RootNamespace>AeroMCSetup</RootNamespace>
|
||||||
|
<ApplicationIcon>..\src\assets\icon.ico</ApplicationIcon>
|
||||||
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
|
<Product>AeroMC Setup</Product>
|
||||||
|
<AssemblyTitle>AeroMC Setup</AssemblyTitle>
|
||||||
|
<Description>Installiert AeroMC</Description>
|
||||||
|
<Company>M_Viper</Company>
|
||||||
|
<Authors>M_Viper</Authors>
|
||||||
|
<Copyright>Copyright (c) M_Viper. Alle Rechte vorbehalten.</Copyright>
|
||||||
|
<NeutralLanguage>de-DE</NeutralLanguage>
|
||||||
|
<!-- Version kommt beim Bauen aus package.json (scripts/build-setup.js) -->
|
||||||
|
<Version Condition="'$(Version)' == ''">0.0.0</Version>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- WinForms nur für den Ordner-Dialog; nicht global, sonst Namenskonflikte mit WPF -->
|
||||||
|
<Using Remove="System.Windows.Forms" />
|
||||||
|
<Using Remove="System.Drawing" />
|
||||||
|
<Reference Include="Microsoft.VisualBasic" />
|
||||||
|
<Reference Include="System.IO.Compression" />
|
||||||
|
<Reference Include="System.Web" />
|
||||||
|
<Reference Include="System.Web.Extensions" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- Themes\*.xaml (gleiches Design wie der DeskCore-Installer) bindet das SDK automatisch als Page ein -->
|
||||||
|
<Resource Include="..\src\assets\icon.ico" Link="app.ico" />
|
||||||
|
<Resource Include="..\src\assets\logo.png" Link="app.png" />
|
||||||
|
<EmbeddedResource Include="..\LICENSE" LogicalName="LICENSE.txt" />
|
||||||
|
</ItemGroup>
|
||||||
|
<!--
|
||||||
|
Die eigentliche App (entpackter Electron-Ordner als ZIP) wird NICHT hier eingebettet,
|
||||||
|
sondern nach dem Bauen hinten an die .exe angehängt (scripts/build-setup.js, siehe Payload.cs).
|
||||||
|
So muss der Compiler keine ~150 MB als Ressource verarbeiten, und das Deinstallationsprogramm
|
||||||
|
ist einfach der vordere Teil der Datei ohne Anhang.
|
||||||
|
-->
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<Application x:Class="AeroMCSetup.App"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<Application.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<!-- [0] wird je nach Hell/Dunkel ersetzt -->
|
||||||
|
<ResourceDictionary Source="Themes/Colors.Light.xaml"/>
|
||||||
|
<ResourceDictionary Source="Themes/Controls.xaml"/>
|
||||||
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
<!-- AeroMC-Blau (wie Logo und Webseite) -->
|
||||||
|
<SolidColorBrush x:Key="AccentBrush" Color="#2F6FDB"/>
|
||||||
|
<SolidColorBrush x:Key="AccentHoverBrush" Color="#4380E6"/>
|
||||||
|
<SolidColorBrush x:Key="OnAccentBrush" Color="White"/>
|
||||||
|
<SolidColorBrush x:Key="AccentSubtleBrush" Color="#222F6FDB"/>
|
||||||
|
<SolidColorBrush x:Key="AccentSubtleStrokeBrush" Color="#552F6FDB"/>
|
||||||
|
</ResourceDictionary>
|
||||||
|
</Application.Resources>
|
||||||
|
</Application>
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
|
||||||
|
namespace AeroMCSetup;
|
||||||
|
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
private static Mutex? _single;
|
||||||
|
|
||||||
|
protected override void OnStartup(StartupEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnStartup(e);
|
||||||
|
bool has(string arg) => e.Args.Any(a => a.Equals(arg, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
// Das Deinstallationsprogramm ist das Setup ohne angehängte App → ohne Anhang immer Deinstallation
|
||||||
|
bool uninstall = has("--uninstall") || !Payload.Exists;
|
||||||
|
bool silent = has("/S");
|
||||||
|
bool update = has("--update");
|
||||||
|
bool launch = has("/launch");
|
||||||
|
var dirArg = e.Args.FirstOrDefault(a => a.StartsWith("/D=", StringComparison.OrdinalIgnoreCase))?.Substring(3);
|
||||||
|
|
||||||
|
_single = new Mutex(true, "AeroMC-Setup", out bool first);
|
||||||
|
if (!first)
|
||||||
|
{
|
||||||
|
Shutdown(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stille Installation/Deinstallation für Skripte: AeroMC-Setup.exe /S [/D=Ordner] [/launch] bzw. --uninstall /S
|
||||||
|
if (silent)
|
||||||
|
{
|
||||||
|
var progress = new Progress<(double, string)>(_ => { });
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (uninstall) Installer.Uninstall(removeData: false, progress);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var dir = dirArg ?? Installer.InstalledDir ?? Installer.DefaultDir;
|
||||||
|
Installer.Install(dir, desktopLink: Installer.InstalledDir is null || Installer.DesktopLinkExists,
|
||||||
|
progress, waitForExit: true);
|
||||||
|
if (launch) System.Diagnostics.Process.Start(Path.Combine(dir, Installer.ExeName));
|
||||||
|
}
|
||||||
|
Shutdown(0);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// ohne Oberfläche: Fehler für Skripte nachvollziehbar ablegen
|
||||||
|
try { File.WriteAllText(Path.Combine(Path.GetTempPath(), "AeroMC-Setup.log"), $"[{DateTime.Now}] {ex}"); } catch { }
|
||||||
|
Shutdown(1);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyTheme();
|
||||||
|
var mode = uninstall ? SetupMode.Uninstall : update ? SetupMode.Update : SetupMode.Install;
|
||||||
|
new MainWindow(mode).Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnExit(ExitEventArgs e)
|
||||||
|
{
|
||||||
|
try { _single?.ReleaseMutex(); } catch { }
|
||||||
|
base.OnExit(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Hell/Dunkel wie in Windows eingestellt.</summary>
|
||||||
|
private void ApplyTheme()
|
||||||
|
{
|
||||||
|
bool light = ReadDword(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", "AppsUseLightTheme") != 0;
|
||||||
|
Resources.MergedDictionaries[0] = new ResourceDictionary
|
||||||
|
{
|
||||||
|
Source = new Uri($"pack://application:,,,/Themes/Colors.{(light ? "Light" : "Dark")}.xaml"),
|
||||||
|
};
|
||||||
|
if (!light)
|
||||||
|
{
|
||||||
|
// im Dunkeln etwas heller, damit Text und Fortschrittsbalken gut lesbar bleiben
|
||||||
|
var a = Color.FromRgb(0x4A, 0x86, 0xEA);
|
||||||
|
Resources["AccentBrush"] = new SolidColorBrush(a);
|
||||||
|
Resources["AccentHoverBrush"] = new SolidColorBrush(Color.FromRgb(0x5E, 0x94, 0xEE));
|
||||||
|
Resources["AccentSubtleBrush"] = new SolidColorBrush(Color.FromArgb(0x30, a.R, a.G, a.B));
|
||||||
|
Resources["AccentSubtleStrokeBrush"] = new SolidColorBrush(Color.FromArgb(0x60, a.R, a.G, a.B));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ReadDword(string key, string name)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var k = Registry.CurrentUser.OpenSubKey(key);
|
||||||
|
return k?.GetValue(name) is int i ? i : -1;
|
||||||
|
}
|
||||||
|
catch { return -1; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum SetupMode { Install, Update, Uninstall }
|
||||||
@@ -0,0 +1,503 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
|
||||||
|
namespace AeroMCSetup;
|
||||||
|
|
||||||
|
/// <summary>Installation/Deinstallation – nur für den aktuellen Benutzer, ohne Administratorrechte.</summary>
|
||||||
|
public static class Installer
|
||||||
|
{
|
||||||
|
public const string AppName = "AeroMC";
|
||||||
|
public const string Publisher = "M_Viper";
|
||||||
|
public const string ExeName = "AeroMC.exe";
|
||||||
|
// Gleicher Name wie beim früheren NSIS-Setup: der Launcher erkennt daran eine Setup-Installation
|
||||||
|
// und bietet Updates als Setup an (src/updater.js → hasUninstaller).
|
||||||
|
public const string UninstallerName = "Uninstall AeroMC.exe";
|
||||||
|
// wie "appId" in package.json – gleiche Taskleisten-/Benachrichtigungs-Zuordnung wie bisher
|
||||||
|
public const string AppUserModelId = "com.aeromc.launcher";
|
||||||
|
|
||||||
|
// Schlüssel vom alten electron-builder-Setup übernommen → in "Apps" bleibt es ein einziger Eintrag
|
||||||
|
private const string KeyId = "12814222-87ee-5ae1-9e10-8eb2741fedd1";
|
||||||
|
private const string UninstallKey = @"Software\Microsoft\Windows\CurrentVersion\Uninstall\" + KeyId;
|
||||||
|
private const string LegacyAppKey = @"Software\" + KeyId;
|
||||||
|
private const string RunKey = @"Software\Microsoft\Windows\CurrentVersion\Run";
|
||||||
|
private const string FileListName = "install-files.txt";
|
||||||
|
|
||||||
|
public static string Version
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var info = typeof(Installer).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
|
||||||
|
if (!string.IsNullOrEmpty(info)) return info!.Split('+')[0];
|
||||||
|
var v = typeof(Installer).Assembly.GetName().Version!;
|
||||||
|
return $"{v.Major}.{v.Minor}.{v.Build}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Pfad dieses Setups (bzw. des Deinstallationsprogramms).</summary>
|
||||||
|
public static string SelfPath => Assembly.GetExecutingAssembly().Location;
|
||||||
|
|
||||||
|
public static string DefaultDir =>
|
||||||
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", AppName);
|
||||||
|
|
||||||
|
/// <summary>Launcher-Daten: Einstellungen, Konten, Java und (standardmäßig) die Instanzen.</summary>
|
||||||
|
public static string DataDir =>
|
||||||
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "aeromc");
|
||||||
|
|
||||||
|
private static string StartMenuLink =>
|
||||||
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Programs), AppName + ".lnk");
|
||||||
|
|
||||||
|
private static string DesktopLink =>
|
||||||
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), AppName + ".lnk");
|
||||||
|
|
||||||
|
// ---------- vorhandene Installation ----------
|
||||||
|
|
||||||
|
private static string? RegValue(string name)
|
||||||
|
{
|
||||||
|
using var k = Registry.CurrentUser.OpenSubKey(UninstallKey);
|
||||||
|
return k?.GetValue(name) as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Ordner einer vorhandenen Installation (dieses Setups oder des alten NSIS-Setups), oder null.</summary>
|
||||||
|
public static string? InstalledDir
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var dir = RegValue("InstallLocation");
|
||||||
|
if (string.IsNullOrWhiteSpace(dir))
|
||||||
|
{
|
||||||
|
// altes NSIS-Setup trägt keinen InstallLocation ein → aus dem Deinstallationsbefehl ablesen
|
||||||
|
var m = Regex.Match(RegValue("UninstallString") ?? "", "^\"([^\"]+)\"");
|
||||||
|
dir = m.Success ? Path.GetDirectoryName(m.Groups[1].Value) : null;
|
||||||
|
}
|
||||||
|
return dir is not null && File.Exists(Path.Combine(dir, ExeName)) ? dir : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Stammt die vorhandene Installation noch vom alten NSIS-Setup?</summary>
|
||||||
|
public static bool IsLegacyInstall =>
|
||||||
|
InstalledDir is not null && (RegValue("UninstallString") ?? "").IndexOf("--uninstall", StringComparison.OrdinalIgnoreCase) < 0;
|
||||||
|
|
||||||
|
public static string? InstalledVersion => RegValue("DisplayVersion");
|
||||||
|
|
||||||
|
public static bool DesktopLinkExists => File.Exists(DesktopLink);
|
||||||
|
|
||||||
|
public static bool IsRunning(string? dir) => AppProcesses(dir, InstalledDir).Any();
|
||||||
|
|
||||||
|
public static string LicenseText
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
using var s = typeof(Installer).Assembly.GetManifestResourceStream("LICENSE.txt");
|
||||||
|
if (s is null) return "";
|
||||||
|
using var r = new StreamReader(s);
|
||||||
|
return r.ReadToEnd().Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Eigener Instanzen-Ordner außerhalb der Launcher-Daten (falls in den Einstellungen gesetzt), sonst null.</summary>
|
||||||
|
public static string? CustomInstancesDir
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(Path.Combine(DataDir, "settings.json"));
|
||||||
|
var m = Regex.Match(json, "\"instancesDir\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\"");
|
||||||
|
if (!m.Success) return null;
|
||||||
|
var dir = Regex.Unescape(m.Groups[1].Value).Trim();
|
||||||
|
// liegt er doch in den Launcher-Daten (z. B. ausdrücklich der Standardordner), ist er nicht "eigen"
|
||||||
|
return dir.Length > 0 && !IsUnder(dir, DataDir) && !SamePath(dir, DataDir) ? dir : null;
|
||||||
|
}
|
||||||
|
catch { return null; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Installieren ----------
|
||||||
|
|
||||||
|
/// <param name="waitForExit">nach einem Update aus dem Launcher: ihm erst Zeit geben, sich selbst zu beenden</param>
|
||||||
|
public static void Install(string dir, bool desktopLink, IProgress<(double, string)> progress, bool waitForExit = false)
|
||||||
|
{
|
||||||
|
if (!Environment.Is64BitOperatingSystem)
|
||||||
|
throw new InvalidOperationException("AeroMC benötigt ein 64-Bit-Windows (Windows 10 oder 11).");
|
||||||
|
|
||||||
|
var oldDir = InstalledDir;
|
||||||
|
progress.Report((0.02, "AeroMC wird beendet …"));
|
||||||
|
CloseRunningApp(waitForExit ? 20000 : 0, dir, oldDir);
|
||||||
|
|
||||||
|
using var zip = Payload.OpenArchive();
|
||||||
|
var topLevel = TopLevelNames(zip);
|
||||||
|
|
||||||
|
progress.Report((0.05, "Alte Version wird entfernt …"));
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
RemoveAppFiles(dir, topLevel);
|
||||||
|
if (oldDir is not null && !SamePath(oldDir, dir))
|
||||||
|
{
|
||||||
|
// Installation wurde in einen anderen Ordner verlegt → alten Ordner aufräumen
|
||||||
|
var oldExe = Path.Combine(oldDir, ExeName);
|
||||||
|
DeleteIfPointsTo(StartMenuLink, oldExe);
|
||||||
|
DeleteIfPointsTo(DesktopLink, oldExe);
|
||||||
|
RemoveAppFiles(oldDir, topLevel);
|
||||||
|
try { Directory.Delete(oldDir); } catch { } // nur wenn leer
|
||||||
|
}
|
||||||
|
|
||||||
|
Extract(zip, dir, p => progress.Report((0.08 + p * 0.8, "Dateien werden kopiert …")));
|
||||||
|
|
||||||
|
progress.Report((0.9, "Deinstallationsprogramm wird angelegt …"));
|
||||||
|
var exe = Path.Combine(dir, ExeName);
|
||||||
|
var uninstaller = Path.Combine(dir, UninstallerName);
|
||||||
|
if (!SamePath(SelfPath, uninstaller))
|
||||||
|
{
|
||||||
|
MoveAside(uninstaller);
|
||||||
|
CopyStub(uninstaller);
|
||||||
|
}
|
||||||
|
File.WriteAllLines(Path.Combine(dir, FileListName), topLevel.Concat([UninstallerName, FileListName]));
|
||||||
|
|
||||||
|
progress.Report((0.94, "Verknüpfungen werden angelegt …"));
|
||||||
|
Shortcut.Create(StartMenuLink, exe, "Minecraft-Launcher", AppUserModelId);
|
||||||
|
if (desktopLink) Shortcut.Create(DesktopLink, exe, "Minecraft-Launcher", AppUserModelId);
|
||||||
|
else DeleteIfPointsTo(DesktopLink, exe);
|
||||||
|
|
||||||
|
progress.Report((0.97, "Windows wird eingerichtet …"));
|
||||||
|
Registry.CurrentUser.DeleteSubKeyTree(UninstallKey, throwOnMissingSubKey: false); // Reste des alten Setups
|
||||||
|
Registry.CurrentUser.DeleteSubKeyTree(LegacyAppKey, throwOnMissingSubKey: false);
|
||||||
|
using (var k = Registry.CurrentUser.CreateSubKey(UninstallKey))
|
||||||
|
{
|
||||||
|
long size = DirSize(dir) / 1024;
|
||||||
|
k.SetValue("DisplayName", AppName);
|
||||||
|
k.SetValue("DisplayVersion", Version);
|
||||||
|
k.SetValue("Publisher", Publisher);
|
||||||
|
k.SetValue("DisplayIcon", $"{exe},0");
|
||||||
|
k.SetValue("InstallLocation", dir);
|
||||||
|
k.SetValue("UninstallString", $"\"{uninstaller}\" --uninstall");
|
||||||
|
k.SetValue("QuietUninstallString", $"\"{uninstaller}\" --uninstall /S");
|
||||||
|
k.SetValue("URLInfoAbout", "https://m-viper.de");
|
||||||
|
k.SetValue("InstallDate", DateTime.Now.ToString("yyyyMMdd"));
|
||||||
|
k.SetValue("EstimatedSize", (int)Math.Min(int.MaxValue, size), RegistryValueKind.DWord);
|
||||||
|
k.SetValue("NoModify", 1, RegistryValueKind.DWord);
|
||||||
|
k.SetValue("NoRepair", 1, RegistryValueKind.DWord);
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.Report((1.0, "Fertig"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<string> TopLevelNames(ZipArchive zip) =>
|
||||||
|
zip.Entries
|
||||||
|
.Select(e => e.FullName.Replace('\\', '/').Split('/')[0])
|
||||||
|
.Where(n => n.Length > 0)
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
private static void Extract(ZipArchive zip, string dir, Action<double> report)
|
||||||
|
{
|
||||||
|
var root = Path.GetFullPath(dir).TrimEnd('\\') + "\\";
|
||||||
|
long total = Math.Max(1, zip.Entries.Sum(e => e.Length)), done = 0;
|
||||||
|
var buffer = new byte[1 << 20];
|
||||||
|
int lastPercent = -1;
|
||||||
|
|
||||||
|
foreach (var entry in zip.Entries)
|
||||||
|
{
|
||||||
|
var target = Path.GetFullPath(Path.Combine(dir, entry.FullName));
|
||||||
|
if (!target.StartsWith(root, StringComparison.OrdinalIgnoreCase)) continue; // nie außerhalb des Ordners schreiben
|
||||||
|
if (entry.Name.Length == 0)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(target);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(target)!);
|
||||||
|
long before = done;
|
||||||
|
Retry(target, () =>
|
||||||
|
{
|
||||||
|
done = before;
|
||||||
|
MoveAside(target);
|
||||||
|
using var src = entry.Open();
|
||||||
|
using var dst = File.Create(target);
|
||||||
|
int n;
|
||||||
|
while ((n = src.Read(buffer, 0, buffer.Length)) > 0)
|
||||||
|
{
|
||||||
|
dst.Write(buffer, 0, n);
|
||||||
|
done += n;
|
||||||
|
int percent = (int)(done * 100 / total);
|
||||||
|
if (percent != lastPercent)
|
||||||
|
{
|
||||||
|
lastPercent = percent;
|
||||||
|
report(Math.Min(1.0, (double)done / total));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
report(1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Setup ohne angehängte App als Deinstallationsprogramm ablegen (nur ein paar hundert KB).</summary>
|
||||||
|
private static void CopyStub(string path)
|
||||||
|
{
|
||||||
|
long length = Payload.StubLength;
|
||||||
|
using var src = File.OpenRead(SelfPath);
|
||||||
|
using var dst = File.Create(path);
|
||||||
|
var buffer = new byte[81920];
|
||||||
|
long left = length;
|
||||||
|
while (left > 0)
|
||||||
|
{
|
||||||
|
int n = src.Read(buffer, 0, (int)Math.Min(buffer.Length, left));
|
||||||
|
if (n <= 0) break;
|
||||||
|
dst.Write(buffer, 0, n);
|
||||||
|
left -= n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Deinstallieren ----------
|
||||||
|
|
||||||
|
public static void Uninstall(bool removeData, IProgress<(double, string)> progress)
|
||||||
|
{
|
||||||
|
// Nur im eingetragenen Installationsordner löschen – nie dort, wo das Setup zufällig liegt (z. B. Downloads)
|
||||||
|
var dir = InstalledDir;
|
||||||
|
var exe = dir is null ? "" : Path.Combine(dir, ExeName);
|
||||||
|
|
||||||
|
progress.Report((0.1, "AeroMC wird beendet …"));
|
||||||
|
CloseRunningApp(0, dir);
|
||||||
|
|
||||||
|
progress.Report((0.3, "Verknüpfungen werden entfernt …"));
|
||||||
|
DeleteIfPointsTo(StartMenuLink, exe);
|
||||||
|
DeleteIfPointsTo(DesktopLink, exe);
|
||||||
|
|
||||||
|
progress.Report((0.45, "Windows-Einträge werden entfernt …"));
|
||||||
|
RemoveAutostart(exe);
|
||||||
|
Registry.CurrentUser.DeleteSubKeyTree(UninstallKey, throwOnMissingSubKey: false);
|
||||||
|
Registry.CurrentUser.DeleteSubKeyTree(LegacyAppKey, throwOnMissingSubKey: false);
|
||||||
|
// Name/Logo für Windows-Benachrichtigungen (legt der Launcher selbst an)
|
||||||
|
Registry.CurrentUser.DeleteSubKeyTree(@"Software\Classes\AppUserModelId\" + AppUserModelId, throwOnMissingSubKey: false);
|
||||||
|
|
||||||
|
if (removeData && Directory.Exists(DataDir))
|
||||||
|
{
|
||||||
|
progress.Report((0.6, "Instanzen, Konten und Einstellungen werden in den Papierkorb gelegt …"));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Microsoft.VisualBasic.FileIO.FileSystem.DeleteDirectory(DataDir,
|
||||||
|
Microsoft.VisualBasic.FileIO.UIOption.OnlyErrorDialogs,
|
||||||
|
Microsoft.VisualBasic.FileIO.RecycleOption.SendToRecycleBin);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.Report((0.8, "Dateien werden entfernt …"));
|
||||||
|
if (dir is null) { progress.Report((1.0, "Fertig")); return; }
|
||||||
|
RemoveAppFiles(dir, null);
|
||||||
|
|
||||||
|
// Das laufende Deinstallationsprogramm kann sich nicht selbst löschen → kurz nach dem Beenden per cmd.
|
||||||
|
// rmdir ohne /s entfernt den Ordner nur, wenn er danach leer ist.
|
||||||
|
var self = SelfPath;
|
||||||
|
if (IsUnder(self, dir))
|
||||||
|
{
|
||||||
|
Process.Start(new ProcessStartInfo("cmd.exe",
|
||||||
|
$"/c ping 127.0.0.1 -n 3 > nul & del /f /q \"{self}\" & del /f /q \"{Path.Combine(dir, "*.old")}\" & rmdir \"{dir}\"")
|
||||||
|
{
|
||||||
|
CreateNoWindow = true, UseShellExecute = false, WindowStyle = ProcessWindowStyle.Hidden,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TryDelete(Path.Combine(dir, UninstallerName));
|
||||||
|
try { Directory.Delete(dir); } catch { } // nur wenn leer
|
||||||
|
}
|
||||||
|
progress.Report((1.0, "Fertig"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Autostart-Einträge entfernen, die der Launcher selbst gesetzt hat (Name vergibt Electron).</summary>
|
||||||
|
private static void RemoveAutostart(string exe)
|
||||||
|
{
|
||||||
|
if (exe.Length == 0) return;
|
||||||
|
using var run = Registry.CurrentUser.OpenSubKey(RunKey, writable: true);
|
||||||
|
if (run is null) return;
|
||||||
|
foreach (var name in run.GetValueNames())
|
||||||
|
{
|
||||||
|
if (run.GetValue(name) is string cmd && cmd.IndexOf(exe, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||||
|
run.DeleteValue(name, throwOnMissingValue: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Dateien ----------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Entfernt die App-Dateien aus einem Installationsordner. Gelöscht wird nur, was nachweislich zu AeroMC gehört:
|
||||||
|
/// Einträge aus der Dateiliste der letzten Installation bzw. die Namen aus dem neuen Paket –
|
||||||
|
/// Ordner nur, wenn sie eindeutig von Electron stammen. Fremde Dateien im Ordner bleiben unangetastet.
|
||||||
|
/// </summary>
|
||||||
|
private static void RemoveAppFiles(string dir, IEnumerable<string>? extraNames)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(dir)) return;
|
||||||
|
CleanupOldFiles(dir);
|
||||||
|
var listFile = Path.Combine(dir, FileListName);
|
||||||
|
var fromList = File.Exists(listFile) ? File.ReadAllLines(listFile).Where(l => l.Trim().Length > 0).ToList() : [];
|
||||||
|
var names = fromList.Concat(extraNames ?? []).Concat([UninstallerName, FileListName])
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var name in names)
|
||||||
|
{
|
||||||
|
if (name.IndexOfAny(['\\', '/', ':']) >= 0 || name is "." or "..") continue;
|
||||||
|
var path = Path.Combine(dir, name);
|
||||||
|
if (SamePath(path, SelfPath)) continue; // das laufende Deinstallationsprogramm räumt cmd weg
|
||||||
|
|
||||||
|
if (Directory.Exists(path))
|
||||||
|
{
|
||||||
|
bool ours = fromList.Contains(name, StringComparer.OrdinalIgnoreCase)
|
||||||
|
|| (name.Equals("resources", StringComparison.OrdinalIgnoreCase) && File.Exists(Path.Combine(path, "app.asar")))
|
||||||
|
|| (name.Equals("locales", StringComparison.OrdinalIgnoreCase) && File.Exists(Path.Combine(path, "en-US.pak")));
|
||||||
|
if (ours) Retry(path, () => Directory.Delete(path, recursive: true));
|
||||||
|
}
|
||||||
|
else if (File.Exists(path))
|
||||||
|
{
|
||||||
|
Retry(path, () => MoveAside(path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Längster Pfad im Paket. Windows erlaubt (ohne Sonderfreigabe) nur 259 Zeichen pro Pfad –
|
||||||
|
/// der Installationsordner darf deshalb höchstens <see cref="MaxDirLength"/> Zeichen lang sein.
|
||||||
|
/// </summary>
|
||||||
|
private static int? _longestEntry;
|
||||||
|
public static int MaxDirLength
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_longestEntry is null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var zip = Payload.OpenArchive();
|
||||||
|
_longestEntry = zip.Entries.Count == 0 ? 0 : zip.Entries.Max(e => e.FullName.Length);
|
||||||
|
}
|
||||||
|
catch { _longestEntry = 0; }
|
||||||
|
}
|
||||||
|
return 259 - 1 - _longestEntry.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Retry(string path, Action action)
|
||||||
|
{
|
||||||
|
for (int attempt = 1; ; attempt++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
action();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (PathTooLongException ex)
|
||||||
|
{
|
||||||
|
throw new IOException($"Der Installationsordner ist zu lang. Bitte einen kürzeren Ordner wählen (höchstens {MaxDirLength} Zeichen).", ex);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when ((ex is IOException || ex is UnauthorizedAccessException) && attempt < 10)
|
||||||
|
{
|
||||||
|
Thread.Sleep(500);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
throw new IOException($"„{Path.GetFileName(path)}“ wird noch verwendet. Bitte AeroMC beenden " +
|
||||||
|
"(auch das Symbol im Infobereich neben der Uhr) und erneut versuchen.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Vorhandene Datei löschen oder – falls sie noch geöffnet ist (Explorer, Virenscanner) – beiseite umbenennen.
|
||||||
|
/// Umbenennen klappt unter Windows auch bei geöffneten Dateien, überschreiben nicht.
|
||||||
|
/// </summary>
|
||||||
|
private static void MoveAside(string path)
|
||||||
|
{
|
||||||
|
if (!File.Exists(path)) return;
|
||||||
|
try { File.Delete(path); return; } catch { }
|
||||||
|
File.Move(path, $"{path}.{Guid.NewGuid():N}.old");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CleanupOldFiles(string dir)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(dir)) return;
|
||||||
|
foreach (var f in Directory.GetFiles(dir, "*.old", SearchOption.AllDirectories))
|
||||||
|
try { File.Delete(f); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryDelete(string path)
|
||||||
|
{
|
||||||
|
try { if (File.Exists(path)) File.Delete(path); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DeleteIfPointsTo(string link, string target)
|
||||||
|
{
|
||||||
|
if (!File.Exists(link) || target.Length == 0) return;
|
||||||
|
if (!SamePath(Shortcut.Target(link) ?? "", target)) return;
|
||||||
|
// Explorer/Indexdienst halten Verknüpfungen manchmal kurz fest → mehrmals versuchen
|
||||||
|
for (int i = 0; i < 10 && File.Exists(link); i++)
|
||||||
|
{
|
||||||
|
try { File.Delete(link); }
|
||||||
|
catch { Thread.Sleep(300); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Prozesse ----------
|
||||||
|
|
||||||
|
private static IEnumerable<Process> AppProcesses(params string?[] dirs)
|
||||||
|
{
|
||||||
|
var roots = dirs.Where(d => !string.IsNullOrEmpty(d)).Select(d => d!).ToList();
|
||||||
|
if (roots.Count == 0) yield break;
|
||||||
|
// Launcher (mehrere Electron-Prozesse) und ein evtl. laufendes ffmpeg der Aufzeichnung
|
||||||
|
foreach (var p in Process.GetProcessesByName("AeroMC").Concat(Process.GetProcessesByName("ffmpeg")))
|
||||||
|
{
|
||||||
|
string? file = null;
|
||||||
|
try { file = p.MainModule?.FileName; } catch { }
|
||||||
|
if (file is not null && roots.Any(r => IsUnder(file, r))) yield return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CloseRunningApp(int waitMs, params string?[] dirs)
|
||||||
|
{
|
||||||
|
var until = DateTime.Now.AddMilliseconds(waitMs);
|
||||||
|
while (waitMs > 0 && DateTime.Now < until && AppProcesses(dirs).Any())
|
||||||
|
Thread.Sleep(300);
|
||||||
|
|
||||||
|
foreach (var p in AppProcesses(dirs).ToList())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
p.Kill();
|
||||||
|
p.WaitForExit(10000);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Helfer ----------
|
||||||
|
|
||||||
|
private static bool SamePath(string a, string b)
|
||||||
|
{
|
||||||
|
try { return string.Equals(Path.GetFullPath(a).TrimEnd('\\'), Path.GetFullPath(b).TrimEnd('\\'), StringComparison.OrdinalIgnoreCase); }
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsUnder(string path, string dir) =>
|
||||||
|
Path.GetFullPath(path).StartsWith(Path.GetFullPath(dir).TrimEnd('\\') + "\\", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private static long DirSize(string dir)
|
||||||
|
{
|
||||||
|
long sum = 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var f in Directory.EnumerateFiles(dir, "*", SearchOption.AllDirectories))
|
||||||
|
try { sum += new FileInfo(f).Length; } catch { }
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long DataSize() => Directory.Exists(DataDir) ? DirSize(DataDir) : 0;
|
||||||
|
|
||||||
|
public static string FormatSize(long bytes) => bytes switch
|
||||||
|
{
|
||||||
|
< 1024 * 1024 => $"{Math.Max(1, bytes / 1024)} KB",
|
||||||
|
< 1024L * 1024 * 1024 => $"{bytes / 1024.0 / 1024:0.#} MB",
|
||||||
|
_ => $"{bytes / 1024.0 / 1024 / 1024:0.##} GB",
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Web.Script.Serialization;
|
||||||
|
|
||||||
|
namespace AeroMCSetup;
|
||||||
|
|
||||||
|
/// <summary>Ein gefundener anderer Launcher, dessen Instanzen AeroMC übernehmen kann.</summary>
|
||||||
|
/// <param name="kind">Kennung wie im Import-Dialog des Launchers: mc, cf, mmc, technic</param>
|
||||||
|
public sealed class FoundLauncher(string kind, string name, string path, int count, string unit)
|
||||||
|
{
|
||||||
|
public string Kind { get; } = kind;
|
||||||
|
public string Name { get; } = name;
|
||||||
|
public string Path { get; } = path;
|
||||||
|
public int Count { get; } = count;
|
||||||
|
public string Unit { get; } = unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sucht andere Launcher an ihren üblichen Orten – nur lesend, es wird nichts verändert.
|
||||||
|
/// Übernommen wird später im Launcher selbst (gleicher Import wie unter „Importieren“):
|
||||||
|
/// das Setup legt dafür nur pending-import.json ab, AeroMC öffnet beim ersten Start
|
||||||
|
/// den Import-Dialog damit fertig durchsucht.
|
||||||
|
/// </summary>
|
||||||
|
public static class LauncherImport
|
||||||
|
{
|
||||||
|
private static string AppData => Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||||
|
private static string Home => Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||||
|
|
||||||
|
private static string PendingFile => System.IO.Path.Combine(Installer.DataDir, "pending-import.json");
|
||||||
|
|
||||||
|
public static List<FoundLauncher> Detect()
|
||||||
|
{
|
||||||
|
var found = new List<FoundLauncher>();
|
||||||
|
void Add(Func<FoundLauncher?> probe)
|
||||||
|
{
|
||||||
|
try { if (probe() is { Count: > 0 } f) found.Add(f); } catch { /* unlesbar → einfach nicht anbieten */ }
|
||||||
|
}
|
||||||
|
Add(DetectMinecraftLauncher);
|
||||||
|
Add(DetectCurseForge);
|
||||||
|
Add(() => DetectMultiMc("Prism Launcher", System.IO.Path.Combine(AppData, "PrismLauncher")));
|
||||||
|
Add(() => DetectMultiMc("PolyMC", System.IO.Path.Combine(AppData, "PolyMC")));
|
||||||
|
// MultiMC (und portable Prism/PolyMC) liegen irgendwo – über ihre Verknüpfungen finden
|
||||||
|
foreach (var (name, root) in PortableMultiMcFromShortcuts())
|
||||||
|
{
|
||||||
|
if (found.Any(f => f.Kind == "mmc" && SamePath(f.Path, root))) continue;
|
||||||
|
Add(() => DetectMultiMc(name, root));
|
||||||
|
}
|
||||||
|
Add(DetectTechnic);
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- offizieller Minecraft Launcher ----------
|
||||||
|
|
||||||
|
private static readonly string[] ProfileFiles =
|
||||||
|
["launcher_profiles.json", "launcher_profiles_microsoft_store.json", "launcher_profiles_microsoft_store_2.json"];
|
||||||
|
|
||||||
|
private static FoundLauncher? DetectMinecraftLauncher()
|
||||||
|
{
|
||||||
|
var root = System.IO.Path.Combine(AppData, ".minecraft");
|
||||||
|
var file = ProfileFiles.Select(f => System.IO.Path.Combine(root, f)).FirstOrDefault(File.Exists);
|
||||||
|
if (file is null) return null;
|
||||||
|
|
||||||
|
var json = new JavaScriptSerializer { MaxJsonLength = int.MaxValue }
|
||||||
|
.Deserialize<Dictionary<string, object>>(File.ReadAllText(file, Encoding.UTF8).TrimStart(''));
|
||||||
|
if (json?.TryGetValue("profiles", out var p) != true || p is not Dictionary<string, object> profiles) return null;
|
||||||
|
|
||||||
|
// wie isImportableProfile in src/mcimport.js: die automatischen "Neueste Version"-Profile zählen nicht
|
||||||
|
int count = profiles.Values.OfType<Dictionary<string, object>>().Count(profile =>
|
||||||
|
{
|
||||||
|
var type = (Get(profile, "type") ?? "").Trim().ToLowerInvariant();
|
||||||
|
if (type is "latest-release" or "latest-snapshot") return false;
|
||||||
|
return !string.IsNullOrWhiteSpace(Get(profile, "name")) || !string.IsNullOrWhiteSpace(Get(profile, "lastVersionId"));
|
||||||
|
});
|
||||||
|
return new FoundLauncher("mc", "Minecraft Launcher", root, count, count == 1 ? "Profil" : "Profile");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? Get(Dictionary<string, object> d, string key) => d.TryGetValue(key, out var v) ? v as string : null;
|
||||||
|
|
||||||
|
// ---------- CurseForge ----------
|
||||||
|
|
||||||
|
private static FoundLauncher? DetectCurseForge()
|
||||||
|
{
|
||||||
|
var dir = new[]
|
||||||
|
{
|
||||||
|
System.IO.Path.Combine(Home, "curseforge", "minecraft", "Instances"),
|
||||||
|
System.IO.Path.Combine(Home, "CurseForge", "minecraft", "Instances"),
|
||||||
|
}.FirstOrDefault(Directory.Exists);
|
||||||
|
if (dir is null) return null;
|
||||||
|
|
||||||
|
string[] markers = ["minecraftinstance.json", "instance.json", "mods", "config", "resourcepacks", "shaderpacks", "saves", "options.txt"];
|
||||||
|
int count = Directory.GetDirectories(dir).Count(d => markers.Any(m => Exists(d, m)));
|
||||||
|
return new FoundLauncher("cf", "CurseForge", dir, count, count == 1 ? "Instanz" : "Instanzen");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Prism Launcher / PolyMC (MultiMC-Format) ----------
|
||||||
|
|
||||||
|
private static FoundLauncher? DetectMultiMc(string name, string root)
|
||||||
|
{
|
||||||
|
var instances = System.IO.Path.Combine(root, "instances");
|
||||||
|
if (!Directory.Exists(instances)) return null;
|
||||||
|
int count = Directory.GetDirectories(instances).Count(d => File.Exists(System.IO.Path.Combine(d, "instance.cfg")));
|
||||||
|
return new FoundLauncher("mmc", name, root, count, count == 1 ? "Instanz" : "Instanzen");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly Dictionary<string, string> MultiMcExes = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["MultiMC.exe"] = "MultiMC",
|
||||||
|
["prismlauncher.exe"] = "Prism Launcher",
|
||||||
|
["polymc.exe"] = "PolyMC",
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>Ordner von MultiMC-artigen Launchern, auf die eine Verknüpfung (Desktop/Startmenü) zeigt.</summary>
|
||||||
|
private static IEnumerable<(string Name, string Root)> PortableMultiMcFromShortcuts()
|
||||||
|
{
|
||||||
|
var places = new[]
|
||||||
|
{
|
||||||
|
(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), SearchOption.TopDirectoryOnly),
|
||||||
|
(Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory), SearchOption.TopDirectoryOnly),
|
||||||
|
(Environment.GetFolderPath(Environment.SpecialFolder.Programs), SearchOption.AllDirectories),
|
||||||
|
(Environment.GetFolderPath(Environment.SpecialFolder.CommonPrograms), SearchOption.AllDirectories),
|
||||||
|
};
|
||||||
|
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var (dir, option) in places)
|
||||||
|
{
|
||||||
|
string[] links;
|
||||||
|
try { links = string.IsNullOrEmpty(dir) || !Directory.Exists(dir) ? [] : Directory.GetFiles(dir, "*.lnk", option); }
|
||||||
|
catch { continue; }
|
||||||
|
foreach (var link in links)
|
||||||
|
{
|
||||||
|
var target = Shortcut.Target(link);
|
||||||
|
if (string.IsNullOrEmpty(target) || !MultiMcExes.TryGetValue(System.IO.Path.GetFileName(target), out var name)) continue;
|
||||||
|
var root = System.IO.Path.GetDirectoryName(target);
|
||||||
|
if (root is null || !Directory.Exists(System.IO.Path.Combine(root, "instances")) || !seen.Add(root)) continue;
|
||||||
|
yield return (name, root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool SamePath(string a, string b) =>
|
||||||
|
string.Equals(System.IO.Path.GetFullPath(a).TrimEnd('\\'), System.IO.Path.GetFullPath(b).TrimEnd('\\'), StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
// ---------- Technic ----------
|
||||||
|
|
||||||
|
private static FoundLauncher? DetectTechnic()
|
||||||
|
{
|
||||||
|
var modpacks = System.IO.Path.Combine(AppData, ".technic", "modpacks");
|
||||||
|
if (!Directory.Exists(modpacks)) return null;
|
||||||
|
string[] markers = ["bin", "mods", "config", "saves", "resourcepacks", "options.txt"];
|
||||||
|
int count = Directory.GetDirectories(modpacks).Count(d => markers.Any(m => Exists(d, m)));
|
||||||
|
return new FoundLauncher("technic", "Technic Launcher", modpacks, count, count == 1 ? "Modpack" : "Modpacks");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool Exists(string dir, string name)
|
||||||
|
{
|
||||||
|
var p = System.IO.Path.Combine(dir, name);
|
||||||
|
return File.Exists(p) || Directory.Exists(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Übergabe an den Launcher ----------
|
||||||
|
|
||||||
|
/// <summary>Merkt die ausgewählten Launcher für den ersten Start von AeroMC vor (oder räumt eine alte Vormerkung weg).</summary>
|
||||||
|
public static void WritePending(IReadOnlyCollection<FoundLauncher> selected)
|
||||||
|
{
|
||||||
|
if (selected.Count == 0)
|
||||||
|
{
|
||||||
|
try { if (File.Exists(PendingFile)) File.Delete(PendingFile); } catch { }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var data = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["sources"] = selected.Select(s => new Dictionary<string, string> { ["kind"] = s.Kind, ["path"] = s.Path }).ToList(),
|
||||||
|
};
|
||||||
|
Directory.CreateDirectory(Installer.DataDir);
|
||||||
|
File.WriteAllText(PendingFile, new JavaScriptSerializer().Serialize(data), new UTF8Encoding(false));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,392 @@
|
|||||||
|
<Window x:Class="AeroMCSetup.MainWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="AeroMC Setup" Icon="app.ico" Width="780" Height="520" ResizeMode="CanMinimize"
|
||||||
|
Background="{DynamicResource LayerBrush}" Foreground="{DynamicResource TextBrush}"
|
||||||
|
FontFamily="{DynamicResource UiFont}" FontSize="14"
|
||||||
|
TextOptions.TextFormattingMode="Display" UseLayoutRounding="True"
|
||||||
|
WindowStartupLocation="CenterScreen">
|
||||||
|
|
||||||
|
<WindowChrome.WindowChrome>
|
||||||
|
<WindowChrome CaptionHeight="36" ResizeBorderThickness="0" GlassFrameThickness="0,0,0,1"
|
||||||
|
CornerRadius="0" UseAeroCaptionButtons="False"/>
|
||||||
|
</WindowChrome.WindowChrome>
|
||||||
|
|
||||||
|
<Window.Resources>
|
||||||
|
<!-- Markenfarben der Seitenleiste (in Hell und Dunkel gleich) -->
|
||||||
|
<SolidColorBrush x:Key="SideText" Color="#FFFFFF"/>
|
||||||
|
<SolidColorBrush x:Key="SideTextSecondary" Color="#B9C8E8"/>
|
||||||
|
<SolidColorBrush x:Key="SideTextMuted" Color="#7F95C2"/>
|
||||||
|
<LinearGradientBrush x:Key="BrandGradient" StartPoint="0,0" EndPoint="1,0">
|
||||||
|
<GradientStop Color="#2F6FDB" Offset="0"/>
|
||||||
|
<GradientStop Color="#4FB8F0" Offset="1"/>
|
||||||
|
</LinearGradientBrush>
|
||||||
|
|
||||||
|
<Style x:Key="PageTitle" TargetType="TextBlock">
|
||||||
|
<Setter Property="FontFamily" Value="{DynamicResource DisplayFont}"/>
|
||||||
|
<Setter Property="FontSize" Value="26"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="Margin" Value="0,0,0,8"/>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="Body" TargetType="TextBlock">
|
||||||
|
<Setter Property="TextWrapping" Value="Wrap"/>
|
||||||
|
<Setter Property="LineHeight" Value="21"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="StepDot" TargetType="Border">
|
||||||
|
<Setter Property="Width" Value="26"/>
|
||||||
|
<Setter Property="Height" Value="26"/>
|
||||||
|
<Setter Property="CornerRadius" Value="13"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1.5"/>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="StepNum" TargetType="TextBlock">
|
||||||
|
<Setter Property="FontSize" Value="12"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Center"/>
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="StepLabel" TargetType="TextBlock">
|
||||||
|
<Setter Property="Margin" Value="12,0,0,0"/>
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||||
|
<Setter Property="FontSize" Value="13.5"/>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="StepLine" TargetType="Border">
|
||||||
|
<Setter Property="Width" Value="2"/>
|
||||||
|
<Setter Property="Height" Value="18"/>
|
||||||
|
<Setter Property="Margin" Value="12,3,0,3"/>
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Left"/>
|
||||||
|
<Setter Property="CornerRadius" Value="1"/>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="FeatureIcon" TargetType="Border">
|
||||||
|
<Setter Property="Width" Value="38"/>
|
||||||
|
<Setter Property="Height" Value="38"/>
|
||||||
|
<Setter Property="CornerRadius" Value="10"/>
|
||||||
|
<Setter Property="Background" Value="{DynamicResource AccentSubtleBrush}"/>
|
||||||
|
<Setter Property="VerticalAlignment" Value="Top"/>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="FeatureGlyph" TargetType="TextBlock">
|
||||||
|
<Setter Property="FontFamily" Value="{DynamicResource IconFont}"/>
|
||||||
|
<Setter Property="FontSize" Value="17"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Center"/>
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="FeatureTitle" TargetType="TextBlock">
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="FontSize" Value="13.5"/>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="FeatureText" TargetType="TextBlock">
|
||||||
|
<Setter Property="FontSize" Value="12"/>
|
||||||
|
<Setter Property="TextWrapping" Value="Wrap"/>
|
||||||
|
<Setter Property="Margin" Value="0,2,0,0"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="Card" TargetType="Border">
|
||||||
|
<Setter Property="Padding" Value="16,14"/>
|
||||||
|
<Setter Property="CornerRadius" Value="8"/>
|
||||||
|
<Setter Property="Background" Value="{DynamicResource ControlFillBrush}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ControlStrokeBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
</Style>
|
||||||
|
</Window.Resources>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="36"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="250"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- ===== Seitenleiste im AeroMC-Look ===== -->
|
||||||
|
<Border Grid.RowSpan="3" ClipToBounds="True">
|
||||||
|
<Border.Background>
|
||||||
|
<LinearGradientBrush StartPoint="0,0" EndPoint="0.6,1">
|
||||||
|
<GradientStop Color="#0C1630" Offset="0"/>
|
||||||
|
<GradientStop Color="#112452" Offset="0.55"/>
|
||||||
|
<GradientStop Color="#173A86" Offset="1"/>
|
||||||
|
</LinearGradientBrush>
|
||||||
|
</Border.Background>
|
||||||
|
<Grid>
|
||||||
|
<!-- Lichtschein hinter dem Logo und unten -->
|
||||||
|
<Ellipse Width="320" Height="320" Margin="-120,-110,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" IsHitTestVisible="False">
|
||||||
|
<Ellipse.Fill>
|
||||||
|
<RadialGradientBrush>
|
||||||
|
<GradientStop Color="#664A86EA" Offset="0"/>
|
||||||
|
<GradientStop Color="#004A86EA" Offset="1"/>
|
||||||
|
</RadialGradientBrush>
|
||||||
|
</Ellipse.Fill>
|
||||||
|
</Ellipse>
|
||||||
|
<Ellipse Width="300" Height="300" Margin="0,0,-150,-150" HorizontalAlignment="Right" VerticalAlignment="Bottom" IsHitTestVisible="False">
|
||||||
|
<Ellipse.Fill>
|
||||||
|
<RadialGradientBrush>
|
||||||
|
<GradientStop Color="#444FB8F0" Offset="0"/>
|
||||||
|
<GradientStop Color="#004FB8F0" Offset="1"/>
|
||||||
|
</RadialGradientBrush>
|
||||||
|
</Ellipse.Fill>
|
||||||
|
</Ellipse>
|
||||||
|
|
||||||
|
<StackPanel Margin="30,64,24,0">
|
||||||
|
<Image Source="app.png" Width="68" Height="68" HorizontalAlignment="Left" RenderOptions.BitmapScalingMode="HighQuality">
|
||||||
|
<Image.Effect>
|
||||||
|
<DropShadowEffect Color="#4A86EA" BlurRadius="28" ShadowDepth="0" Opacity="0.75"/>
|
||||||
|
</Image.Effect>
|
||||||
|
</Image>
|
||||||
|
<TextBlock Text="AeroMC" FontFamily="{DynamicResource DisplayFont}" FontSize="26" FontWeight="SemiBold"
|
||||||
|
Foreground="{StaticResource SideText}" Margin="0,16,0,0"/>
|
||||||
|
<TextBlock x:Name="VersionText" Foreground="{StaticResource SideTextSecondary}" FontSize="12" Margin="0,2,0,0"/>
|
||||||
|
|
||||||
|
<StackPanel x:Name="Steps" Margin="0,38,0,0">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Border x:Name="Dot0" Style="{StaticResource StepDot}"><TextBlock x:Name="Num0" Text="1" Style="{StaticResource StepNum}"/></Border>
|
||||||
|
<TextBlock x:Name="Label0" Text="Willkommen" Style="{StaticResource StepLabel}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<Border x:Name="Line0" Style="{StaticResource StepLine}"/>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Border x:Name="Dot1" Style="{StaticResource StepDot}"><TextBlock x:Name="Num1" Text="2" Style="{StaticResource StepNum}"/></Border>
|
||||||
|
<TextBlock x:Name="Label1" Text="Lizenz" Style="{StaticResource StepLabel}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<Border x:Name="Line1" Style="{StaticResource StepLine}"/>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Border x:Name="Dot2" Style="{StaticResource StepDot}"><TextBlock x:Name="Num2" Text="3" Style="{StaticResource StepNum}"/></Border>
|
||||||
|
<TextBlock x:Name="Label2" Text="Optionen" Style="{StaticResource StepLabel}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<Border x:Name="Line2" Style="{StaticResource StepLine}"/>
|
||||||
|
<!-- Import: nur sichtbar, wenn andere Launcher gefunden wurden -->
|
||||||
|
<StackPanel x:Name="ImportStepRow" Orientation="Horizontal">
|
||||||
|
<Border x:Name="Dot3" Style="{StaticResource StepDot}"><TextBlock x:Name="Num3" Text="4" Style="{StaticResource StepNum}"/></Border>
|
||||||
|
<TextBlock x:Name="Label3" Text="Import" Style="{StaticResource StepLabel}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<Border x:Name="Line3" Style="{StaticResource StepLine}"/>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Border x:Name="Dot4" Style="{StaticResource StepDot}"><TextBlock x:Name="Num4" Text="5" Style="{StaticResource StepNum}"/></Border>
|
||||||
|
<TextBlock x:Name="Label4" Text="Installation" Style="{StaticResource StepLabel}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Text="© M_Viper · m-viper.de" FontSize="11" Foreground="{StaticResource SideTextMuted}"
|
||||||
|
VerticalAlignment="Bottom" Margin="30,0,0,20"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Titelleiste -->
|
||||||
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="14,0,0,0">
|
||||||
|
<Image Source="app.ico" Width="16" Height="16" RenderOptions.BitmapScalingMode="HighQuality"/>
|
||||||
|
<TextBlock x:Name="WindowTitle" Text="AeroMC Setup" FontSize="12" Margin="10,0,0,0" VerticalAlignment="Center"
|
||||||
|
Foreground="{StaticResource SideTextSecondary}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Top">
|
||||||
|
<Button Style="{StaticResource CaptionButton}" Content="" Click="Minimize_Click"
|
||||||
|
WindowChrome.IsHitTestVisibleInChrome="True"/>
|
||||||
|
<Button x:Name="CloseButton" Style="{StaticResource CloseCaptionButton}" Content="" Click="Cancel_Click"
|
||||||
|
WindowChrome.IsHitTestVisibleInChrome="True"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- ===== Inhalt ===== -->
|
||||||
|
<Grid Grid.Row="1" Grid.Column="1" Margin="40,18,40,0">
|
||||||
|
|
||||||
|
<!-- 0: Willkommen -->
|
||||||
|
<StackPanel x:Name="PageWelcome">
|
||||||
|
<TextBlock Text="Willkommen bei AeroMC" Style="{StaticResource PageTitle}"/>
|
||||||
|
<TextBlock Style="{StaticResource Body}"
|
||||||
|
Text="Der Assistent richtet AeroMC in wenigen Schritten ein – nur für dein Benutzerkonto und ohne Administratorrechte."/>
|
||||||
|
|
||||||
|
<Grid Margin="0,24,0,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="20"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="18"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<DockPanel>
|
||||||
|
<Border Style="{StaticResource FeatureIcon}" DockPanel.Dock="Left"><TextBlock Text="" Style="{StaticResource FeatureGlyph}"/></Border>
|
||||||
|
<StackPanel Margin="12,0,0,0">
|
||||||
|
<TextBlock Text="Getrennte Instanzen" Style="{StaticResource FeatureTitle}"/>
|
||||||
|
<TextBlock Text="Mods und Modpacks von Modrinth und CurseForge" Style="{StaticResource FeatureText}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DockPanel>
|
||||||
|
<DockPanel Grid.Column="2">
|
||||||
|
<Border Style="{StaticResource FeatureIcon}" DockPanel.Dock="Left"><TextBlock Text="" Style="{StaticResource FeatureGlyph}"/></Border>
|
||||||
|
<StackPanel Margin="12,0,0,0">
|
||||||
|
<TextBlock Text="Spielaufzeichnung" Style="{StaticResource FeatureTitle}"/>
|
||||||
|
<TextBlock Text="Als MP4, ganz ohne Zusatzprogramm" Style="{StaticResource FeatureText}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DockPanel>
|
||||||
|
<DockPanel Grid.Row="2">
|
||||||
|
<Border Style="{StaticResource FeatureIcon}" DockPanel.Dock="Left"><TextBlock Text="" Style="{StaticResource FeatureGlyph}"/></Border>
|
||||||
|
<StackPanel Margin="12,0,0,0">
|
||||||
|
<TextBlock Text="Freunde" Style="{StaticResource FeatureTitle}"/>
|
||||||
|
<TextBlock Text="Online-Status und Meldungen im Spiel" Style="{StaticResource FeatureText}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DockPanel>
|
||||||
|
<DockPanel Grid.Row="2" Grid.Column="2">
|
||||||
|
<Border Style="{StaticResource FeatureIcon}" DockPanel.Dock="Left"><TextBlock Text="" Style="{StaticResource FeatureGlyph}"/></Border>
|
||||||
|
<StackPanel Margin="12,0,0,0">
|
||||||
|
<TextBlock Text="Microsoft-Konten" Style="{StaticResource FeatureTitle}"/>
|
||||||
|
<TextBlock Text="Mehrere Konten, Skins direkt ändern" Style="{StaticResource FeatureText}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DockPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Border x:Name="UpdateHint" Margin="0,26,0,0" Padding="14,10" CornerRadius="8" Visibility="Collapsed"
|
||||||
|
Background="{DynamicResource AccentSubtleBrush}" BorderBrush="{DynamicResource AccentSubtleStrokeBrush}" BorderThickness="1">
|
||||||
|
<DockPanel>
|
||||||
|
<TextBlock DockPanel.Dock="Left" Text="" FontFamily="{DynamicResource IconFont}" FontSize="16"
|
||||||
|
Foreground="{DynamicResource AccentBrush}" Margin="0,2,12,0" VerticalAlignment="Top"/>
|
||||||
|
<TextBlock x:Name="UpdateHintText" TextWrapping="Wrap" LineHeight="20" FontSize="13"/>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
<Border x:Name="RunningHint" Margin="0,10,0,0" Padding="14,10" CornerRadius="8" Visibility="Collapsed"
|
||||||
|
Background="{DynamicResource ControlFillBrush}" BorderBrush="{DynamicResource ControlStrokeBrush}" BorderThickness="1">
|
||||||
|
<DockPanel>
|
||||||
|
<TextBlock DockPanel.Dock="Left" Text="" FontFamily="{DynamicResource IconFont}" FontSize="16"
|
||||||
|
Foreground="{DynamicResource AccentBrush}" Margin="0,2,12,0" VerticalAlignment="Top"/>
|
||||||
|
<TextBlock TextWrapping="Wrap" LineHeight="20" FontSize="13"
|
||||||
|
Text="AeroMC läuft gerade und wird für die Installation automatisch beendet."/>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 1: Lizenz -->
|
||||||
|
<DockPanel x:Name="PageLicense" Visibility="Collapsed">
|
||||||
|
<TextBlock DockPanel.Dock="Top" Text="Lizenz" Style="{StaticResource PageTitle}"/>
|
||||||
|
<TextBlock DockPanel.Dock="Top" Style="{StaticResource Body}" Margin="0,0,0,14"
|
||||||
|
Text="Bitte lies die Nutzungsbedingungen. AeroMC ist kostenlos für den privaten Gebrauch."/>
|
||||||
|
<CheckBox x:Name="AcceptLicense" DockPanel.Dock="Bottom" Margin="0,14,0,0"
|
||||||
|
Checked="AcceptLicense_Click" Unchecked="AcceptLicense_Click"
|
||||||
|
Content="Ich akzeptiere die Lizenz- und Nutzungsbedingungen"/>
|
||||||
|
<Border Background="{DynamicResource ControlFillBrush}" BorderBrush="{DynamicResource ControlStrokeBrush}"
|
||||||
|
BorderThickness="1" CornerRadius="8">
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
|
<!-- wird aus LICENSE aufgebaut: Überschriften, Absätze, Aufzählungen (siehe ZeigeLizenz) -->
|
||||||
|
<StackPanel x:Name="LicensePanel" Margin="16,12,16,14"/>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Border>
|
||||||
|
</DockPanel>
|
||||||
|
|
||||||
|
<!-- 2: Optionen -->
|
||||||
|
<StackPanel x:Name="PageOptions" Visibility="Collapsed">
|
||||||
|
<TextBlock Text="Optionen" Style="{StaticResource PageTitle}"/>
|
||||||
|
<TextBlock Style="{StaticResource Body}" Text="Wohin soll AeroMC installiert werden?"/>
|
||||||
|
|
||||||
|
<Border Style="{StaticResource Card}" Margin="0,18,0,0">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="Installationsordner" FontWeight="SemiBold" FontSize="13" Margin="0,0,0,10"/>
|
||||||
|
<DockPanel>
|
||||||
|
<Button DockPanel.Dock="Right" Content="Ändern …" Margin="8,0,0,0" Click="ChangeDir_Click"/>
|
||||||
|
<TextBox x:Name="DirBox"/>
|
||||||
|
</DockPanel>
|
||||||
|
<TextBlock Style="{StaticResource Body}" FontSize="12" LineHeight="18" Margin="0,10,0,0"
|
||||||
|
Text="Benötigt ca. 400 MB. Deine Instanzen, Welten und Einstellungen liegen getrennt davon und bleiben bei Updates erhalten."/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Style="{StaticResource Card}" Margin="0,12,0,0">
|
||||||
|
<StackPanel>
|
||||||
|
<CheckBox x:Name="DesktopLinkBox" Content="Verknüpfung auf dem Desktop" IsChecked="True"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 3: Import aus anderen Launchern (optional) -->
|
||||||
|
<StackPanel x:Name="PageImport" Visibility="Collapsed">
|
||||||
|
<TextBlock Text="Import" Style="{StaticResource PageTitle}"/>
|
||||||
|
<TextBlock Style="{StaticResource Body}"
|
||||||
|
Text="Optional: Diese Launcher wurden auf deinem PC gefunden. Was du ankreuzt, bietet AeroMC beim ersten Start zum Übernehmen an – dort wählst du die einzelnen Instanzen aus."/>
|
||||||
|
<Border Style="{StaticResource Card}" Margin="0,18,0,0" Padding="16,6">
|
||||||
|
<StackPanel x:Name="ImportList"/>
|
||||||
|
</Border>
|
||||||
|
<TextBlock Style="{StaticResource Body}" FontSize="12" LineHeight="18" Margin="0,12,0,0"
|
||||||
|
Text="In den anderen Launchern wird nichts verändert – AeroMC legt Kopien an. Später geht das jederzeit über „Importieren“ in der Toolleiste."/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 4: Fortschritt / Fertig -->
|
||||||
|
<StackPanel x:Name="PageProgress" Visibility="Collapsed">
|
||||||
|
<Border x:Name="SuccessIcon" Width="64" Height="64" CornerRadius="32" HorizontalAlignment="Left" Margin="0,4,0,20"
|
||||||
|
Background="{StaticResource BrandGradient}" RenderTransformOrigin="0.5,0.5" Visibility="Collapsed">
|
||||||
|
<Border.RenderTransform><ScaleTransform x:Name="SuccessScale" ScaleX="1" ScaleY="1"/></Border.RenderTransform>
|
||||||
|
<Border.Effect><DropShadowEffect Color="#4A86EA" BlurRadius="24" ShadowDepth="0" Opacity="0.55"/></Border.Effect>
|
||||||
|
<TextBlock Text="" FontFamily="{DynamicResource IconFont}" FontSize="26" Foreground="White"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<TextBlock x:Name="ProgressTitle" Text="AeroMC wird installiert …" Style="{StaticResource PageTitle}"/>
|
||||||
|
|
||||||
|
<StackPanel x:Name="ProgressBarPanel" Margin="0,18,0,0">
|
||||||
|
<TextBlock x:Name="PercentText" Text="0 %" FontFamily="{DynamicResource DisplayFont}" FontSize="44" FontWeight="SemiBold"
|
||||||
|
Foreground="{DynamicResource AccentBrush}"/>
|
||||||
|
<Border x:Name="ProgressTrack" SizeChanged="ProgressTrack_SizeChanged" Height="8" CornerRadius="4" Margin="0,10,0,0" Background="{DynamicResource SubtleSelectedBrush}">
|
||||||
|
<Border x:Name="ProgressFill" HorizontalAlignment="Left" Width="0" CornerRadius="4" Background="{StaticResource BrandGradient}"/>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock x:Name="ProgressText" Style="{StaticResource Body}" Margin="0,12,0,0" Text="Einen Moment bitte."/>
|
||||||
|
|
||||||
|
<StackPanel x:Name="DonePanel" Visibility="Collapsed">
|
||||||
|
<CheckBox x:Name="LaunchBox" Content="AeroMC jetzt starten" IsChecked="True" Margin="0,24,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Deinstallation -->
|
||||||
|
<StackPanel x:Name="PageUninstall" Visibility="Collapsed">
|
||||||
|
<TextBlock Text="AeroMC deinstallieren" Style="{StaticResource PageTitle}"/>
|
||||||
|
<TextBlock Style="{StaticResource Body}"
|
||||||
|
Text="AeroMC wird von deinem PC entfernt: Programm, Verknüpfungen, Autostart und der Eintrag in den Windows-Einstellungen."/>
|
||||||
|
<Border Style="{StaticResource Card}" Margin="0,22,0,0">
|
||||||
|
<StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="" FontFamily="{DynamicResource IconFont}" FontSize="15" Foreground="{DynamicResource AccentBrush}"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="Deine Instanzen und Welten bleiben erhalten" FontWeight="SemiBold" Margin="10,0,0,0" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock x:Name="DataInfo" Style="{StaticResource Body}" FontSize="12.5" Margin="0,6,0,14"/>
|
||||||
|
<CheckBox x:Name="RemoveDataBox" Content="Auch Instanzen, Welten, Konten und Einstellungen löschen"
|
||||||
|
Checked="RemoveDataBox_Click" Unchecked="RemoveDataBox_Click"/>
|
||||||
|
<TextBlock x:Name="RemoveDataWarning" Visibility="Collapsed" Margin="30,8,0,0" TextWrapping="Wrap" FontSize="12.5"
|
||||||
|
Foreground="{DynamicResource DangerBrush}"
|
||||||
|
Text="Achtung: Damit wandern alle Welten und Spielstände aus AeroMC in den Papierkorb. Aufnahmen (Videos) und Backups (Dokumente) bleiben erhalten."/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Fußleiste -->
|
||||||
|
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,14,40,24">
|
||||||
|
<Button x:Name="BackButton" Content="Zurück" Width="110" Margin="0,0,8,0" Click="Back_Click"/>
|
||||||
|
<Button x:Name="NextButton" Content="Weiter" Width="140" Margin="0,0,8,0" Style="{StaticResource AccentButton}" IsDefault="True" Click="Next_Click"/>
|
||||||
|
<Button x:Name="CancelButton" Content="Abbrechen" Width="110" Click="Cancel_Click"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Eigene Rückfrage im Setup-Look statt Windows-Meldungsfenster (siehe Frage()) -->
|
||||||
|
<Grid x:Name="DialogOverlay" Grid.RowSpan="3" Grid.ColumnSpan="2" Visibility="Collapsed" Background="#99000000">
|
||||||
|
<Border x:Name="DialogCard" Width="420" HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
|
Background="{DynamicResource LayerBrush}" BorderBrush="{DynamicResource ControlStrokeBrush}" BorderThickness="1"
|
||||||
|
CornerRadius="10" Padding="26,22,22,18" Effect="{StaticResource DialogShadow}" RenderTransformOrigin="0.5,0.5">
|
||||||
|
<Border.RenderTransform><ScaleTransform x:Name="DialogScale" ScaleX="1" ScaleY="1"/></Border.RenderTransform>
|
||||||
|
<StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Border Width="34" Height="34" CornerRadius="17" Background="{DynamicResource AccentSubtleBrush}" VerticalAlignment="Top">
|
||||||
|
<TextBlock x:Name="DialogIcon" Text="" FontFamily="{DynamicResource IconFont}" FontSize="16"
|
||||||
|
Foreground="{DynamicResource AccentBrush}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<TextBlock x:Name="DialogTitle" FontFamily="{DynamicResource DisplayFont}" FontSize="18" FontWeight="SemiBold"
|
||||||
|
VerticalAlignment="Center" Margin="12,0,0,0" TextWrapping="Wrap" MaxWidth="320"/>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock x:Name="DialogText" TextWrapping="Wrap" LineHeight="20" Margin="46,8,0,0"
|
||||||
|
Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,22,0,0">
|
||||||
|
<Button x:Name="DialogSecondary" MinWidth="120" Margin="0,0,8,0" Click="DialogSecondary_Click"/>
|
||||||
|
<Button x:Name="DialogPrimary" MinWidth="130" Style="{StaticResource AccentButton}" Click="DialogPrimary_Click"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,611 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Animation;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
|
||||||
|
namespace AeroMCSetup;
|
||||||
|
|
||||||
|
public partial class MainWindow : Window
|
||||||
|
{
|
||||||
|
private readonly SetupMode _mode;
|
||||||
|
private int _page;
|
||||||
|
private bool _busy;
|
||||||
|
private bool _done;
|
||||||
|
private string? _installedExe;
|
||||||
|
// andere Launcher für den optionalen Import (leer → Import-Schritt entfällt)
|
||||||
|
private List<FoundLauncher> _found = [];
|
||||||
|
private bool HasImport => _found.Count > 0;
|
||||||
|
|
||||||
|
private const int PageNumImport = 3;
|
||||||
|
private const int PageNumProgress = 4;
|
||||||
|
|
||||||
|
public MainWindow(SetupMode mode)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_mode = mode;
|
||||||
|
VersionText.Text = $"Version {Installer.Version}";
|
||||||
|
ZeigeLizenz(Installer.LicenseText);
|
||||||
|
|
||||||
|
var installed = Installer.InstalledDir;
|
||||||
|
DirBox.Text = installed ?? Installer.DefaultDir;
|
||||||
|
DesktopLinkBox.IsChecked = installed is null || Installer.DesktopLinkExists;
|
||||||
|
|
||||||
|
if (installed is not null)
|
||||||
|
{
|
||||||
|
UpdateHint.Visibility = Visibility.Visible;
|
||||||
|
UpdateHintText.Text = Installer.InstalledVersion == Installer.Version
|
||||||
|
? $"AeroMC {Installer.Version} ist bereits installiert und wird neu installiert. Deine Instanzen, Welten und Einstellungen bleiben erhalten."
|
||||||
|
: $"Die vorhandene Installation{(Installer.InstalledVersion is { } v ? $" (Version {v})" : "")} wird durch Version {Installer.Version} ersetzt. Deine Instanzen, Welten und Einstellungen bleiben erhalten.";
|
||||||
|
}
|
||||||
|
if (Installer.IsRunning(installed)) RunningHint.Visibility = Visibility.Visible;
|
||||||
|
|
||||||
|
switch (mode)
|
||||||
|
{
|
||||||
|
case SetupMode.Uninstall:
|
||||||
|
Title = WindowTitle.Text = "AeroMC deinstallieren";
|
||||||
|
Steps.Visibility = Visibility.Collapsed;
|
||||||
|
ShowOnly(PageUninstall);
|
||||||
|
NextButton.Content = "Deinstallieren";
|
||||||
|
BackButton.Visibility = Visibility.Collapsed;
|
||||||
|
LoadDataInfo();
|
||||||
|
break;
|
||||||
|
case SetupMode.Update:
|
||||||
|
// aus dem Launcher heraus: ohne Rückfragen direkt aktualisieren und danach wieder starten
|
||||||
|
Title = WindowTitle.Text = "AeroMC wird aktualisiert";
|
||||||
|
Steps.Visibility = Visibility.Collapsed;
|
||||||
|
Loaded += async (_, _) => await RunInstall(DirBox.Text, DesktopLinkBox.IsChecked == true, update: true);
|
||||||
|
ShowOnly(PageProgress);
|
||||||
|
ProgressTitle.Text = "AeroMC wird aktualisiert …";
|
||||||
|
BackButton.Visibility = Visibility.Hidden;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
SetupImportPage();
|
||||||
|
ShowPage(0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Lizenztext ----------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// LICENSE ist für die Textansicht auf ~80 Zeichen umbrochen und eingerückt. Hier wird das
|
||||||
|
/// wieder zu fließendem Text zusammengesetzt, damit der Umbruch zur Fensterbreite passt:
|
||||||
|
/// Leerzeilen trennen Abschnitte, "1. …" sind Überschriften, "- …" Aufzählungspunkte
|
||||||
|
/// (Folgezeilen gehören zum Punkt), alles andere ist Absatztext.
|
||||||
|
/// </summary>
|
||||||
|
private void ZeigeLizenz(string text)
|
||||||
|
{
|
||||||
|
LicensePanel.Children.Clear();
|
||||||
|
var abschnitte = new List<List<string>>();
|
||||||
|
var aktuell = new List<string>();
|
||||||
|
foreach (var zeile in text.Replace("\r", "").Split('\n'))
|
||||||
|
{
|
||||||
|
if (zeile.Trim().Length == 0)
|
||||||
|
{
|
||||||
|
if (aktuell.Count > 0) { abschnitte.Add(aktuell); aktuell = []; }
|
||||||
|
}
|
||||||
|
else aktuell.Add(zeile);
|
||||||
|
}
|
||||||
|
if (aktuell.Count > 0) abschnitte.Add(aktuell);
|
||||||
|
|
||||||
|
for (int a = 0; a < abschnitte.Count; a++)
|
||||||
|
{
|
||||||
|
var abschnitt = abschnitte[a];
|
||||||
|
int i = 0;
|
||||||
|
if (a == 0)
|
||||||
|
{
|
||||||
|
LicensePanel.Children.Add(LizenzText(abschnitt[0].Trim(), 14, FontWeights.SemiBold, "TextBrush", 0));
|
||||||
|
i = 1;
|
||||||
|
}
|
||||||
|
else if (System.Text.RegularExpressions.Regex.IsMatch(abschnitt[0], @"^\s*\d+\.\s"))
|
||||||
|
{
|
||||||
|
LicensePanel.Children.Add(LizenzText(abschnitt[0].Trim(), 13, FontWeights.SemiBold, "TextBrush", 14));
|
||||||
|
i = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// nach einer Überschrift eng anschließen, sonst Abstand zum vorigen Abschnitt
|
||||||
|
double absatzAbstand = i == 1 && a > 0 ? 4 : 10;
|
||||||
|
var absatz = new System.Text.StringBuilder();
|
||||||
|
string? punkt = null;
|
||||||
|
void AbsatzFertig()
|
||||||
|
{
|
||||||
|
if (absatz.Length == 0) return;
|
||||||
|
LicensePanel.Children.Add(LizenzText(absatz.ToString(), 12.5, FontWeights.Normal, "TextSecondaryBrush", absatzAbstand));
|
||||||
|
absatz.Clear();
|
||||||
|
}
|
||||||
|
void PunktFertig()
|
||||||
|
{
|
||||||
|
if (punkt is null) return;
|
||||||
|
LicensePanel.Children.Add(LizenzPunkt(punkt));
|
||||||
|
punkt = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (; i < abschnitt.Count; i++)
|
||||||
|
{
|
||||||
|
var t = abschnitt[i].Trim();
|
||||||
|
if (t.StartsWith("- "))
|
||||||
|
{
|
||||||
|
AbsatzFertig();
|
||||||
|
PunktFertig();
|
||||||
|
punkt = t.Substring(2).Trim();
|
||||||
|
}
|
||||||
|
else if (punkt is not null) punkt += " " + t;
|
||||||
|
else absatz.Append(absatz.Length > 0 ? " " : "").Append(t);
|
||||||
|
}
|
||||||
|
AbsatzFertig();
|
||||||
|
PunktFertig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextBlock LizenzText(string text, double groesse, FontWeight gewicht, string farbe, double abstandOben)
|
||||||
|
{
|
||||||
|
var tb = new TextBlock
|
||||||
|
{
|
||||||
|
Text = text,
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
FontSize = groesse,
|
||||||
|
FontWeight = gewicht,
|
||||||
|
LineHeight = groesse + 6,
|
||||||
|
Margin = new Thickness(0, abstandOben, 0, 0),
|
||||||
|
};
|
||||||
|
tb.SetResourceReference(TextBlock.ForegroundProperty, farbe);
|
||||||
|
return tb;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aufzählungspunkt mit hängendem Einzug (umbrochene Zeilen beginnen unter dem Text, nicht unter dem Punkt)
|
||||||
|
private Grid LizenzPunkt(string text)
|
||||||
|
{
|
||||||
|
var grid = new Grid { Margin = new Thickness(4, 4, 0, 0) };
|
||||||
|
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(16) });
|
||||||
|
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||||
|
var punkt = LizenzText("•", 12.5, FontWeights.Normal, "AccentBrush", 0);
|
||||||
|
var inhalt = LizenzText(text, 12.5, FontWeights.Normal, "TextSecondaryBrush", 0);
|
||||||
|
Grid.SetColumn(inhalt, 1);
|
||||||
|
grid.Children.Add(punkt);
|
||||||
|
grid.Children.Add(inhalt);
|
||||||
|
return grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Seiten ----------
|
||||||
|
|
||||||
|
private UIElement? _shownPage;
|
||||||
|
|
||||||
|
/// <summary>Seite wechseln – mit kurzem Einblenden von rechts.</summary>
|
||||||
|
private void ShowOnly(UIElement page)
|
||||||
|
{
|
||||||
|
foreach (var p in new UIElement[] { PageWelcome, PageLicense, PageOptions, PageImport, PageProgress, PageUninstall })
|
||||||
|
p.Visibility = p == page ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
if (page == _shownPage) return;
|
||||||
|
_shownPage = page;
|
||||||
|
|
||||||
|
var ease = new CubicEase { EasingMode = EasingMode.EaseOut };
|
||||||
|
var slide = new TranslateTransform(18, 0);
|
||||||
|
page.RenderTransform = slide;
|
||||||
|
slide.BeginAnimation(TranslateTransform.XProperty, new DoubleAnimation(18, 0, TimeSpan.FromMilliseconds(260)) { EasingFunction = ease });
|
||||||
|
page.BeginAnimation(OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(220)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Farben der Schritt-Anzeige in der (immer dunkelblauen) Seitenleiste
|
||||||
|
private static readonly Brush StepDoneFill = Frozen(Color.FromRgb(0x4A, 0x86, 0xEA));
|
||||||
|
private static readonly Brush StepCurrentFill = Brushes.White;
|
||||||
|
private static readonly Brush StepCurrentText = Frozen(Color.FromRgb(0x1C, 0x4E, 0xB8));
|
||||||
|
private static readonly Brush StepUpcoming = Frozen(Color.FromRgb(0x7F, 0x95, 0xC2));
|
||||||
|
private static readonly Brush StepUpcomingStroke = Frozen(Color.FromRgb(0x46, 0x5E, 0x92));
|
||||||
|
private static readonly Brush StepDoneText = Frozen(Color.FromRgb(0xB9, 0xC8, 0xE8));
|
||||||
|
private static readonly Brush StepLineIdle = Frozen(Color.FromRgb(0x2E, 0x45, 0x78));
|
||||||
|
|
||||||
|
private static Brush Frozen(Color c)
|
||||||
|
{
|
||||||
|
var b = new SolidColorBrush(c);
|
||||||
|
b.Freeze();
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateSteps(int page)
|
||||||
|
{
|
||||||
|
var dots = new[] { Dot0, Dot1, Dot2, Dot3, Dot4 };
|
||||||
|
var nums = new[] { Num0, Num1, Num2, Num3, Num4 };
|
||||||
|
var labels = new[] { Label0, Label1, Label2, Label3, Label4 };
|
||||||
|
var lines = new[] { Line0, Line1, Line2, Line3 };
|
||||||
|
var iconFont = (FontFamily)FindResource("IconFont");
|
||||||
|
var uiFont = (FontFamily)FindResource("UiFont");
|
||||||
|
|
||||||
|
// ohne gefundene Launcher fällt der Import-Schritt weg → Nummern rücken auf
|
||||||
|
ImportStepRow.Visibility = Line3.Visibility = HasImport ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
int number = 0;
|
||||||
|
for (int i = 0; i < dots.Length; i++)
|
||||||
|
{
|
||||||
|
if (i == PageNumImport && !HasImport) continue;
|
||||||
|
number++;
|
||||||
|
bool done = i < page || (_done && i == page);
|
||||||
|
bool current = i == page && !done;
|
||||||
|
dots[i].Background = done ? StepDoneFill : current ? StepCurrentFill : Brushes.Transparent;
|
||||||
|
dots[i].BorderBrush = done ? StepDoneFill : current ? StepCurrentFill : StepUpcomingStroke;
|
||||||
|
nums[i].Text = done ? "" : number.ToString();
|
||||||
|
nums[i].FontFamily = done ? iconFont : uiFont;
|
||||||
|
nums[i].FontSize = done ? 11 : 12;
|
||||||
|
nums[i].Foreground = done ? Brushes.White : current ? StepCurrentText : StepUpcoming;
|
||||||
|
labels[i].Foreground = current ? Brushes.White : done ? StepDoneText : StepUpcoming;
|
||||||
|
labels[i].FontWeight = current ? FontWeights.SemiBold : FontWeights.Normal;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < lines.Length; i++)
|
||||||
|
lines[i].Background = i < page ? StepDoneFill : StepLineIdle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowPage(int page)
|
||||||
|
{
|
||||||
|
_page = page;
|
||||||
|
ShowOnly(page switch { 0 => PageWelcome, 1 => PageLicense, 2 => PageOptions, PageNumImport => PageImport, _ => PageProgress });
|
||||||
|
UpdateSteps(page);
|
||||||
|
|
||||||
|
BackButton.Visibility = page is 1 or 2 or PageNumImport ? Visibility.Visible : Visibility.Hidden;
|
||||||
|
NextButton.Content = page switch
|
||||||
|
{
|
||||||
|
2 => HasImport ? "Weiter" : "Installieren",
|
||||||
|
PageNumImport => "Installieren",
|
||||||
|
PageNumProgress => "Fertigstellen",
|
||||||
|
_ => "Weiter",
|
||||||
|
};
|
||||||
|
NextButton.IsEnabled = page != 1 || AcceptLicense.IsChecked == true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AcceptLicense_Click(object sender, RoutedEventArgs e) => NextButton.IsEnabled = AcceptLicense.IsChecked == true;
|
||||||
|
|
||||||
|
private void Back_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (!_busy && _page > 0) ShowPage(_page - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Next_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_busy) return;
|
||||||
|
|
||||||
|
if (_mode == SetupMode.Uninstall)
|
||||||
|
{
|
||||||
|
if (_done) { Close(); return; }
|
||||||
|
await RunUninstall();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_mode == SetupMode.Update)
|
||||||
|
{
|
||||||
|
Close(); // nur nach einem Fehler erreichbar
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (_page)
|
||||||
|
{
|
||||||
|
case 0: ShowPage(1); break;
|
||||||
|
case 1 when AcceptLicense.IsChecked == true: ShowPage(2); break;
|
||||||
|
case 2:
|
||||||
|
var dir = DirBox.Text.Trim();
|
||||||
|
if (!IsFullPath(dir) || dir.IndexOfAny(Path.GetInvalidPathChars()) >= 0)
|
||||||
|
{
|
||||||
|
await Frage("Ungültiger Ordner", "Bitte einen vollständigen Ordner angeben, z. B. C:\\Spiele\\AeroMC.", "OK");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (dir.TrimEnd('\\', '/').Length > Installer.MaxDirLength)
|
||||||
|
{
|
||||||
|
await Frage("Ordnerpfad zu lang",
|
||||||
|
$"Der Pfad hat {dir.TrimEnd('\\', '/').Length} Zeichen, Windows erlaubt hier höchstens {Installer.MaxDirLength}. " +
|
||||||
|
"Bitte einen kürzeren Ordner wählen, z. B. C:\\Spiele\\AeroMC.", "OK");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (HasImport) ShowPage(PageNumImport);
|
||||||
|
else await RunInstall(dir, DesktopLinkBox.IsChecked == true, update: false);
|
||||||
|
break;
|
||||||
|
case PageNumImport:
|
||||||
|
await RunInstall(DirBox.Text.Trim(), DesktopLinkBox.IsChecked == true, update: false);
|
||||||
|
break;
|
||||||
|
case PageNumProgress:
|
||||||
|
if (LaunchBox.IsChecked == true) StartApp();
|
||||||
|
Close();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Import aus anderen Launchern ----------
|
||||||
|
|
||||||
|
private void SetupImportPage()
|
||||||
|
{
|
||||||
|
try { _found = LauncherImport.Detect(); } catch { _found = []; }
|
||||||
|
ImportList.Children.Clear();
|
||||||
|
for (int i = 0; i < _found.Count; i++)
|
||||||
|
{
|
||||||
|
var f = _found[i];
|
||||||
|
var text = new StackPanel { Margin = new Thickness(4, 0, 0, 0) };
|
||||||
|
text.Children.Add(new TextBlock { Text = f.Name, FontWeight = FontWeights.SemiBold });
|
||||||
|
var detail = new TextBlock
|
||||||
|
{
|
||||||
|
Text = $"{f.Count} {f.Unit} gefunden · {f.Path}",
|
||||||
|
FontSize = 12,
|
||||||
|
Margin = new Thickness(0, 2, 0, 0),
|
||||||
|
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||||
|
ToolTip = f.Path,
|
||||||
|
};
|
||||||
|
detail.SetResourceReference(TextBlock.ForegroundProperty, "TextSecondaryBrush");
|
||||||
|
text.Children.Add(detail);
|
||||||
|
|
||||||
|
var box = new CheckBox { Content = text, Tag = f, Margin = new Thickness(0, 10, 0, 10), IsChecked = false };
|
||||||
|
if (i > 0)
|
||||||
|
{
|
||||||
|
var line = new Border { Height = 1 };
|
||||||
|
line.SetResourceReference(Border.BackgroundProperty, "ControlStrokeBrush");
|
||||||
|
ImportList.Children.Add(line);
|
||||||
|
}
|
||||||
|
ImportList.Children.Add(box);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<FoundLauncher> SelectedImports() =>
|
||||||
|
ImportList.Children.OfType<CheckBox>()
|
||||||
|
.Where(c => c.IsChecked == true && c.Tag is FoundLauncher)
|
||||||
|
.Select(c => (FoundLauncher)c.Tag)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
private void ChangeDir_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
using var dlg = new System.Windows.Forms.FolderBrowserDialog
|
||||||
|
{
|
||||||
|
Description = "Installationsordner wählen",
|
||||||
|
SelectedPath = Directory.Exists(DirBox.Text) ? DirBox.Text : Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||||
|
ShowNewFolderButton = true,
|
||||||
|
};
|
||||||
|
if (dlg.ShowDialog() != System.Windows.Forms.DialogResult.OK) return;
|
||||||
|
var chosen = dlg.SelectedPath;
|
||||||
|
// wie bei üblichen Installern: Unterordner "AeroMC" anhängen
|
||||||
|
DirBox.Text = Path.GetFileName(chosen.TrimEnd('\\')).Equals("AeroMC", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? chosen : Path.Combine(chosen, "AeroMC");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Installation ----------
|
||||||
|
|
||||||
|
private async Task RunInstall(string dir, bool desktop, bool update)
|
||||||
|
{
|
||||||
|
_busy = true;
|
||||||
|
if (!update) ShowPage(PageNumProgress);
|
||||||
|
SetButtons(false);
|
||||||
|
var progress = new Progress<(double Value, string Text)>(p => SetProgress(p.Value, p.Text));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Run(() => Installer.Install(dir, desktop, progress, waitForExit: update));
|
||||||
|
_installedExe = Path.Combine(dir, Installer.ExeName);
|
||||||
|
_done = true;
|
||||||
|
|
||||||
|
if (update)
|
||||||
|
{
|
||||||
|
ShowSuccess("Update abgeschlossen", $"AeroMC {Installer.Version} wird gestartet …");
|
||||||
|
StartApp();
|
||||||
|
_busy = false;
|
||||||
|
var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1.5) };
|
||||||
|
timer.Tick += (_, _) => { timer.Stop(); Close(); };
|
||||||
|
timer.Start();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ausgewählte Launcher für den ersten Start vormerken (Fehler hier sollen die Installation nicht kippen)
|
||||||
|
var selected = SelectedImports();
|
||||||
|
try { LauncherImport.WritePending(selected); } catch { selected = []; }
|
||||||
|
|
||||||
|
ShowSuccess("Alles bereit!", "AeroMC ist installiert. Du findest es im Startmenü" + (desktop ? " und auf dem Desktop." : "."));
|
||||||
|
if (selected.Count > 0)
|
||||||
|
ProgressText.Text += $" Beim ersten Start bietet AeroMC dir die Instanzen aus {string.Join(" und ", selected.Select(f => f.Name))} zum Übernehmen an.";
|
||||||
|
DonePanel.Visibility = Visibility.Visible;
|
||||||
|
ShowPage(PageNumProgress);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ProgressTitle.Text = update ? "Update fehlgeschlagen" : "Installation fehlgeschlagen";
|
||||||
|
ProgressBarPanel.Visibility = Visibility.Collapsed;
|
||||||
|
ProgressText.Text = ex.Message;
|
||||||
|
ProgressText.Foreground = (Brush)FindResource("DangerBrush");
|
||||||
|
NextButton.Content = "Schließen";
|
||||||
|
_installedExe = null;
|
||||||
|
}
|
||||||
|
_busy = false;
|
||||||
|
NextButton.IsEnabled = true;
|
||||||
|
CloseButton.IsEnabled = true;
|
||||||
|
BackButton.Visibility = Visibility.Hidden;
|
||||||
|
CancelButton.Visibility = Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Fortschritt ausblenden und den Haken mit kurzem "Aufploppen" zeigen.</summary>
|
||||||
|
private void ShowSuccess(string title, string text)
|
||||||
|
{
|
||||||
|
ProgressBarPanel.Visibility = Visibility.Collapsed;
|
||||||
|
ProgressTitle.Text = title;
|
||||||
|
ProgressText.Text = text;
|
||||||
|
SuccessIcon.Visibility = Visibility.Visible;
|
||||||
|
var pop = new DoubleAnimation(0.4, 1, TimeSpan.FromMilliseconds(420)) { EasingFunction = new BackEase { EasingMode = EasingMode.EaseOut, Amplitude = 0.5 } };
|
||||||
|
SuccessScale.BeginAnimation(ScaleTransform.ScaleXProperty, pop);
|
||||||
|
SuccessScale.BeginAnimation(ScaleTransform.ScaleYProperty, pop);
|
||||||
|
SuccessIcon.BeginAnimation(OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(200)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartApp()
|
||||||
|
{
|
||||||
|
if (_installedExe is null || !File.Exists(_installedExe)) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Process.Start(new ProcessStartInfo(_installedExe)
|
||||||
|
{
|
||||||
|
UseShellExecute = true,
|
||||||
|
WorkingDirectory = Path.GetDirectoryName(_installedExe)!,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Deinstallation ----------
|
||||||
|
|
||||||
|
private async void LoadDataInfo()
|
||||||
|
{
|
||||||
|
var root = Installer.DataDir;
|
||||||
|
var custom = Installer.CustomInstancesDir;
|
||||||
|
var customNote = custom is null ? "" : $" Instanzen in deinem eigenen Ordner {custom} werden nie angefasst.";
|
||||||
|
if (!Directory.Exists(root))
|
||||||
|
{
|
||||||
|
DataInfo.Text = "Es wurden keine Launcher-Daten gefunden." + customNote;
|
||||||
|
RemoveDataBox.IsEnabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DataInfo.Text = $"Sie liegen in {root}. Größe wird berechnet …";
|
||||||
|
var size = await Task.Run(Installer.DataSize);
|
||||||
|
DataInfo.Text = $"Sie liegen in {root} ({Installer.FormatSize(size)}) und stehen wieder bereit, falls du AeroMC später neu installierst." + customNote;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveDataBox_Click(object sender, RoutedEventArgs e) =>
|
||||||
|
RemoveDataWarning.Visibility = RemoveDataBox.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
|
||||||
|
private async Task RunUninstall()
|
||||||
|
{
|
||||||
|
_busy = true;
|
||||||
|
bool removeData = RemoveDataBox.IsChecked == true;
|
||||||
|
ShowOnly(PageProgress);
|
||||||
|
ProgressTitle.Text = "AeroMC wird entfernt …";
|
||||||
|
LaunchBox.Visibility = Visibility.Collapsed;
|
||||||
|
SetButtons(false);
|
||||||
|
var progress = new Progress<(double Value, string Text)>(p => SetProgress(p.Value, p.Text));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Run(() => Installer.Uninstall(removeData, progress));
|
||||||
|
ShowSuccess("AeroMC wurde entfernt", removeData
|
||||||
|
? "AeroMC, deine Instanzen, Konten und Einstellungen wurden entfernt (die Daten liegen im Papierkorb)."
|
||||||
|
: "Deine Instanzen und Einstellungen liegen weiterhin in " + Installer.DataDir + " – falls du AeroMC später wieder installierst.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ProgressTitle.Text = "Deinstallation fehlgeschlagen";
|
||||||
|
ProgressBarPanel.Visibility = Visibility.Collapsed;
|
||||||
|
ProgressText.Text = ex.Message;
|
||||||
|
}
|
||||||
|
_done = true;
|
||||||
|
_busy = false;
|
||||||
|
CloseButton.IsEnabled = true;
|
||||||
|
NextButton.Content = "Schließen";
|
||||||
|
NextButton.IsEnabled = true;
|
||||||
|
CancelButton.Visibility = Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Helfer ----------
|
||||||
|
|
||||||
|
private void SetButtons(bool enabled)
|
||||||
|
{
|
||||||
|
NextButton.IsEnabled = enabled;
|
||||||
|
BackButton.IsEnabled = enabled;
|
||||||
|
CancelButton.IsEnabled = enabled;
|
||||||
|
CloseButton.IsEnabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetProgress(double value, string text)
|
||||||
|
{
|
||||||
|
if (_done) return;
|
||||||
|
ProgressText.Text = text;
|
||||||
|
PercentText.Text = $"{(int)Math.Round(value * 100)} %";
|
||||||
|
_progress = value;
|
||||||
|
AnimateProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
private double _progress;
|
||||||
|
|
||||||
|
private void AnimateProgress()
|
||||||
|
{
|
||||||
|
var target = Math.Max(0, ProgressTrack.ActualWidth * _progress);
|
||||||
|
ProgressFill.BeginAnimation(WidthProperty,
|
||||||
|
new DoubleAnimation(target, TimeSpan.FromMilliseconds(250)) { EasingFunction = new CubicEase() });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kommt der erste Wert, bevor die Seite fertig angeordnet ist (Breite 0), nachträglich richtig setzen
|
||||||
|
private void ProgressTrack_SizeChanged(object sender, SizeChangedEventArgs e) => AnimateProgress();
|
||||||
|
|
||||||
|
private static bool IsFullPath(string p) =>
|
||||||
|
p.Length >= 3 && char.IsLetter(p[0]) && p[1] == ':' && (p[2] == '\\' || p[2] == '/');
|
||||||
|
|
||||||
|
private void Minimize_Click(object sender, RoutedEventArgs e) => WindowState = WindowState.Minimized;
|
||||||
|
|
||||||
|
private async void Cancel_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_busy || _dialog is not null) return;
|
||||||
|
if (!_done && _mode == SetupMode.Install && _page > 0)
|
||||||
|
{
|
||||||
|
bool abbrechen = await Frage("Installation abbrechen?",
|
||||||
|
"AeroMC wird nicht installiert. Du kannst das Setup später jederzeit erneut starten.",
|
||||||
|
"Ja, abbrechen", "Weiter installieren", "\uE897");
|
||||||
|
if (!abbrechen) return;
|
||||||
|
}
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- eigene Rückfragen im Setup-Look ----------
|
||||||
|
|
||||||
|
private TaskCompletionSource<bool>? _dialog;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Zeigt eine Rückfrage über dem Fenster. Mit <paramref name="zurueck"/>: zwei Knöpfe –
|
||||||
|
/// der hervorgehobene ist der sichere Weg zurück (liefert false), der andere bestätigt (true).
|
||||||
|
/// Ohne: nur ein Hinweis mit einem Knopf (liefert true).
|
||||||
|
/// </summary>
|
||||||
|
private Task<bool> Frage(string titel, string text, string bestaetigen, string? zurueck = null, string symbol = "\uE7BA")
|
||||||
|
{
|
||||||
|
_dialog?.TrySetResult(false);
|
||||||
|
_dialog = new TaskCompletionSource<bool>();
|
||||||
|
DialogTitle.Text = titel;
|
||||||
|
DialogText.Text = text;
|
||||||
|
DialogIcon.Text = symbol;
|
||||||
|
if (zurueck is null)
|
||||||
|
{
|
||||||
|
DialogSecondary.Visibility = Visibility.Collapsed;
|
||||||
|
DialogPrimary.Content = bestaetigen;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DialogSecondary.Visibility = Visibility.Visible;
|
||||||
|
DialogSecondary.Content = bestaetigen;
|
||||||
|
DialogPrimary.Content = zurueck;
|
||||||
|
}
|
||||||
|
|
||||||
|
DialogOverlay.Visibility = Visibility.Visible;
|
||||||
|
var ein = new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(160));
|
||||||
|
DialogOverlay.BeginAnimation(OpacityProperty, ein);
|
||||||
|
var groesse = new DoubleAnimation(0.94, 1, TimeSpan.FromMilliseconds(220)) { EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } };
|
||||||
|
DialogScale.BeginAnimation(ScaleTransform.ScaleXProperty, groesse);
|
||||||
|
DialogScale.BeginAnimation(ScaleTransform.ScaleYProperty, groesse);
|
||||||
|
DialogPrimary.Focus();
|
||||||
|
return _dialog.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SchliesseFrage(bool ergebnis)
|
||||||
|
{
|
||||||
|
if (_dialog is null) return;
|
||||||
|
var tcs = _dialog;
|
||||||
|
_dialog = null;
|
||||||
|
DialogOverlay.Visibility = Visibility.Collapsed;
|
||||||
|
tcs.TrySetResult(ergebnis);
|
||||||
|
}
|
||||||
|
|
||||||
|
// hervorgehobener Knopf: bei Rückfragen "zurück" (false), beim reinen Hinweis "OK" (true)
|
||||||
|
private void DialogPrimary_Click(object sender, RoutedEventArgs e) =>
|
||||||
|
SchliesseFrage(DialogSecondary.Visibility != Visibility.Visible);
|
||||||
|
|
||||||
|
private void DialogSecondary_Click(object sender, RoutedEventArgs e) => SchliesseFrage(true);
|
||||||
|
|
||||||
|
protected override void OnPreviewKeyDown(System.Windows.Input.KeyEventArgs e)
|
||||||
|
{
|
||||||
|
// solange die Rückfrage offen ist: Enter = hervorgehobener Knopf, Esc = zurück
|
||||||
|
if (_dialog is not null)
|
||||||
|
{
|
||||||
|
if (e.Key == System.Windows.Input.Key.Escape) { SchliesseFrage(DialogSecondary.Visibility != Visibility.Visible); e.Handled = true; return; }
|
||||||
|
if (e.Key == System.Windows.Input.Key.Enter) { DialogPrimary_Click(this, new RoutedEventArgs()); e.Handled = true; return; }
|
||||||
|
}
|
||||||
|
base.OnPreviewKeyDown(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
|
||||||
|
{
|
||||||
|
if (_busy) e.Cancel = true; // während der Installation nicht schließen
|
||||||
|
_dialog?.TrySetResult(false);
|
||||||
|
base.OnClosing(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace AeroMCSetup;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Die App liegt als ZIP hinten an der Setup-.exe angehängt:
|
||||||
|
/// [Setup.exe][ZIP][8 Byte: ZIP-Länge][8 Byte: "AEROMCPK"]
|
||||||
|
/// Windows ignoriert angehängte Daten beim Starten einer .exe.
|
||||||
|
/// </summary>
|
||||||
|
public static class Payload
|
||||||
|
{
|
||||||
|
private static readonly byte[] Magic = Encoding.ASCII.GetBytes("AEROMCPK");
|
||||||
|
private const int FooterSize = 16;
|
||||||
|
|
||||||
|
private static (long Offset, long Length)? _info;
|
||||||
|
private static bool _checked;
|
||||||
|
|
||||||
|
/// <summary>Position und Länge des ZIPs in dieser .exe, oder null (z. B. beim Deinstallationsprogramm).</summary>
|
||||||
|
public static (long Offset, long Length)? Info
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_checked) return _info;
|
||||||
|
_checked = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var f = File.OpenRead(Installer.SelfPath);
|
||||||
|
if (f.Length < FooterSize) return null;
|
||||||
|
f.Seek(-FooterSize, SeekOrigin.End);
|
||||||
|
var footer = new byte[FooterSize];
|
||||||
|
if (f.Read(footer, 0, FooterSize) != FooterSize) return null;
|
||||||
|
for (int i = 0; i < Magic.Length; i++)
|
||||||
|
if (footer[8 + i] != Magic[i]) return null;
|
||||||
|
long length = BitConverter.ToInt64(footer, 0);
|
||||||
|
long offset = f.Length - FooterSize - length;
|
||||||
|
if (length <= 0 || offset <= 0) return null;
|
||||||
|
_info = (offset, length);
|
||||||
|
}
|
||||||
|
catch { _info = null; }
|
||||||
|
return _info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool Exists => Info is not null;
|
||||||
|
|
||||||
|
/// <summary>Länge des eigentlichen Setup-Programms ohne Anhang.</summary>
|
||||||
|
public static long StubLength => Info?.Offset ?? new FileInfo(Installer.SelfPath).Length;
|
||||||
|
|
||||||
|
public static ZipArchive OpenArchive()
|
||||||
|
{
|
||||||
|
var info = Info ?? throw new InvalidOperationException("Die Installationsdaten fehlen in dieser Datei. Bitte AeroMC-Setup.exe neu herunterladen.");
|
||||||
|
var file = File.OpenRead(Installer.SelfPath);
|
||||||
|
return new ZipArchive(new SubStream(file, info.Offset, info.Length), ZipArchiveMode.Read, leaveOpen: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Nur-Lese-Ausschnitt einer Datei, damit ZipArchive den Anhang wie eine eigene Datei sieht.</summary>
|
||||||
|
private sealed class SubStream(Stream inner, long start, long length) : Stream
|
||||||
|
{
|
||||||
|
private long _pos;
|
||||||
|
|
||||||
|
public override bool CanRead => true;
|
||||||
|
public override bool CanSeek => true;
|
||||||
|
public override bool CanWrite => false;
|
||||||
|
public override long Length => length;
|
||||||
|
|
||||||
|
public override long Position
|
||||||
|
{
|
||||||
|
get => _pos;
|
||||||
|
set => _pos = Math.Max(0, Math.Min(value, length));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int Read(byte[] buffer, int offset, int count)
|
||||||
|
{
|
||||||
|
long left = length - _pos;
|
||||||
|
if (left <= 0) return 0;
|
||||||
|
if (count > left) count = (int)left;
|
||||||
|
inner.Position = start + _pos;
|
||||||
|
int n = inner.Read(buffer, offset, count);
|
||||||
|
_pos += n;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override long Seek(long offset, SeekOrigin origin)
|
||||||
|
{
|
||||||
|
Position = origin switch
|
||||||
|
{
|
||||||
|
SeekOrigin.Begin => offset,
|
||||||
|
SeekOrigin.Current => _pos + offset,
|
||||||
|
_ => length + offset,
|
||||||
|
};
|
||||||
|
return _pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Flush() { }
|
||||||
|
public override void SetLength(long value) => throw new NotSupportedException();
|
||||||
|
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing) inner.Dispose();
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Runtime.InteropServices.ComTypes;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace AeroMCSetup;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verknüpfungen (.lnk) über die Shell-Schnittstellen – inklusive AppUserModelID, damit
|
||||||
|
/// Taskleiste und Windows-Benachrichtigungen AeroMC wie bisher zuordnen (wie beim alten NSIS-Setup).
|
||||||
|
/// </summary>
|
||||||
|
public static class Shortcut
|
||||||
|
{
|
||||||
|
public static void Create(string link, string target, string description, string appUserModelId)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(link)!);
|
||||||
|
var shellLink = (IShellLinkW)new CShellLink();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
shellLink.SetPath(target);
|
||||||
|
shellLink.SetWorkingDirectory(Path.GetDirectoryName(target)!);
|
||||||
|
shellLink.SetIconLocation(target, 0);
|
||||||
|
shellLink.SetDescription(description);
|
||||||
|
|
||||||
|
var store = (IPropertyStore)shellLink;
|
||||||
|
var key = new PropertyKey(new Guid("9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3"), 5); // PKEY_AppUserModel_ID
|
||||||
|
var value = new PropVariant { vt = 31 /* VT_LPWSTR */, p = Marshal.StringToCoTaskMemUni(appUserModelId) };
|
||||||
|
try
|
||||||
|
{
|
||||||
|
store.SetValue(ref key, ref value);
|
||||||
|
store.Commit();
|
||||||
|
}
|
||||||
|
finally { Marshal.FreeCoTaskMem(value.p); }
|
||||||
|
|
||||||
|
((IPersistFile)shellLink).Save(link, true);
|
||||||
|
}
|
||||||
|
finally { Marshal.ReleaseComObject(shellLink); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Ziel einer vorhandenen Verknüpfung, oder null.</summary>
|
||||||
|
public static string? Target(string link)
|
||||||
|
{
|
||||||
|
if (!File.Exists(link)) return null;
|
||||||
|
var shellLink = (IShellLinkW)new CShellLink();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
((IPersistFile)shellLink).Load(link, 0);
|
||||||
|
var sb = new StringBuilder(1024);
|
||||||
|
shellLink.GetPath(sb, sb.Capacity, IntPtr.Zero, 0);
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
catch { return null; }
|
||||||
|
finally { Marshal.ReleaseComObject(shellLink); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- COM ----------
|
||||||
|
|
||||||
|
[ComImport, Guid("00021401-0000-0000-C000-000000000046")]
|
||||||
|
private class CShellLink { }
|
||||||
|
|
||||||
|
[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("000214F9-0000-0000-C000-000000000046")]
|
||||||
|
private interface IShellLinkW
|
||||||
|
{
|
||||||
|
void GetPath([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxPath, IntPtr pfd, uint fFlags);
|
||||||
|
void GetIDList(out IntPtr ppidl);
|
||||||
|
void SetIDList(IntPtr pidl);
|
||||||
|
void GetDescription([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cchMaxName);
|
||||||
|
void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
|
||||||
|
void GetWorkingDirectory([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cchMaxPath);
|
||||||
|
void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
|
||||||
|
void GetArguments([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cchMaxPath);
|
||||||
|
void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
|
||||||
|
void GetHotkey(out short pwHotkey);
|
||||||
|
void SetHotkey(short wHotkey);
|
||||||
|
void GetShowCmd(out int piShowCmd);
|
||||||
|
void SetShowCmd(int iShowCmd);
|
||||||
|
void GetIconLocation([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath, int cchIconPath, out int piIcon);
|
||||||
|
void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
|
||||||
|
void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, uint dwReserved);
|
||||||
|
void Resolve(IntPtr hwnd, uint fFlags);
|
||||||
|
void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99")]
|
||||||
|
private interface IPropertyStore
|
||||||
|
{
|
||||||
|
void GetCount(out uint cProps);
|
||||||
|
void GetAt(uint iProp, out PropertyKey pkey);
|
||||||
|
void GetValue(ref PropertyKey key, out PropVariant pv);
|
||||||
|
void SetValue(ref PropertyKey key, ref PropVariant pv);
|
||||||
|
void Commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||||
|
private struct PropertyKey(Guid fmtid, uint pid)
|
||||||
|
{
|
||||||
|
public Guid fmtid = fmtid;
|
||||||
|
public uint pid = pid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PROPVARIANT: 16 Byte (32 Bit) bzw. 24 Byte (64 Bit)
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct PropVariant
|
||||||
|
{
|
||||||
|
public ushort vt;
|
||||||
|
public ushort r1, r2, r3;
|
||||||
|
public IntPtr p;
|
||||||
|
public IntPtr p2;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<SolidColorBrush x:Key="BaseBrush" Color="#202020"/>
|
||||||
|
<SolidColorBrush x:Key="LayerBrush" Color="#282828"/>
|
||||||
|
<SolidColorBrush x:Key="LayerStrokeBrush" Color="#1A1A1A"/>
|
||||||
|
<SolidColorBrush x:Key="SubtleHoverBrush" Color="#0FFFFFFF"/>
|
||||||
|
<SolidColorBrush x:Key="SubtlePressedBrush" Color="#0AFFFFFF"/>
|
||||||
|
<SolidColorBrush x:Key="SubtleSelectedBrush" Color="#14FFFFFF"/>
|
||||||
|
<SolidColorBrush x:Key="ControlFillBrush" Color="#2D2D2D"/>
|
||||||
|
<SolidColorBrush x:Key="ControlHoverBrush" Color="#323232"/>
|
||||||
|
<SolidColorBrush x:Key="ControlPressedBrush" Color="#272727"/>
|
||||||
|
<SolidColorBrush x:Key="ControlStrokeBrush" Color="#353535"/>
|
||||||
|
<SolidColorBrush x:Key="ControlStrongStrokeBrush" Color="#8A8A8A"/>
|
||||||
|
<SolidColorBrush x:Key="InputFocusBrush" Color="#1F1F1F"/>
|
||||||
|
<SolidColorBrush x:Key="TextBrush" Color="#FFFFFF"/>
|
||||||
|
<SolidColorBrush x:Key="TextSecondaryBrush" Color="#C8C8C8"/>
|
||||||
|
<SolidColorBrush x:Key="TextTertiaryBrush" Color="#9A9A9A"/>
|
||||||
|
<SolidColorBrush x:Key="FlyoutBrush" Color="#2C2C2C"/>
|
||||||
|
<SolidColorBrush x:Key="FlyoutStrokeBrush" Color="#404040"/>
|
||||||
|
<SolidColorBrush x:Key="DialogFooterBrush" Color="#202020"/>
|
||||||
|
<SolidColorBrush x:Key="ScrollThumbBrush" Color="#9F9F9F"/>
|
||||||
|
<SolidColorBrush x:Key="OverlayBrush" Color="#66000000"/>
|
||||||
|
<SolidColorBrush x:Key="SuccessBrush" Color="#4CAF50"/>
|
||||||
|
<SolidColorBrush x:Key="DangerBrush" Color="#FF99A4"/>
|
||||||
|
<SolidColorBrush x:Key="FenceBrush" Color="#F21C1C1C"/>
|
||||||
|
<Color x:Key="ShadowColor">#000000</Color>
|
||||||
|
</ResourceDictionary>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<SolidColorBrush x:Key="BaseBrush" Color="#F3F3F3"/>
|
||||||
|
<SolidColorBrush x:Key="LayerBrush" Color="#FAFAFA"/>
|
||||||
|
<SolidColorBrush x:Key="LayerStrokeBrush" Color="#E5E5E5"/>
|
||||||
|
<SolidColorBrush x:Key="SubtleHoverBrush" Color="#0A000000"/>
|
||||||
|
<SolidColorBrush x:Key="SubtlePressedBrush" Color="#06000000"/>
|
||||||
|
<SolidColorBrush x:Key="SubtleSelectedBrush" Color="#0F000000"/>
|
||||||
|
<SolidColorBrush x:Key="ControlFillBrush" Color="#FDFDFD"/>
|
||||||
|
<SolidColorBrush x:Key="ControlHoverBrush" Color="#F6F6F6"/>
|
||||||
|
<SolidColorBrush x:Key="ControlPressedBrush" Color="#F2F2F2"/>
|
||||||
|
<SolidColorBrush x:Key="ControlStrokeBrush" Color="#E3E3E3"/>
|
||||||
|
<SolidColorBrush x:Key="ControlStrongStrokeBrush" Color="#C4C4C4"/>
|
||||||
|
<SolidColorBrush x:Key="InputFocusBrush" Color="#FFFFFF"/>
|
||||||
|
<SolidColorBrush x:Key="TextBrush" Color="#1B1B1B"/>
|
||||||
|
<SolidColorBrush x:Key="TextSecondaryBrush" Color="#5D5D5D"/>
|
||||||
|
<SolidColorBrush x:Key="TextTertiaryBrush" Color="#8B8B8B"/>
|
||||||
|
<SolidColorBrush x:Key="FlyoutBrush" Color="#F9F9F9"/>
|
||||||
|
<SolidColorBrush x:Key="FlyoutStrokeBrush" Color="#DCDCDC"/>
|
||||||
|
<SolidColorBrush x:Key="DialogFooterBrush" Color="#F3F3F3"/>
|
||||||
|
<SolidColorBrush x:Key="ScrollThumbBrush" Color="#8A8A8A"/>
|
||||||
|
<SolidColorBrush x:Key="OverlayBrush" Color="#4D000000"/>
|
||||||
|
<SolidColorBrush x:Key="SuccessBrush" Color="#0F7B0F"/>
|
||||||
|
<SolidColorBrush x:Key="DangerBrush" Color="#C42B1C"/>
|
||||||
|
<SolidColorBrush x:Key="FenceBrush" Color="#F5F7F7F7"/>
|
||||||
|
<Color x:Key="ShadowColor">#000000</Color>
|
||||||
|
</ResourceDictionary>
|
||||||
@@ -0,0 +1,651 @@
|
|||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
|
||||||
|
<!-- Schriften: Windows 11 nutzt "Segoe UI Variable" / "Segoe Fluent Icons", Windows 10 fällt auf Segoe UI / MDL2 zurück -->
|
||||||
|
<FontFamily x:Key="UiFont">Segoe UI Variable Text, Segoe UI</FontFamily>
|
||||||
|
<FontFamily x:Key="DisplayFont">Segoe UI Variable Display, Segoe UI</FontFamily>
|
||||||
|
<FontFamily x:Key="IconFont">Segoe Fluent Icons, Segoe MDL2 Assets</FontFamily>
|
||||||
|
|
||||||
|
<DropShadowEffect x:Key="FlyoutShadow" BlurRadius="24" ShadowDepth="6" Direction="270" Opacity="0.18"
|
||||||
|
Color="Black"/>
|
||||||
|
<DropShadowEffect x:Key="DialogShadow" BlurRadius="40" ShadowDepth="12" Direction="270" Opacity="0.28"
|
||||||
|
Color="Black"/>
|
||||||
|
|
||||||
|
<!-- ===== Buttons ===== -->
|
||||||
|
<Style TargetType="Button">
|
||||||
|
<Setter Property="FontFamily" Value="{DynamicResource UiFont}"/>
|
||||||
|
<Setter Property="FontSize" Value="14"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="Background" Value="{DynamicResource ControlFillBrush}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ControlStrokeBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="Padding" Value="14,5"/>
|
||||||
|
<Setter Property="MinHeight" Value="32"/>
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border x:Name="Bd" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="4" Padding="{TemplateBinding Padding}"
|
||||||
|
SnapsToDevicePixels="True">
|
||||||
|
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||||
|
VerticalAlignment="Center" RecognizesAccessKey="True"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ControlHoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsPressed" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ControlPressedBrush}"/>
|
||||||
|
<Setter Property="Opacity" Value="0.85"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsKeyboardFocused" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource TextBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.4"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="AccentButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource OnAccentBrush}"/>
|
||||||
|
<Setter Property="Background" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border x:Name="Bd" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="1" CornerRadius="4" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="True">
|
||||||
|
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||||
|
VerticalAlignment="Center" RecognizesAccessKey="True"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentHoverBrush}"/>
|
||||||
|
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource AccentHoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsPressed" Value="True">
|
||||||
|
<Setter Property="Opacity" Value="0.8"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsKeyboardFocused" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource TextBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.4"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Transparenter Button (Symbolleisten, Sidebar) -->
|
||||||
|
<Style x:Key="SubtleButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
<Setter Property="Padding" Value="9,6"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border x:Name="Bd" Background="{TemplateBinding Background}" CornerRadius="4" Padding="{TemplateBinding Padding}">
|
||||||
|
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsPressed" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtlePressedBrush}"/>
|
||||||
|
<Setter Property="Opacity" Value="0.75"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.35"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Titelleisten-Buttons -->
|
||||||
|
<Style x:Key="CaptionButton" TargetType="Button">
|
||||||
|
<Setter Property="Width" Value="46"/>
|
||||||
|
<Setter Property="Height" Value="32"/>
|
||||||
|
<Setter Property="FontFamily" Value="{DynamicResource IconFont}"/>
|
||||||
|
<Setter Property="FontSize" Value="10"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="IsTabStop" Value="False"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border x:Name="Bd" Background="{TemplateBinding Background}">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsPressed" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtlePressedBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="CloseCaptionButton" TargetType="Button" BasedOn="{StaticResource CaptionButton}">
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border x:Name="Bd" Background="Transparent">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="#C42B1C"/>
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsPressed" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="#C83C30"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== TextBox ===== -->
|
||||||
|
<Style TargetType="TextBox">
|
||||||
|
<Setter Property="FontFamily" Value="{DynamicResource UiFont}"/>
|
||||||
|
<Setter Property="FontSize" Value="14"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="CaretBrush" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="SelectionBrush" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
<Setter Property="Background" Value="{DynamicResource ControlFillBrush}"/>
|
||||||
|
<Setter Property="Padding" Value="10,6"/>
|
||||||
|
<Setter Property="MinHeight" Value="32"/>
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="TextBox">
|
||||||
|
<Grid>
|
||||||
|
<Border x:Name="Bd" Background="{TemplateBinding Background}" CornerRadius="4"
|
||||||
|
BorderBrush="{DynamicResource ControlStrokeBrush}" BorderThickness="1"/>
|
||||||
|
<!-- Unterstrich wie bei Windows 11 -->
|
||||||
|
<Border x:Name="Line" Height="1" VerticalAlignment="Bottom" Margin="3,0" CornerRadius="0,0,4,4"
|
||||||
|
Background="{DynamicResource ControlStrongStrokeBrush}"/>
|
||||||
|
<ScrollViewer x:Name="PART_ContentHost" Margin="{TemplateBinding Padding}" VerticalAlignment="Center"
|
||||||
|
Focusable="False" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden"/>
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ControlHoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsKeyboardFocused" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource InputFocusBrush}"/>
|
||||||
|
<Setter TargetName="Line" Property="Height" Value="2"/>
|
||||||
|
<Setter TargetName="Line" Property="Margin" Value="1,0"/>
|
||||||
|
<Setter TargetName="Line" Property="Background" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== Scrollbar (schmal, wird beim Überfahren breiter) ===== -->
|
||||||
|
<Style x:Key="ScrollThumb" TargetType="Thumb">
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Thumb">
|
||||||
|
<Border Background="{DynamicResource ScrollThumbBrush}" CornerRadius="3"/>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="ScrollBar">
|
||||||
|
<Setter Property="Width" Value="12"/>
|
||||||
|
<Setter Property="MinWidth" Value="12"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ScrollBar">
|
||||||
|
<Grid Background="Transparent">
|
||||||
|
<Track x:Name="PART_Track" IsDirectionReversed="True">
|
||||||
|
<Track.Thumb>
|
||||||
|
<Thumb x:Name="Th" Style="{StaticResource ScrollThumb}" Width="3" HorizontalAlignment="Center" Opacity="0.7"/>
|
||||||
|
</Track.Thumb>
|
||||||
|
</Track>
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Th" Property="Width" Value="6"/>
|
||||||
|
<Setter TargetName="Th" Property="Opacity" Value="1"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="Orientation" Value="Horizontal">
|
||||||
|
<Setter Property="Width" Value="Auto"/>
|
||||||
|
<Setter Property="MinWidth" Value="0"/>
|
||||||
|
<Setter Property="Height" Value="12"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ScrollBar">
|
||||||
|
<Grid Background="Transparent">
|
||||||
|
<Track x:Name="PART_Track">
|
||||||
|
<Track.Thumb>
|
||||||
|
<Thumb x:Name="Th" Style="{StaticResource ScrollThumb}" Height="3" VerticalAlignment="Center" Opacity="0.7"/>
|
||||||
|
</Track.Thumb>
|
||||||
|
</Track>
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Th" Property="Height" Value="6"/>
|
||||||
|
<Setter TargetName="Th" Property="Opacity" Value="1"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== ToolTip ===== -->
|
||||||
|
<Style TargetType="ToolTip">
|
||||||
|
<Setter Property="FontFamily" Value="{DynamicResource UiFont}"/>
|
||||||
|
<Setter Property="FontSize" Value="12"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="HasDropShadow" Value="False"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ToolTip">
|
||||||
|
<Border Margin="8,4,8,12" Padding="9,6" CornerRadius="4" MaxWidth="320"
|
||||||
|
Background="{DynamicResource FlyoutBrush}" BorderBrush="{DynamicResource FlyoutStrokeBrush}" BorderThickness="1"
|
||||||
|
Effect="{StaticResource FlyoutShadow}">
|
||||||
|
<ContentPresenter>
|
||||||
|
<ContentPresenter.Resources>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="TextWrapping" Value="Wrap"/>
|
||||||
|
</Style>
|
||||||
|
</ContentPresenter.Resources>
|
||||||
|
</ContentPresenter>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== Kontextmenü ===== -->
|
||||||
|
<Style TargetType="ContextMenu">
|
||||||
|
<Setter Property="FontFamily" Value="{DynamicResource UiFont}"/>
|
||||||
|
<Setter Property="FontSize" Value="14"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="HasDropShadow" Value="False"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ContextMenu">
|
||||||
|
<Border Margin="12,4,12,16" Padding="0,4" CornerRadius="8" MinWidth="200"
|
||||||
|
Background="{DynamicResource FlyoutBrush}" BorderBrush="{DynamicResource FlyoutStrokeBrush}" BorderThickness="1"
|
||||||
|
Effect="{StaticResource FlyoutShadow}">
|
||||||
|
<ItemsPresenter KeyboardNavigation.DirectionalNavigation="Cycle"/>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="{x:Static MenuItem.SeparatorStyleKey}" TargetType="Separator">
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Separator">
|
||||||
|
<Border Height="1" Margin="0,4" Background="{DynamicResource FlyoutStrokeBrush}"/>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="MenuItem">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="MenuItem">
|
||||||
|
<Border x:Name="Bd" Background="Transparent" CornerRadius="4" Margin="4,1" Padding="11,0" Height="34">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="28"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<ContentPresenter ContentSource="Icon" VerticalAlignment="Center" HorizontalAlignment="Left"
|
||||||
|
TextElement.FontFamily="{DynamicResource IconFont}" TextElement.FontSize="16"/>
|
||||||
|
<ContentPresenter Grid.Column="1" ContentSource="Header" VerticalAlignment="Center" RecognizesAccessKey="True"/>
|
||||||
|
<TextBlock x:Name="Gesture" Grid.Column="2" Text="{TemplateBinding InputGestureText}" FontSize="12"
|
||||||
|
VerticalAlignment="Center" Margin="24,0,0,0" Foreground="{DynamicResource TextTertiaryBrush}"/>
|
||||||
|
<TextBlock x:Name="Arrow" Grid.Column="2" Text="" FontFamily="{DynamicResource IconFont}" FontSize="11"
|
||||||
|
VerticalAlignment="Center" Margin="24,0,0,0" Visibility="Collapsed"
|
||||||
|
Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
<Popup x:Name="PART_Popup" Placement="Right" HorizontalOffset="-4" VerticalOffset="-12"
|
||||||
|
IsOpen="{Binding IsSubmenuOpen, RelativeSource={RelativeSource TemplatedParent}}"
|
||||||
|
AllowsTransparency="True" Focusable="False" PopupAnimation="Fade">
|
||||||
|
<Border Margin="12,4,12,16" Padding="0,4" CornerRadius="8" MinWidth="180"
|
||||||
|
Background="{DynamicResource FlyoutBrush}" BorderBrush="{DynamicResource FlyoutStrokeBrush}"
|
||||||
|
BorderThickness="1" Effect="{StaticResource FlyoutShadow}">
|
||||||
|
<ScrollViewer MaxHeight="420" VerticalScrollBarVisibility="Auto">
|
||||||
|
<ItemsPresenter KeyboardNavigation.DirectionalNavigation="Cycle"/>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Border>
|
||||||
|
</Popup>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsHighlighted" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="Role" Value="SubmenuHeader">
|
||||||
|
<Setter TargetName="Arrow" Property="Visibility" Value="Visible"/>
|
||||||
|
<Setter TargetName="Gesture" Property="Visibility" Value="Collapsed"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.4"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== Listen ===== -->
|
||||||
|
<Style TargetType="ListBox">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ListBox">
|
||||||
|
<ScrollViewer Focusable="False" Padding="{TemplateBinding Padding}">
|
||||||
|
<ItemsPresenter/>
|
||||||
|
</ScrollViewer>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Navigationseintrag in der Seitenleiste -->
|
||||||
|
<Style x:Key="NavItem" TargetType="ListBoxItem">
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="Height" Value="38"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ListBoxItem">
|
||||||
|
<Grid Margin="4,1" Background="Transparent">
|
||||||
|
<Border x:Name="Bd" CornerRadius="4" BorderThickness="1" BorderBrush="Transparent" Background="Transparent"/>
|
||||||
|
<Rectangle x:Name="Pill" Width="3" Height="16" RadiusX="1.5" RadiusY="1.5" HorizontalAlignment="Left"
|
||||||
|
Fill="{DynamicResource AccentBrush}" Visibility="Collapsed"/>
|
||||||
|
<ContentPresenter Margin="14,0,12,0" VerticalAlignment="Center"/>
|
||||||
|
<!-- Einfügelinie beim Umsortieren -->
|
||||||
|
<Border x:Name="InsertTop" Height="2" CornerRadius="1" VerticalAlignment="Top" Margin="6,-2,6,0"
|
||||||
|
Background="{DynamicResource AccentBrush}" Visibility="Collapsed"/>
|
||||||
|
<Border x:Name="InsertBottom" Height="2" CornerRadius="1" VerticalAlignment="Bottom" Margin="6,0,6,-2"
|
||||||
|
Background="{DynamicResource AccentBrush}" Visibility="Collapsed"/>
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsSelected" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleSelectedBrush}"/>
|
||||||
|
<Setter TargetName="Pill" Property="Visibility" Value="Visible"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="Tag" Value="before">
|
||||||
|
<Setter TargetName="InsertTop" Property="Visibility" Value="Visible"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="Tag" Value="after">
|
||||||
|
<Setter TargetName="InsertBottom" Property="Visibility" Value="Visible"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="Tag" Value="drop">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentSubtleBrush}"/>
|
||||||
|
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Icon-Kachel -->
|
||||||
|
<Style x:Key="TileItem" TargetType="ListBoxItem">
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ListBoxItem">
|
||||||
|
<Border x:Name="Bd" Margin="2" CornerRadius="6" BorderThickness="1" BorderBrush="Transparent" Background="Transparent">
|
||||||
|
<ContentPresenter/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsSelected" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentSubtleBrush}"/>
|
||||||
|
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource AccentSubtleStrokeBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== Listenansicht-Zeile ===== -->
|
||||||
|
<Style x:Key="RowItem" TargetType="ListBoxItem">
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ListBoxItem">
|
||||||
|
<Grid Margin="0,1">
|
||||||
|
<Border x:Name="Bd" CornerRadius="4" Background="Transparent"/>
|
||||||
|
<Rectangle x:Name="Pill" Width="3" Height="16" RadiusX="1.5" RadiusY="1.5" HorizontalAlignment="Left"
|
||||||
|
Fill="{DynamicResource AccentBrush}" Visibility="Collapsed"/>
|
||||||
|
<ContentPresenter/>
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsSelected" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentSubtleBrush}"/>
|
||||||
|
<Setter TargetName="Pill" Property="Visibility" Value="Visible"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== Karte (Einstellungen) ===== -->
|
||||||
|
<Style x:Key="Card" TargetType="Border">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource ControlFillBrush}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ControlStrokeBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="CornerRadius" Value="6"/>
|
||||||
|
<Setter Property="Padding" Value="18,14"/>
|
||||||
|
<Setter Property="Margin" Value="0,0,0,4"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="SectionTitle" TargetType="TextBlock">
|
||||||
|
<Setter Property="FontSize" Value="14"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="Margin" Value="2,24,0,8"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== Kippschalter (Windows 11) ===== -->
|
||||||
|
<Style x:Key="ToggleSwitch" TargetType="CheckBox">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="CheckBox">
|
||||||
|
<StackPanel Orientation="Horizontal" Background="Transparent">
|
||||||
|
<TextBlock x:Name="State" Text="Aus" VerticalAlignment="Center" Margin="0,0,12,0" MinWidth="24" TextAlignment="Right"/>
|
||||||
|
<Grid Width="40" Height="20">
|
||||||
|
<Border x:Name="Track" CornerRadius="10" BorderThickness="1"
|
||||||
|
Background="{DynamicResource SubtleSelectedBrush}" BorderBrush="{DynamicResource ControlStrongStrokeBrush}"/>
|
||||||
|
<Ellipse x:Name="Knob" Width="12" Height="12" HorizontalAlignment="Left" Margin="4,0,0,0"
|
||||||
|
Fill="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsChecked" Value="True">
|
||||||
|
<Setter TargetName="Track" Property="Background" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
<Setter TargetName="Track" Property="BorderBrush" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
<Setter TargetName="Knob" Property="Fill" Value="{DynamicResource OnAccentBrush}"/>
|
||||||
|
<Setter TargetName="Knob" Property="HorizontalAlignment" Value="Right"/>
|
||||||
|
<Setter TargetName="Knob" Property="Margin" Value="0,0,4,0"/>
|
||||||
|
<Setter TargetName="State" Property="Text" Value="Ein"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Knob" Property="Width" Value="14"/>
|
||||||
|
<Setter TargetName="Knob" Property="Height" Value="14"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.4"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== Kontrollkästchen ===== -->
|
||||||
|
<Style TargetType="CheckBox">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="FontFamily" Value="{DynamicResource UiFont}"/>
|
||||||
|
<Setter Property="FontSize" Value="14"/>
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="CheckBox">
|
||||||
|
<StackPanel Orientation="Horizontal" Background="Transparent">
|
||||||
|
<Border x:Name="Box" Width="20" Height="20" CornerRadius="4" BorderThickness="1" VerticalAlignment="Center"
|
||||||
|
Background="{DynamicResource ControlFillBrush}" BorderBrush="{DynamicResource ControlStrongStrokeBrush}">
|
||||||
|
<TextBlock x:Name="Check" Text="" FontFamily="{DynamicResource IconFont}" FontSize="12"
|
||||||
|
Foreground="{DynamicResource OnAccentBrush}" HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
|
Visibility="Collapsed"/>
|
||||||
|
</Border>
|
||||||
|
<ContentPresenter Margin="10,0,0,0" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsChecked" Value="True">
|
||||||
|
<Setter TargetName="Box" Property="Background" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
<Setter TargetName="Box" Property="BorderBrush" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
<Setter TargetName="Check" Property="Visibility" Value="Visible"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Box" Property="Opacity" Value="0.85"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.4"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== Segment-Auswahl (z. B. Hell | Dunkel | System) ===== -->
|
||||||
|
<Style x:Key="SegmentRadio" TargetType="RadioButton">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="Padding" Value="14,6"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="RadioButton">
|
||||||
|
<Border x:Name="Bd" CornerRadius="4" Padding="{TemplateBinding Padding}" Margin="2" Background="Transparent">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsChecked" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource OnAccentBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="SegmentGroup" TargetType="Border">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource SubtlePressedBrush}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ControlStrokeBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="CornerRadius" Value="6"/>
|
||||||
|
<Setter Property="Padding" Value="1"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== Farb- und Symbolauswahl ===== -->
|
||||||
|
<Style x:Key="SwatchRadio" TargetType="RadioButton">
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="RadioButton">
|
||||||
|
<Grid Width="34" Height="34" Margin="0,0,6,6" Background="Transparent">
|
||||||
|
<Ellipse x:Name="Ring" Stroke="{DynamicResource TextBrush}" StrokeThickness="2" Visibility="Collapsed"/>
|
||||||
|
<Ellipse Margin="5" Fill="{TemplateBinding Background}" Stroke="{DynamicResource ControlStrokeBrush}"/>
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsChecked" Value="True">
|
||||||
|
<Setter TargetName="Ring" Property="Visibility" Value="Visible"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="GlyphRadio" TargetType="RadioButton">
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="FontFamily" Value="{DynamicResource IconFont}"/>
|
||||||
|
<Setter Property="FontSize" Value="16"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="RadioButton">
|
||||||
|
<Border x:Name="Bd" Width="36" Height="36" Margin="0,0,4,4" CornerRadius="4" BorderThickness="1"
|
||||||
|
BorderBrush="Transparent" Background="Transparent">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsChecked" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentSubtleBrush}"/>
|
||||||
|
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
</ResourceDictionary>
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<assemblyIdentity version="1.0.0.0" name="AeroMC.Setup"/>
|
||||||
|
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||||
|
<security>
|
||||||
|
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<!-- Installation nur für den aktuellen Benutzer → keine Administratorrechte -->
|
||||||
|
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
||||||
|
</requestedPrivileges>
|
||||||
|
</security>
|
||||||
|
</trustInfo>
|
||||||
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||||
|
<application>
|
||||||
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||||
|
</application>
|
||||||
|
</compatibility>
|
||||||
|
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<windowsSettings>
|
||||||
|
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||||
|
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||||
|
</windowsSettings>
|
||||||
|
</application>
|
||||||
|
</assembly>
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"format": 1,
|
||||||
|
"restore": {
|
||||||
|
"C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj": {}
|
||||||
|
},
|
||||||
|
"projects": {
|
||||||
|
"C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"restore": {
|
||||||
|
"projectUniqueName": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj",
|
||||||
|
"projectName": "AeroMC-Setup",
|
||||||
|
"projectPath": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj",
|
||||||
|
"packagesPath": "C:\\Users\\Viper\\.nuget\\packages\\",
|
||||||
|
"outputPath": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\obj\\",
|
||||||
|
"projectStyle": "PackageReference",
|
||||||
|
"configFilePaths": [
|
||||||
|
"C:\\Users\\Viper\\AppData\\Roaming\\NuGet\\NuGet.Config"
|
||||||
|
],
|
||||||
|
"originalTargetFrameworks": [
|
||||||
|
"net48"
|
||||||
|
],
|
||||||
|
"sources": {
|
||||||
|
"https://api.nuget.org/v3/index.json": {}
|
||||||
|
},
|
||||||
|
"frameworks": {
|
||||||
|
"net48": {
|
||||||
|
"targetAlias": "net48",
|
||||||
|
"projectReferences": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"warningProperties": {
|
||||||
|
"warnAsError": [
|
||||||
|
"NU1605"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"restoreAuditProperties": {
|
||||||
|
"enableAudit": "true",
|
||||||
|
"auditLevel": "low",
|
||||||
|
"auditMode": "direct"
|
||||||
|
},
|
||||||
|
"SdkAnalysisLevel": "10.0.100"
|
||||||
|
},
|
||||||
|
"frameworks": {
|
||||||
|
"net48": {
|
||||||
|
"targetAlias": "net48",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.NETFramework.ReferenceAssemblies": {
|
||||||
|
"suppressParent": "All",
|
||||||
|
"target": "Package",
|
||||||
|
"version": "[1.0.3, )",
|
||||||
|
"autoReferenced": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.112\\RuntimeIdentifierGraph.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||||
|
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<PropertyGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||||
|
<RestoreSuccess Condition=" '$(RestoreSuccess)' == '' ">True</RestoreSuccess>
|
||||||
|
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
|
||||||
|
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
|
||||||
|
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot>
|
||||||
|
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\Viper\.nuget\packages\</NuGetPackageFolders>
|
||||||
|
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
|
||||||
|
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">7.0.0</NuGetToolVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||||
|
<SourceRoot Include="C:\Users\Viper\.nuget\packages\" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||||
|
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<ImportGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||||
|
<Import Project="$(NuGetPackageRoot)microsoft.netframework.referenceassemblies.net48\1.0.3\build\Microsoft.NETFramework.ReferenceAssemblies.net48.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.netframework.referenceassemblies.net48\1.0.3\build\Microsoft.NETFramework.ReferenceAssemblies.net48.targets')" />
|
||||||
|
</ImportGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
// <autogenerated />
|
||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
|
||||||
Binary file not shown.
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<startup>
|
||||||
|
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
|
||||||
|
</startup>
|
||||||
|
</configuration>
|
||||||
Binary file not shown.
@@ -0,0 +1,20 @@
|
|||||||
|
AeroMC-Setup
|
||||||
|
|
||||||
|
|
||||||
|
winexe
|
||||||
|
C#
|
||||||
|
.cs
|
||||||
|
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\obj\Release\net48\
|
||||||
|
AeroMCSetup
|
||||||
|
none
|
||||||
|
false
|
||||||
|
TRACE;RELEASE;NETFRAMEWORK;NET48;NET30_OR_GREATER;NET35_OR_GREATER;NET40_OR_GREATER;NET45_OR_GREATER;NET451_OR_GREATER;NET452_OR_GREATER;NET46_OR_GREATER;NET461_OR_GREATER;NET462_OR_GREATER;NET47_OR_GREATER;NET471_OR_GREATER;NET472_OR_GREATER;NET48_OR_GREATER
|
||||||
|
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\App.xaml
|
||||||
|
41549299691
|
||||||
|
|
||||||
|
7-621693760
|
||||||
|
251587687494
|
||||||
|
MainWindow.xaml;Themes\Colors.Dark.xaml;Themes\Colors.Light.xaml;Themes\Controls.xaml;
|
||||||
|
|
||||||
|
False
|
||||||
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// <auto-generated>
|
||||||
|
// This code was generated by a tool.
|
||||||
|
//
|
||||||
|
// Changes to this file may cause incorrect behavior and will be lost if
|
||||||
|
// the code is regenerated.
|
||||||
|
// </auto-generated>
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
[assembly: System.Reflection.AssemblyCompanyAttribute("M_Viper")]
|
||||||
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
|
||||||
|
[assembly: System.Reflection.AssemblyCopyrightAttribute("Copyright (c) M_Viper. Alle Rechte vorbehalten.")]
|
||||||
|
[assembly: System.Reflection.AssemblyDescriptionAttribute("Installiert AeroMC")]
|
||||||
|
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.1.5.0")]
|
||||||
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.1.5")]
|
||||||
|
[assembly: System.Reflection.AssemblyProductAttribute("AeroMC Setup")]
|
||||||
|
[assembly: System.Reflection.AssemblyTitleAttribute("AeroMC Setup")]
|
||||||
|
[assembly: System.Reflection.AssemblyVersionAttribute("1.1.5.0")]
|
||||||
|
[assembly: System.Resources.NeutralResourcesLanguageAttribute("de-DE")]
|
||||||
|
|
||||||
|
// Von der MSBuild WriteCodeFragment-Klasse generiert.
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
6fa3eb63bf9e7c9a77cca67d608dff49330e3a93e70e18548b24192d4ef82422
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
is_global = true
|
||||||
|
build_property.ApplicationManifest = app.manifest
|
||||||
|
build_property.StartupObject =
|
||||||
|
build_property.ApplicationDefaultFont =
|
||||||
|
build_property.ApplicationHighDpiMode =
|
||||||
|
build_property.ApplicationUseCompatibleTextRendering =
|
||||||
|
build_property.ApplicationVisualStyles =
|
||||||
|
build_property.RootNamespace = AeroMCSetup
|
||||||
|
build_property.ProjectDir = C:\Users\Viper\Desktop\AeroMC-Launcher\installer\
|
||||||
|
build_property.EnableComHosting =
|
||||||
|
build_property.EnableGeneratedComInterfaceComImportInterop =
|
||||||
|
build_property.CsWinRTUseWindowsUIXamlProjections = false
|
||||||
|
build_property.EffectiveAnalysisLevelStyle =
|
||||||
|
build_property.EnableCodeStyleSeverity =
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// <auto-generated/>
|
||||||
|
global using System;
|
||||||
|
global using System.Collections.Generic;
|
||||||
|
global using System.Linq;
|
||||||
|
global using System.Threading;
|
||||||
|
global using System.Threading.Tasks;
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
75ed8898b0d415db8840bc8e86cbe862583b62e51d945f302629778ca05c0e99
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
C:\Users\Viper\Desktop\AeroMC-Launcher\dist-setup-test\AeroMC-Setup.exe.config
|
||||||
|
C:\Users\Viper\Desktop\AeroMC-Launcher\dist-setup-test\AeroMC-Setup.exe
|
||||||
|
C:\Users\Viper\Desktop\AeroMC-Launcher\dist-setup-test\AeroMC-Setup.pdb
|
||||||
|
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\obj\Release\net48\AeroMCSetup.csproj.AssemblyReference.cache
|
||||||
|
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\obj\Release\net48\MainWindow.baml
|
||||||
|
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\obj\Release\net48\Themes\Colors.Dark.baml
|
||||||
|
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\obj\Release\net48\Themes\Colors.Light.baml
|
||||||
|
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\obj\Release\net48\Themes\Controls.baml
|
||||||
|
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\obj\Release\net48\App.baml
|
||||||
|
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\obj\Release\net48\MainWindow.g.cs
|
||||||
|
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\obj\Release\net48\App.g.cs
|
||||||
|
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\obj\Release\net48\AeroMC-Setup_MarkupCompile.cache
|
||||||
|
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\obj\Release\net48\AeroMC-Setup.g.resources
|
||||||
|
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\obj\Release\net48\AeroMCSetup.GeneratedMSBuildEditorConfig.editorconfig
|
||||||
|
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\obj\Release\net48\AeroMCSetup.AssemblyInfoInputs.cache
|
||||||
|
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\obj\Release\net48\AeroMCSetup.AssemblyInfo.cs
|
||||||
|
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\obj\Release\net48\AeroMCSetup.csproj.CoreCompileInputs.cache
|
||||||
|
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\obj\Release\net48\AeroMC-Setup.exe
|
||||||
|
C:\Users\Viper\Desktop\AeroMC-Launcher\dist\.setup\stub\AeroMC-Setup.exe.config
|
||||||
|
C:\Users\Viper\Desktop\AeroMC-Launcher\dist\.setup\stub\AeroMC-Setup.exe
|
||||||
|
C:\Users\Viper\AppData\Local\Temp\claude\C--Users-Viper-Desktop-AeroMC-Launcher\1859f458-aac9-4e4b-be87-3e40160ebab1\scratchpad\stubtest\AeroMC-Setup.exe.config
|
||||||
|
C:\Users\Viper\AppData\Local\Temp\claude\C--Users-Viper-Desktop-AeroMC-Launcher\1859f458-aac9-4e4b-be87-3e40160ebab1\scratchpad\stubtest\AeroMC-Setup.exe
|
||||||
|
C:\Users\Viper\AppData\Local\Temp\claude\C--Users-Viper-Desktop-AeroMC-Launcher\1859f458-aac9-4e4b-be87-3e40160ebab1\scratchpad\stubtest\AeroMC-Setup.pdb
|
||||||
|
C:\Users\Viper\AppData\Local\Temp\claude\C--Users-Viper-Desktop-AeroMC-Launcher\1859f458-aac9-4e4b-be87-3e40160ebab1\scratchpad\stubtest2\AeroMC-Setup.exe.config
|
||||||
|
C:\Users\Viper\AppData\Local\Temp\claude\C--Users-Viper-Desktop-AeroMC-Launcher\1859f458-aac9-4e4b-be87-3e40160ebab1\scratchpad\stubtest2\AeroMC-Setup.exe
|
||||||
|
C:\Users\Viper\AppData\Local\Temp\claude\C--Users-Viper-Desktop-AeroMC-Launcher\1859f458-aac9-4e4b-be87-3e40160ebab1\scratchpad\stubtest2\AeroMC-Setup.pdb
|
||||||
Binary file not shown.
@@ -0,0 +1,77 @@
|
|||||||
|
#pragma checksum "..\..\..\App.xaml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "85CCB5F529C7F3695DF076BCDBCCA84D8F4ED0DE"
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// <auto-generated>
|
||||||
|
// This code was generated by a tool.
|
||||||
|
//
|
||||||
|
// Changes to this file may cause incorrect behavior and will be lost if
|
||||||
|
// the code is regenerated.
|
||||||
|
// </auto-generated>
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Automation;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Controls.Primitives;
|
||||||
|
using System.Windows.Controls.Ribbon;
|
||||||
|
using System.Windows.Data;
|
||||||
|
using System.Windows.Documents;
|
||||||
|
using System.Windows.Forms.Integration;
|
||||||
|
using System.Windows.Ink;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Markup;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Animation;
|
||||||
|
using System.Windows.Media.Effects;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
using System.Windows.Media.Media3D;
|
||||||
|
using System.Windows.Media.TextFormatting;
|
||||||
|
using System.Windows.Navigation;
|
||||||
|
using System.Windows.Shapes;
|
||||||
|
using System.Windows.Shell;
|
||||||
|
|
||||||
|
|
||||||
|
namespace AeroMCSetup {
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// App
|
||||||
|
/// </summary>
|
||||||
|
public partial class App : System.Windows.Application {
|
||||||
|
|
||||||
|
private bool _contentLoaded;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// InitializeComponent
|
||||||
|
/// </summary>
|
||||||
|
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
|
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.12.0")]
|
||||||
|
public void InitializeComponent() {
|
||||||
|
if (_contentLoaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_contentLoaded = true;
|
||||||
|
System.Uri resourceLocater = new System.Uri("/AeroMC-Setup;component/app.xaml", System.UriKind.Relative);
|
||||||
|
|
||||||
|
#line 1 "..\..\..\App.xaml"
|
||||||
|
System.Windows.Application.LoadComponent(this, resourceLocater);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Application Entry Point.
|
||||||
|
/// </summary>
|
||||||
|
[System.STAThreadAttribute()]
|
||||||
|
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
|
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.12.0")]
|
||||||
|
public static void Main() {
|
||||||
|
AeroMCSetup.App app = new AeroMCSetup.App();
|
||||||
|
app.InitializeComponent();
|
||||||
|
app.Run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Binary file not shown.
@@ -0,0 +1,842 @@
|
|||||||
|
#pragma checksum "..\..\..\MainWindow.xaml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "8CB5D6EC1BE0A9A19A04D9A92B2B0961370843E0"
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// <auto-generated>
|
||||||
|
// This code was generated by a tool.
|
||||||
|
//
|
||||||
|
// Changes to this file may cause incorrect behavior and will be lost if
|
||||||
|
// the code is regenerated.
|
||||||
|
// </auto-generated>
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Automation;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Controls.Primitives;
|
||||||
|
using System.Windows.Controls.Ribbon;
|
||||||
|
using System.Windows.Data;
|
||||||
|
using System.Windows.Documents;
|
||||||
|
using System.Windows.Forms.Integration;
|
||||||
|
using System.Windows.Ink;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Markup;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Animation;
|
||||||
|
using System.Windows.Media.Effects;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
using System.Windows.Media.Media3D;
|
||||||
|
using System.Windows.Media.TextFormatting;
|
||||||
|
using System.Windows.Navigation;
|
||||||
|
using System.Windows.Shapes;
|
||||||
|
using System.Windows.Shell;
|
||||||
|
|
||||||
|
|
||||||
|
namespace AeroMCSetup {
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MainWindow
|
||||||
|
/// </summary>
|
||||||
|
public partial class MainWindow : System.Windows.Window, System.Windows.Markup.IComponentConnector {
|
||||||
|
|
||||||
|
|
||||||
|
#line 140 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock VersionText;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 142 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.StackPanel Steps;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 144 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border Dot0;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 144 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock Num0;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 145 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock Label0;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 147 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border Line0;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 149 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border Dot1;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 149 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock Num1;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 150 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock Label1;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 152 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border Line1;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 154 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border Dot2;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 154 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock Num2;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 155 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock Label2;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 157 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border Line2;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 159 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.StackPanel ImportStepRow;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 160 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border Dot3;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 160 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock Num3;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 161 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock Label3;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 163 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border Line3;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 165 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border Dot4;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 165 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock Num4;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 166 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock Label4;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 179 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock WindowTitle;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 185 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Button CloseButton;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 193 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.StackPanel PageWelcome;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 240 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border UpdateHint;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 245 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock UpdateHintText;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 248 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border RunningHint;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 260 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.DockPanel PageLicense;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 264 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.CheckBox AcceptLicense;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 271 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.StackPanel LicensePanel;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 277 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.StackPanel PageOptions;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 286 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBox DirBox;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 295 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.CheckBox DesktopLinkBox;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 301 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.StackPanel PageImport;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 306 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.StackPanel ImportList;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 313 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.StackPanel PageProgress;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 314 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border SuccessIcon;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 316 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Media.ScaleTransform SuccessScale;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 321 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock ProgressTitle;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 323 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.StackPanel ProgressBarPanel;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 324 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock PercentText;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 326 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border ProgressTrack;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 327 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border ProgressFill;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 330 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock ProgressText;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 332 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.StackPanel DonePanel;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 333 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.CheckBox LaunchBox;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 338 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.StackPanel PageUninstall;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 349 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock DataInfo;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 350 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.CheckBox RemoveDataBox;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 352 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock RemoveDataWarning;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 362 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Button BackButton;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 363 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Button NextButton;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 364 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Button CancelButton;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 368 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Grid DialogOverlay;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 369 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border DialogCard;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 372 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Media.ScaleTransform DialogScale;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 376 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock DialogIcon;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 379 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock DialogTitle;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 382 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock DialogText;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 385 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Button DialogSecondary;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 386 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Button DialogPrimary;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
private bool _contentLoaded;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// InitializeComponent
|
||||||
|
/// </summary>
|
||||||
|
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
|
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.12.0")]
|
||||||
|
public void InitializeComponent() {
|
||||||
|
if (_contentLoaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_contentLoaded = true;
|
||||||
|
System.Uri resourceLocater = new System.Uri("/AeroMC-Setup;component/mainwindow.xaml", System.UriKind.Relative);
|
||||||
|
|
||||||
|
#line 1 "..\..\..\MainWindow.xaml"
|
||||||
|
System.Windows.Application.LoadComponent(this, resourceLocater);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
|
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.12.0")]
|
||||||
|
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes")]
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")]
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")]
|
||||||
|
void System.Windows.Markup.IComponentConnector.Connect(int connectionId, object target) {
|
||||||
|
switch (connectionId)
|
||||||
|
{
|
||||||
|
case 1:
|
||||||
|
this.VersionText = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 2:
|
||||||
|
this.Steps = ((System.Windows.Controls.StackPanel)(target));
|
||||||
|
return;
|
||||||
|
case 3:
|
||||||
|
this.Dot0 = ((System.Windows.Controls.Border)(target));
|
||||||
|
return;
|
||||||
|
case 4:
|
||||||
|
this.Num0 = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 5:
|
||||||
|
this.Label0 = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 6:
|
||||||
|
this.Line0 = ((System.Windows.Controls.Border)(target));
|
||||||
|
return;
|
||||||
|
case 7:
|
||||||
|
this.Dot1 = ((System.Windows.Controls.Border)(target));
|
||||||
|
return;
|
||||||
|
case 8:
|
||||||
|
this.Num1 = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 9:
|
||||||
|
this.Label1 = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 10:
|
||||||
|
this.Line1 = ((System.Windows.Controls.Border)(target));
|
||||||
|
return;
|
||||||
|
case 11:
|
||||||
|
this.Dot2 = ((System.Windows.Controls.Border)(target));
|
||||||
|
return;
|
||||||
|
case 12:
|
||||||
|
this.Num2 = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 13:
|
||||||
|
this.Label2 = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 14:
|
||||||
|
this.Line2 = ((System.Windows.Controls.Border)(target));
|
||||||
|
return;
|
||||||
|
case 15:
|
||||||
|
this.ImportStepRow = ((System.Windows.Controls.StackPanel)(target));
|
||||||
|
return;
|
||||||
|
case 16:
|
||||||
|
this.Dot3 = ((System.Windows.Controls.Border)(target));
|
||||||
|
return;
|
||||||
|
case 17:
|
||||||
|
this.Num3 = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 18:
|
||||||
|
this.Label3 = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 19:
|
||||||
|
this.Line3 = ((System.Windows.Controls.Border)(target));
|
||||||
|
return;
|
||||||
|
case 20:
|
||||||
|
this.Dot4 = ((System.Windows.Controls.Border)(target));
|
||||||
|
return;
|
||||||
|
case 21:
|
||||||
|
this.Num4 = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 22:
|
||||||
|
this.Label4 = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 23:
|
||||||
|
this.WindowTitle = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 24:
|
||||||
|
|
||||||
|
#line 183 "..\..\..\MainWindow.xaml"
|
||||||
|
((System.Windows.Controls.Button)(target)).Click += new System.Windows.RoutedEventHandler(this.Minimize_Click);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
return;
|
||||||
|
case 25:
|
||||||
|
this.CloseButton = ((System.Windows.Controls.Button)(target));
|
||||||
|
|
||||||
|
#line 185 "..\..\..\MainWindow.xaml"
|
||||||
|
this.CloseButton.Click += new System.Windows.RoutedEventHandler(this.Cancel_Click);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
return;
|
||||||
|
case 26:
|
||||||
|
this.PageWelcome = ((System.Windows.Controls.StackPanel)(target));
|
||||||
|
return;
|
||||||
|
case 27:
|
||||||
|
this.UpdateHint = ((System.Windows.Controls.Border)(target));
|
||||||
|
return;
|
||||||
|
case 28:
|
||||||
|
this.UpdateHintText = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 29:
|
||||||
|
this.RunningHint = ((System.Windows.Controls.Border)(target));
|
||||||
|
return;
|
||||||
|
case 30:
|
||||||
|
this.PageLicense = ((System.Windows.Controls.DockPanel)(target));
|
||||||
|
return;
|
||||||
|
case 31:
|
||||||
|
this.AcceptLicense = ((System.Windows.Controls.CheckBox)(target));
|
||||||
|
|
||||||
|
#line 265 "..\..\..\MainWindow.xaml"
|
||||||
|
this.AcceptLicense.Checked += new System.Windows.RoutedEventHandler(this.AcceptLicense_Click);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
#line 265 "..\..\..\MainWindow.xaml"
|
||||||
|
this.AcceptLicense.Unchecked += new System.Windows.RoutedEventHandler(this.AcceptLicense_Click);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
return;
|
||||||
|
case 32:
|
||||||
|
this.LicensePanel = ((System.Windows.Controls.StackPanel)(target));
|
||||||
|
return;
|
||||||
|
case 33:
|
||||||
|
this.PageOptions = ((System.Windows.Controls.StackPanel)(target));
|
||||||
|
return;
|
||||||
|
case 34:
|
||||||
|
|
||||||
|
#line 285 "..\..\..\MainWindow.xaml"
|
||||||
|
((System.Windows.Controls.Button)(target)).Click += new System.Windows.RoutedEventHandler(this.ChangeDir_Click);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
return;
|
||||||
|
case 35:
|
||||||
|
this.DirBox = ((System.Windows.Controls.TextBox)(target));
|
||||||
|
return;
|
||||||
|
case 36:
|
||||||
|
this.DesktopLinkBox = ((System.Windows.Controls.CheckBox)(target));
|
||||||
|
return;
|
||||||
|
case 37:
|
||||||
|
this.PageImport = ((System.Windows.Controls.StackPanel)(target));
|
||||||
|
return;
|
||||||
|
case 38:
|
||||||
|
this.ImportList = ((System.Windows.Controls.StackPanel)(target));
|
||||||
|
return;
|
||||||
|
case 39:
|
||||||
|
this.PageProgress = ((System.Windows.Controls.StackPanel)(target));
|
||||||
|
return;
|
||||||
|
case 40:
|
||||||
|
this.SuccessIcon = ((System.Windows.Controls.Border)(target));
|
||||||
|
return;
|
||||||
|
case 41:
|
||||||
|
this.SuccessScale = ((System.Windows.Media.ScaleTransform)(target));
|
||||||
|
return;
|
||||||
|
case 42:
|
||||||
|
this.ProgressTitle = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 43:
|
||||||
|
this.ProgressBarPanel = ((System.Windows.Controls.StackPanel)(target));
|
||||||
|
return;
|
||||||
|
case 44:
|
||||||
|
this.PercentText = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 45:
|
||||||
|
this.ProgressTrack = ((System.Windows.Controls.Border)(target));
|
||||||
|
|
||||||
|
#line 326 "..\..\..\MainWindow.xaml"
|
||||||
|
this.ProgressTrack.SizeChanged += new System.Windows.SizeChangedEventHandler(this.ProgressTrack_SizeChanged);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
return;
|
||||||
|
case 46:
|
||||||
|
this.ProgressFill = ((System.Windows.Controls.Border)(target));
|
||||||
|
return;
|
||||||
|
case 47:
|
||||||
|
this.ProgressText = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 48:
|
||||||
|
this.DonePanel = ((System.Windows.Controls.StackPanel)(target));
|
||||||
|
return;
|
||||||
|
case 49:
|
||||||
|
this.LaunchBox = ((System.Windows.Controls.CheckBox)(target));
|
||||||
|
return;
|
||||||
|
case 50:
|
||||||
|
this.PageUninstall = ((System.Windows.Controls.StackPanel)(target));
|
||||||
|
return;
|
||||||
|
case 51:
|
||||||
|
this.DataInfo = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 52:
|
||||||
|
this.RemoveDataBox = ((System.Windows.Controls.CheckBox)(target));
|
||||||
|
|
||||||
|
#line 351 "..\..\..\MainWindow.xaml"
|
||||||
|
this.RemoveDataBox.Checked += new System.Windows.RoutedEventHandler(this.RemoveDataBox_Click);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
#line 351 "..\..\..\MainWindow.xaml"
|
||||||
|
this.RemoveDataBox.Unchecked += new System.Windows.RoutedEventHandler(this.RemoveDataBox_Click);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
return;
|
||||||
|
case 53:
|
||||||
|
this.RemoveDataWarning = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 54:
|
||||||
|
this.BackButton = ((System.Windows.Controls.Button)(target));
|
||||||
|
|
||||||
|
#line 362 "..\..\..\MainWindow.xaml"
|
||||||
|
this.BackButton.Click += new System.Windows.RoutedEventHandler(this.Back_Click);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
return;
|
||||||
|
case 55:
|
||||||
|
this.NextButton = ((System.Windows.Controls.Button)(target));
|
||||||
|
|
||||||
|
#line 363 "..\..\..\MainWindow.xaml"
|
||||||
|
this.NextButton.Click += new System.Windows.RoutedEventHandler(this.Next_Click);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
return;
|
||||||
|
case 56:
|
||||||
|
this.CancelButton = ((System.Windows.Controls.Button)(target));
|
||||||
|
|
||||||
|
#line 364 "..\..\..\MainWindow.xaml"
|
||||||
|
this.CancelButton.Click += new System.Windows.RoutedEventHandler(this.Cancel_Click);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
return;
|
||||||
|
case 57:
|
||||||
|
this.DialogOverlay = ((System.Windows.Controls.Grid)(target));
|
||||||
|
return;
|
||||||
|
case 58:
|
||||||
|
this.DialogCard = ((System.Windows.Controls.Border)(target));
|
||||||
|
return;
|
||||||
|
case 59:
|
||||||
|
this.DialogScale = ((System.Windows.Media.ScaleTransform)(target));
|
||||||
|
return;
|
||||||
|
case 60:
|
||||||
|
this.DialogIcon = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 61:
|
||||||
|
this.DialogTitle = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 62:
|
||||||
|
this.DialogText = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 63:
|
||||||
|
this.DialogSecondary = ((System.Windows.Controls.Button)(target));
|
||||||
|
|
||||||
|
#line 385 "..\..\..\MainWindow.xaml"
|
||||||
|
this.DialogSecondary.Click += new System.Windows.RoutedEventHandler(this.DialogSecondary_Click);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
return;
|
||||||
|
case 64:
|
||||||
|
this.DialogPrimary = ((System.Windows.Controls.Button)(target));
|
||||||
|
|
||||||
|
#line 386 "..\..\..\MainWindow.xaml"
|
||||||
|
this.DialogPrimary.Click += new System.Windows.RoutedEventHandler(this.DialogPrimary_Click);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._contentLoaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,471 @@
|
|||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"targets": {
|
||||||
|
".NETFramework,Version=v4.8": {
|
||||||
|
"Microsoft.NETFramework.ReferenceAssemblies/1.0.3": {
|
||||||
|
"type": "package",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.NETFramework.ReferenceAssemblies.net48": "1.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.NETFramework.ReferenceAssemblies.net48/1.0.3": {
|
||||||
|
"type": "package",
|
||||||
|
"build": {
|
||||||
|
"build/Microsoft.NETFramework.ReferenceAssemblies.net48.targets": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"libraries": {
|
||||||
|
"Microsoft.NETFramework.ReferenceAssemblies/1.0.3": {
|
||||||
|
"sha512": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
|
||||||
|
"type": "package",
|
||||||
|
"path": "microsoft.netframework.referenceassemblies/1.0.3",
|
||||||
|
"files": [
|
||||||
|
".nupkg.metadata",
|
||||||
|
".signature.p7s",
|
||||||
|
"microsoft.netframework.referenceassemblies.1.0.3.nupkg.sha512",
|
||||||
|
"microsoft.netframework.referenceassemblies.nuspec"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Microsoft.NETFramework.ReferenceAssemblies.net48/1.0.3": {
|
||||||
|
"sha512": "zMk4D+9zyiEWByyQ7oPImPN/Jhpj166Ky0Nlla4eXlNL8hI/BtSJsgR8Inldd4NNpIAH3oh8yym0W2DrhXdSLQ==",
|
||||||
|
"type": "package",
|
||||||
|
"path": "microsoft.netframework.referenceassemblies.net48/1.0.3",
|
||||||
|
"files": [
|
||||||
|
".nupkg.metadata",
|
||||||
|
".signature.p7s",
|
||||||
|
"build/.NETFramework/v4.8/Accessibility.dll",
|
||||||
|
"build/.NETFramework/v4.8/Accessibility.xml",
|
||||||
|
"build/.NETFramework/v4.8/CustomMarshalers.dll",
|
||||||
|
"build/.NETFramework/v4.8/CustomMarshalers.xml",
|
||||||
|
"build/.NETFramework/v4.8/Facades/Microsoft.Win32.Primitives.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.AppContext.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Collections.Concurrent.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Collections.NonGeneric.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Collections.Specialized.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Collections.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.ComponentModel.Annotations.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.ComponentModel.EventBasedAsync.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.ComponentModel.Primitives.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.ComponentModel.TypeConverter.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.ComponentModel.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Console.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Data.Common.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Diagnostics.Contracts.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Diagnostics.Debug.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Diagnostics.FileVersionInfo.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Diagnostics.Process.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Diagnostics.StackTrace.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Diagnostics.TextWriterTraceListener.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Diagnostics.Tools.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Diagnostics.TraceSource.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Drawing.Primitives.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Dynamic.Runtime.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Globalization.Calendars.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Globalization.Extensions.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Globalization.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.IO.Compression.ZipFile.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.IO.FileSystem.DriveInfo.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.IO.FileSystem.Primitives.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.IO.FileSystem.Watcher.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.IO.FileSystem.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.IO.IsolatedStorage.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.IO.MemoryMappedFiles.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.IO.Pipes.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.IO.UnmanagedMemoryStream.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.IO.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Linq.Expressions.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Linq.Parallel.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Linq.Queryable.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Linq.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Net.Http.Rtc.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Net.NameResolution.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Net.NetworkInformation.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Net.Ping.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Net.Primitives.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Net.Requests.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Net.Security.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Net.Sockets.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Net.WebHeaderCollection.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Net.WebSockets.Client.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Net.WebSockets.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.ObjectModel.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Reflection.Emit.ILGeneration.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Reflection.Emit.Lightweight.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Reflection.Emit.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Reflection.Extensions.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Reflection.Primitives.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Reflection.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Resources.Reader.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Resources.ResourceManager.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Resources.Writer.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Runtime.CompilerServices.VisualC.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Runtime.Extensions.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Runtime.Handles.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Runtime.InteropServices.RuntimeInformation.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Runtime.InteropServices.WindowsRuntime.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Runtime.InteropServices.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Runtime.Numerics.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Runtime.Serialization.Formatters.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Runtime.Serialization.Json.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Runtime.Serialization.Primitives.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Runtime.Serialization.Xml.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Runtime.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Security.Claims.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Security.Cryptography.Algorithms.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Security.Cryptography.Csp.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Security.Cryptography.Encoding.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Security.Cryptography.Primitives.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Security.Cryptography.X509Certificates.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Security.Principal.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Security.SecureString.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.ServiceModel.Duplex.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.ServiceModel.Http.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.ServiceModel.NetTcp.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.ServiceModel.Primitives.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.ServiceModel.Security.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Text.Encoding.Extensions.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Text.Encoding.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Text.RegularExpressions.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Threading.Overlapped.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Threading.Tasks.Parallel.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Threading.Tasks.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Threading.Thread.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Threading.ThreadPool.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Threading.Timer.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Threading.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.ValueTuple.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Xml.ReaderWriter.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Xml.XDocument.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Xml.XPath.XDocument.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Xml.XPath.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Xml.XmlDocument.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Xml.XmlSerializer.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/netstandard.dll",
|
||||||
|
"build/.NETFramework/v4.8/ISymWrapper.dll",
|
||||||
|
"build/.NETFramework/v4.8/ISymWrapper.xml",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.Activities.Build.dll",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.Activities.Build.xml",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.Build.Conversion.v4.0.dll",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.Build.Conversion.v4.0.xml",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.Build.Engine.dll",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.Build.Engine.xml",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.Build.Framework.dll",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.Build.Framework.xml",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.Build.Tasks.v4.0.dll",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.Build.Tasks.v4.0.xml",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.Build.Utilities.v4.0.dll",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.Build.Utilities.v4.0.xml",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.Build.dll",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.Build.xml",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.CSharp.dll",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.CSharp.xml",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.JScript.dll",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.JScript.xml",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.VisualBasic.Compatibility.Data.dll",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.VisualBasic.Compatibility.Data.xml",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.VisualBasic.Compatibility.dll",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.VisualBasic.Compatibility.xml",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.VisualBasic.dll",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.VisualBasic.xml",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.VisualC.STLCLR.dll",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.VisualC.STLCLR.xml",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.VisualC.dll",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.VisualC.xml",
|
||||||
|
"build/.NETFramework/v4.8/PermissionSets/FullTrust.xml",
|
||||||
|
"build/.NETFramework/v4.8/PermissionSets/Internet.xml",
|
||||||
|
"build/.NETFramework/v4.8/PermissionSets/LocalIntranet.xml",
|
||||||
|
"build/.NETFramework/v4.8/PresentationBuildTasks.dll",
|
||||||
|
"build/.NETFramework/v4.8/PresentationBuildTasks.xml",
|
||||||
|
"build/.NETFramework/v4.8/PresentationCore.dll",
|
||||||
|
"build/.NETFramework/v4.8/PresentationCore.xml",
|
||||||
|
"build/.NETFramework/v4.8/PresentationFramework.Aero.dll",
|
||||||
|
"build/.NETFramework/v4.8/PresentationFramework.Aero.xml",
|
||||||
|
"build/.NETFramework/v4.8/PresentationFramework.Aero2.dll",
|
||||||
|
"build/.NETFramework/v4.8/PresentationFramework.Aero2.xml",
|
||||||
|
"build/.NETFramework/v4.8/PresentationFramework.AeroLite.dll",
|
||||||
|
"build/.NETFramework/v4.8/PresentationFramework.AeroLite.xml",
|
||||||
|
"build/.NETFramework/v4.8/PresentationFramework.Classic.dll",
|
||||||
|
"build/.NETFramework/v4.8/PresentationFramework.Classic.xml",
|
||||||
|
"build/.NETFramework/v4.8/PresentationFramework.Luna.dll",
|
||||||
|
"build/.NETFramework/v4.8/PresentationFramework.Luna.xml",
|
||||||
|
"build/.NETFramework/v4.8/PresentationFramework.Royale.dll",
|
||||||
|
"build/.NETFramework/v4.8/PresentationFramework.Royale.xml",
|
||||||
|
"build/.NETFramework/v4.8/PresentationFramework.dll",
|
||||||
|
"build/.NETFramework/v4.8/PresentationFramework.xml",
|
||||||
|
"build/.NETFramework/v4.8/ReachFramework.dll",
|
||||||
|
"build/.NETFramework/v4.8/ReachFramework.xml",
|
||||||
|
"build/.NETFramework/v4.8/RedistList/FrameworkList.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Activities.Core.Presentation.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Activities.Core.Presentation.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Activities.DurableInstancing.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Activities.DurableInstancing.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Activities.Presentation.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Activities.Presentation.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Activities.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Activities.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.AddIn.Contract.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.AddIn.Contract.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.AddIn.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.AddIn.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.ComponentModel.Composition.Registration.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.ComponentModel.Composition.Registration.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.ComponentModel.Composition.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.ComponentModel.Composition.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.ComponentModel.DataAnnotations.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.ComponentModel.DataAnnotations.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Configuration.Install.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Configuration.Install.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Configuration.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Configuration.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Core.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Core.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.DataSetExtensions.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.DataSetExtensions.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.Entity.Design.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.Entity.Design.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.Entity.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.Entity.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.Linq.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.Linq.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.OracleClient.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.OracleClient.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.Services.Client.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.Services.Client.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.Services.Design.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.Services.Design.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.Services.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.Services.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.SqlXml.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.SqlXml.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Deployment.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Deployment.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Design.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Design.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Device.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Device.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Diagnostics.Tracing.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Diagnostics.Tracing.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.DirectoryServices.AccountManagement.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.DirectoryServices.AccountManagement.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.DirectoryServices.Protocols.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.DirectoryServices.Protocols.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.DirectoryServices.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.DirectoryServices.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Drawing.Design.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Drawing.Design.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Drawing.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Drawing.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Dynamic.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.EnterpriseServices.Thunk.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.EnterpriseServices.Wrapper.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.EnterpriseServices.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.EnterpriseServices.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.IO.Compression.FileSystem.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.IO.Compression.FileSystem.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.IO.Compression.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.IO.Compression.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.IO.Log.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.IO.Log.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.IdentityModel.Selectors.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.IdentityModel.Selectors.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.IdentityModel.Services.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.IdentityModel.Services.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.IdentityModel.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.IdentityModel.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Linq.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Management.Instrumentation.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Management.Instrumentation.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Management.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Management.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Messaging.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Messaging.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Net.Http.WebRequest.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Net.Http.WebRequest.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Net.Http.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Net.Http.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Net.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Net.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Numerics.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Numerics.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Printing.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Printing.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Reflection.Context.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Reflection.Context.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Runtime.Caching.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Runtime.Caching.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Runtime.DurableInstancing.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Runtime.DurableInstancing.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Runtime.Remoting.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Runtime.Remoting.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Runtime.Serialization.Formatters.Soap.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Runtime.Serialization.Formatters.Soap.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Runtime.Serialization.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Runtime.Serialization.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Security.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Security.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.ServiceModel.Activation.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.ServiceModel.Activation.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.ServiceModel.Activities.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.ServiceModel.Activities.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.ServiceModel.Channels.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.ServiceModel.Channels.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.ServiceModel.Discovery.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.ServiceModel.Discovery.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.ServiceModel.Routing.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.ServiceModel.Routing.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.ServiceModel.Web.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.ServiceModel.Web.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.ServiceModel.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.ServiceModel.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.ServiceProcess.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.ServiceProcess.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Speech.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Speech.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Threading.Tasks.Dataflow.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Transactions.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Transactions.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.Abstractions.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.ApplicationServices.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.ApplicationServices.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.DataVisualization.Design.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.DataVisualization.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.DataVisualization.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.DynamicData.Design.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.DynamicData.Design.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.DynamicData.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.DynamicData.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.Entity.Design.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.Entity.Design.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.Entity.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.Entity.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.Extensions.Design.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.Extensions.Design.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.Extensions.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.Extensions.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.Mobile.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.Mobile.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.RegularExpressions.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.RegularExpressions.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.Routing.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.Services.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.Services.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Windows.Controls.Ribbon.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Windows.Controls.Ribbon.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Windows.Forms.DataVisualization.Design.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Windows.Forms.DataVisualization.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Windows.Forms.DataVisualization.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Windows.Forms.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Windows.Forms.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Windows.Input.Manipulations.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Windows.Input.Manipulations.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Windows.Presentation.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Windows.Presentation.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Windows.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Workflow.Activities.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Workflow.Activities.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Workflow.ComponentModel.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Workflow.ComponentModel.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Workflow.Runtime.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Workflow.Runtime.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.WorkflowServices.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.WorkflowServices.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Xaml.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Xaml.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Xml.Linq.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Xml.Linq.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Xml.Serialization.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Xml.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Xml.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.xml",
|
||||||
|
"build/.NETFramework/v4.8/UIAutomationClient.dll",
|
||||||
|
"build/.NETFramework/v4.8/UIAutomationClient.xml",
|
||||||
|
"build/.NETFramework/v4.8/UIAutomationClientsideProviders.dll",
|
||||||
|
"build/.NETFramework/v4.8/UIAutomationClientsideProviders.xml",
|
||||||
|
"build/.NETFramework/v4.8/UIAutomationProvider.dll",
|
||||||
|
"build/.NETFramework/v4.8/UIAutomationProvider.xml",
|
||||||
|
"build/.NETFramework/v4.8/UIAutomationTypes.dll",
|
||||||
|
"build/.NETFramework/v4.8/UIAutomationTypes.xml",
|
||||||
|
"build/.NETFramework/v4.8/WindowsBase.dll",
|
||||||
|
"build/.NETFramework/v4.8/WindowsBase.xml",
|
||||||
|
"build/.NETFramework/v4.8/WindowsFormsIntegration.dll",
|
||||||
|
"build/.NETFramework/v4.8/WindowsFormsIntegration.xml",
|
||||||
|
"build/.NETFramework/v4.8/XamlBuildTask.dll",
|
||||||
|
"build/.NETFramework/v4.8/XamlBuildTask.xml",
|
||||||
|
"build/.NETFramework/v4.8/mscorlib.dll",
|
||||||
|
"build/.NETFramework/v4.8/mscorlib.xml",
|
||||||
|
"build/.NETFramework/v4.8/namespaces.xml",
|
||||||
|
"build/.NETFramework/v4.8/sysglobl.dll",
|
||||||
|
"build/.NETFramework/v4.8/sysglobl.xml",
|
||||||
|
"build/Microsoft.NETFramework.ReferenceAssemblies.net48.targets",
|
||||||
|
"microsoft.netframework.referenceassemblies.net48.1.0.3.nupkg.sha512",
|
||||||
|
"microsoft.netframework.referenceassemblies.net48.nuspec"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"projectFileDependencyGroups": {
|
||||||
|
".NETFramework,Version=v4.8": [
|
||||||
|
"Microsoft.NETFramework.ReferenceAssemblies >= 1.0.3"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"packageFolders": {
|
||||||
|
"C:\\Users\\Viper\\.nuget\\packages\\": {}
|
||||||
|
},
|
||||||
|
"project": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"restore": {
|
||||||
|
"projectUniqueName": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj",
|
||||||
|
"projectName": "AeroMC-Setup",
|
||||||
|
"projectPath": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj",
|
||||||
|
"packagesPath": "C:\\Users\\Viper\\.nuget\\packages\\",
|
||||||
|
"outputPath": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\obj\\",
|
||||||
|
"projectStyle": "PackageReference",
|
||||||
|
"configFilePaths": [
|
||||||
|
"C:\\Users\\Viper\\AppData\\Roaming\\NuGet\\NuGet.Config"
|
||||||
|
],
|
||||||
|
"originalTargetFrameworks": [
|
||||||
|
"net48"
|
||||||
|
],
|
||||||
|
"sources": {
|
||||||
|
"https://api.nuget.org/v3/index.json": {}
|
||||||
|
},
|
||||||
|
"frameworks": {
|
||||||
|
"net48": {
|
||||||
|
"targetAlias": "net48",
|
||||||
|
"projectReferences": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"warningProperties": {
|
||||||
|
"warnAsError": [
|
||||||
|
"NU1605"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"restoreAuditProperties": {
|
||||||
|
"enableAudit": "true",
|
||||||
|
"auditLevel": "low",
|
||||||
|
"auditMode": "direct"
|
||||||
|
},
|
||||||
|
"SdkAnalysisLevel": "10.0.100"
|
||||||
|
},
|
||||||
|
"frameworks": {
|
||||||
|
"net48": {
|
||||||
|
"targetAlias": "net48",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.NETFramework.ReferenceAssemblies": {
|
||||||
|
"suppressParent": "All",
|
||||||
|
"target": "Package",
|
||||||
|
"version": "[1.0.3, )",
|
||||||
|
"autoReferenced": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.112\\RuntimeIdentifierGraph.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"dgSpecHash": "za8GIOA75zI=",
|
||||||
|
"success": true,
|
||||||
|
"projectFilePath": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj",
|
||||||
|
"expectedPackageFiles": [
|
||||||
|
"C:\\Users\\Viper\\.nuget\\packages\\microsoft.netframework.referenceassemblies\\1.0.3\\microsoft.netframework.referenceassemblies.1.0.3.nupkg.sha512",
|
||||||
|
"C:\\Users\\Viper\\.nuget\\packages\\microsoft.netframework.referenceassemblies.net48\\1.0.3\\microsoft.netframework.referenceassemblies.net48.1.0.3.nupkg.sha512"
|
||||||
|
],
|
||||||
|
"logs": []
|
||||||
|
}
|
||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "aeromc",
|
"name": "aeromc",
|
||||||
"version": "1.1.3",
|
"version": "1.1.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "aeromc",
|
"name": "aeromc",
|
||||||
"version": "1.1.3",
|
"version": "1.1.5",
|
||||||
"license": "SEE LICENSE IN LICENSE",
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@xhayper/discord-rpc": "^1.3.4",
|
"@xhayper/discord-rpc": "^1.3.4",
|
||||||
|
|||||||
+3
-7
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "aeromc",
|
"name": "aeromc",
|
||||||
"productName": "AeroMC",
|
"productName": "AeroMC",
|
||||||
"version": "1.1.3",
|
"version": "1.1.5",
|
||||||
"description": "AeroMC – Instanz-basierter Minecraft-Launcher (à la MultiMC/Prism)",
|
"description": "AeroMC – Instanz-basierter Minecraft-Launcher (à la MultiMC/Prism)",
|
||||||
"homepage": "https://m-viper.de",
|
"homepage": "https://m-viper.de",
|
||||||
"launcherWebsite": "https://aeromc.viper.ipv64.net",
|
"launcherWebsite": "https://aeromc.viper.ipv64.net",
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"start": "electron .",
|
"start": "electron .",
|
||||||
"prebuild": "node scripts/write-build-config.js",
|
"prebuild": "node scripts/write-build-config.js",
|
||||||
"prebuild:portable": "node scripts/write-build-config.js",
|
"prebuild:portable": "node scripts/write-build-config.js",
|
||||||
"build": "electron-builder --win nsis --x64 && node -e \"const fs=require('fs');const path=require('path');['dist/builder-debug.yml','dist/builder-effective-config.yaml','dist/win-unpacked'].forEach((target)=>fs.rmSync(target,{recursive:true,force:true}));if(fs.existsSync('dist')){const source=path.join('dist','AeroMC.exe');const target=path.join('dist','AeroMC-Setup.exe');if(fs.existsSync(target))fs.rmSync(target,{force:true});if(fs.existsSync(source))fs.renameSync(source,target);for(const name of fs.readdirSync('dist')){if((name.endsWith('.exe')&&name!=='AeroMC-Setup.exe'&&name!=='AeroMC-Portable.exe')||name.endsWith('.blockmap')){fs.rmSync(path.join('dist',name),{force:true})}}}\"",
|
"build": "electron-builder --win dir && node scripts/build-setup.js",
|
||||||
"build:portable": "electron-builder --win portable --x64 && node -e \"const fs=require('fs');const path=require('path');['dist/builder-debug.yml','dist/builder-effective-config.yaml','dist/win-unpacked'].forEach((target)=>fs.rmSync(target,{recursive:true,force:true}));if(fs.existsSync('dist')){const source=path.join('dist','AeroMC.exe');const target=path.join('dist','AeroMC-Portable.exe');if(fs.existsSync(target))fs.rmSync(target,{force:true});if(fs.existsSync(source))fs.renameSync(source,target);for(const name of fs.readdirSync('dist')){if((name.endsWith('.exe')&&name!=='AeroMC-Setup.exe'&&name!=='AeroMC-Portable.exe')||name.endsWith('.blockmap')){fs.rmSync(path.join('dist',name),{force:true})}}}\""
|
"build:portable": "electron-builder --win portable --x64 && node -e \"const fs=require('fs');const path=require('path');['dist/builder-debug.yml','dist/builder-effective-config.yaml','dist/win-unpacked'].forEach((target)=>fs.rmSync(target,{recursive:true,force:true}));if(fs.existsSync('dist')){const source=path.join('dist','AeroMC.exe');const target=path.join('dist','AeroMC-Portable.exe');if(fs.existsSync(target))fs.rmSync(target,{force:true});if(fs.existsSync(source))fs.renameSync(source,target);for(const name of fs.readdirSync('dist')){if((name.endsWith('.exe')&&name!=='AeroMC-Setup.exe'&&name!=='AeroMC-Portable.exe')||name.endsWith('.blockmap')){fs.rmSync(path.join('dist',name),{force:true})}}}\""
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
@@ -32,12 +32,8 @@
|
|||||||
"node_modules/ffmpeg-static/**/*"
|
"node_modules/ffmpeg-static/**/*"
|
||||||
],
|
],
|
||||||
"win": {
|
"win": {
|
||||||
"target": "nsis",
|
"target": "dir",
|
||||||
"icon": "src/assets/icon.ico"
|
"icon": "src/assets/icon.ico"
|
||||||
},
|
|
||||||
"nsis": {
|
|
||||||
"oneClick": false,
|
|
||||||
"allowToChangeInstallationDirectory": true
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* build-setup.js – eigenes Setup (installer/) mit der App zusammenbauen
|
||||||
|
* ---------------------------------------------------------------------
|
||||||
|
* Läuft nach "electron-builder --win dir" (siehe "build" in package.json):
|
||||||
|
*
|
||||||
|
* 1. dist/win-unpacked (die fertige App) als ZIP packen
|
||||||
|
* 2. installer/ (WPF, .NET Framework 4.8) mit der Version aus package.json bauen
|
||||||
|
* 3. ZIP hinten an die Setup-.exe hängen:
|
||||||
|
* [Setup.exe][ZIP][8 Byte ZIP-Länge][8 Byte "AEROMCPK"]
|
||||||
|
* (das Setup liest den Anhang selbst aus, siehe installer/Payload.cs)
|
||||||
|
* 4. Zwischenstände wegräumen → übrig bleibt dist/AeroMC-Setup.exe
|
||||||
|
*
|
||||||
|
* Benötigt das .NET SDK (dotnet) – das Setup selbst läuft danach auf jedem
|
||||||
|
* Windows 10/11 ohne Zusatzinstallation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { execFileSync } = require('child_process');
|
||||||
|
|
||||||
|
const ROOT = path.join(__dirname, '..');
|
||||||
|
const DIST = path.join(ROOT, 'dist');
|
||||||
|
const APP_DIR = path.join(DIST, 'win-unpacked');
|
||||||
|
const WORK = path.join(DIST, '.setup');
|
||||||
|
const ZIP = path.join(WORK, 'app.zip');
|
||||||
|
const OUT = path.join(DIST, 'AeroMC-Setup.exe');
|
||||||
|
const MAGIC = Buffer.from('AEROMCPK', 'ascii');
|
||||||
|
|
||||||
|
function schritt(text) {
|
||||||
|
console.log(`\n» ${text}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const version = require(path.join(ROOT, 'package.json')).version;
|
||||||
|
if (!fs.existsSync(path.join(APP_DIR, 'AeroMC.exe'))) {
|
||||||
|
throw new Error(`${APP_DIR}\\AeroMC.exe fehlt – vorher "electron-builder --win dir" ausführen.`);
|
||||||
|
}
|
||||||
|
fs.rmSync(WORK, { recursive: true, force: true });
|
||||||
|
fs.mkdirSync(WORK, { recursive: true });
|
||||||
|
|
||||||
|
schritt('App wird gepackt …');
|
||||||
|
// .NET-ZIP über PowerShell: in jedem Windows enthalten, keine zusätzliche npm-Abhängigkeit
|
||||||
|
const ps = [
|
||||||
|
'Add-Type -AssemblyName System.IO.Compression.FileSystem',
|
||||||
|
`[System.IO.Compression.ZipFile]::CreateFromDirectory('${APP_DIR.replace(/'/g, "''")}', '${ZIP.replace(/'/g, "''")}', [System.IO.Compression.CompressionLevel]::Optimal, $false)`,
|
||||||
|
].join('; ');
|
||||||
|
execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', ps], { stdio: 'inherit' });
|
||||||
|
|
||||||
|
schritt(`Setup ${version} wird gebaut …`);
|
||||||
|
const stubDir = path.join(WORK, 'stub');
|
||||||
|
execFileSync('dotnet', [
|
||||||
|
'build', path.join(ROOT, 'installer', 'AeroMCSetup.csproj'),
|
||||||
|
'-c', 'Release', `-p:Version=${version}`, '-p:DebugType=none',
|
||||||
|
'--nologo', '-v', 'q', '-o', stubDir,
|
||||||
|
], { stdio: 'inherit' });
|
||||||
|
const stub = path.join(stubDir, 'AeroMC-Setup.exe');
|
||||||
|
if (!fs.existsSync(stub)) throw new Error('Build des Setups fehlgeschlagen.');
|
||||||
|
|
||||||
|
schritt('App wird ans Setup angehängt …');
|
||||||
|
const zipSize = fs.statSync(ZIP).size;
|
||||||
|
const footer = Buffer.alloc(16);
|
||||||
|
footer.writeBigInt64LE(BigInt(zipSize), 0);
|
||||||
|
MAGIC.copy(footer, 8);
|
||||||
|
fs.rmSync(OUT, { force: true });
|
||||||
|
fs.copyFileSync(stub, OUT);
|
||||||
|
// ZIP in Blöcken anhängen statt komplett in den Speicher zu laden
|
||||||
|
const fd = fs.openSync(OUT, 'a');
|
||||||
|
try {
|
||||||
|
const src = fs.openSync(ZIP, 'r');
|
||||||
|
try {
|
||||||
|
const buf = Buffer.alloc(8 * 1024 * 1024);
|
||||||
|
let n;
|
||||||
|
while ((n = fs.readSync(src, buf, 0, buf.length, null)) > 0) fs.writeSync(fd, buf, 0, n);
|
||||||
|
} finally { fs.closeSync(src); }
|
||||||
|
fs.writeSync(fd, footer);
|
||||||
|
} finally { fs.closeSync(fd); }
|
||||||
|
|
||||||
|
schritt('Aufräumen …');
|
||||||
|
for (const t of [WORK, APP_DIR, path.join(DIST, 'builder-debug.yml'), path.join(DIST, 'builder-effective-config.yaml')]) {
|
||||||
|
fs.rmSync(t, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const mb = (fs.statSync(OUT).size / 1024 / 1024).toFixed(1);
|
||||||
|
console.log(`\nFertig: ${OUT} (${mb} MB)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
main();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`\nFehler: ${err.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* crashhelp.js – Absturz-Assistent
|
||||||
|
* ---------------------------------
|
||||||
|
* Liest Absturzbericht, latest.log und die letzte Konsolenausgabe eines Starts
|
||||||
|
* und erkennt die häufigsten Ursachen. Jeder Fund bekommt eine verständliche
|
||||||
|
* Erklärung und – wo möglich – eine Aktion, die der Launcher mit einem Klick
|
||||||
|
* ausführen kann:
|
||||||
|
*
|
||||||
|
* { typ: 'mod-installieren', projekt, name } fehlende Abhängigkeit von Modrinth holen
|
||||||
|
* { typ: 'mod-deaktivieren', dateiname, name } Mod abschalten (umbenennen, nichts gelöscht)
|
||||||
|
* { typ: 'ram', mb } Arbeitsspeicher der Instanz ändern
|
||||||
|
* { typ: 'java-auto' } passendes Java automatisch wählen lassen
|
||||||
|
* { typ: 'mods-ordner' } Mods-Ordner öffnen
|
||||||
|
*
|
||||||
|
* Bewusst nur Muster, die eindeutig sind – lieber keinen Fund als einen falschen.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Mod-IDs, die keine installierbaren Mods sind
|
||||||
|
const KEINE_MODS = new Set(['minecraft', 'java', 'fabricloader', 'fabric-loader', 'forge', 'neoforge', 'quilt_loader', 'quilt-loader', 'fml', 'mixinextras']);
|
||||||
|
|
||||||
|
// Mod-ID -> Modrinth-Projekt, wo sich beides unterscheidet
|
||||||
|
const PROJEKT_FUER_ID = {
|
||||||
|
fabric: 'fabric-api',
|
||||||
|
'fabric-api-base': 'fabric-api',
|
||||||
|
quilted_fabric_api: 'qsl',
|
||||||
|
cloth_config: 'cloth-config',
|
||||||
|
'cloth-config2': 'cloth-config',
|
||||||
|
architectury: 'architectury-api',
|
||||||
|
geckolib3: 'geckolib',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Java-Hauptversion aus der "class file version" (52 = Java 8, 61 = 17, 65 = 21 …)
|
||||||
|
const javaAusKlassenversion = (v) => Number(v) - 44;
|
||||||
|
|
||||||
|
function projektFuer(modId) {
|
||||||
|
const id = String(modId || '').toLowerCase();
|
||||||
|
return PROJEKT_FUER_ID[id] || id.replace(/_/g, '-');
|
||||||
|
}
|
||||||
|
|
||||||
|
// installierte Mod zu einer ID/einem Namen finden (für "abschalten")
|
||||||
|
function findeMod(mods, modId, name) {
|
||||||
|
const id = String(modId || '').toLowerCase();
|
||||||
|
const kurz = id.replace(/[-_]/g, '');
|
||||||
|
const n = String(name || '').toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||||
|
return (mods || []).find((m) => {
|
||||||
|
if (m.enabled === false) return false;
|
||||||
|
const datei = String(m.filename || '').toLowerCase();
|
||||||
|
const titel = String(m.title || '').toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||||
|
return (id && (datei.startsWith(id) || datei.replace(/[-_]/g, '').startsWith(kurz)))
|
||||||
|
|| (n && titel && (titel === n || datei.replace(/[^a-z0-9]/g, '').startsWith(n)));
|
||||||
|
}) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* text: zusammengeführter Text aus Bericht, Log und Konsole
|
||||||
|
* ctx: { loader, version, maxMemMb, gesamtMb, mods: [{ filename, title, projectId, enabled }] }
|
||||||
|
* -> [{ schluessel, titel, text, aktion? }]
|
||||||
|
*/
|
||||||
|
function analysiere(text, ctx = {}) {
|
||||||
|
const t = String(text || '');
|
||||||
|
const funde = [];
|
||||||
|
const schonDa = new Set();
|
||||||
|
const fund = (schluessel, titel, erklaerung, aktion) => {
|
||||||
|
if (schonDa.has(schluessel)) return;
|
||||||
|
schonDa.add(schluessel);
|
||||||
|
funde.push({ schluessel, titel, text: erklaerung, aktion: aktion || null });
|
||||||
|
};
|
||||||
|
const mods = ctx.mods || [];
|
||||||
|
|
||||||
|
// --- Fehlende Abhängigkeiten (Fabric/Quilt) ---
|
||||||
|
// "Mod 'Mod Menu' (modmenu) 7.2.2 requires any version of fabric-api, which is missing!"
|
||||||
|
// neuere Fassung: "... requires any version of mod 'Fabric API' (fabric-api), which is missing!"
|
||||||
|
for (const m of t.matchAll(/Mod '([^']+)' \(([\w.-]+)\)[^\n]*? requires (?:version ([^\n,]+?)|any version) of (?:mod '([^']+)' \(([\w.-]+)\)|([\w.-]+)), which is missing/gi)) {
|
||||||
|
const [, name, , version, fehltName, fehltIdNeu, fehltIdAlt] = m;
|
||||||
|
const fehlt = fehltIdNeu || fehltIdAlt;
|
||||||
|
if (KEINE_MODS.has(fehlt.toLowerCase())) continue;
|
||||||
|
const anzeige = fehltName || fehlt;
|
||||||
|
fund(`fehlt:${fehlt}`, `„${name}“ braucht ${anzeige}`,
|
||||||
|
`Der Mod „${name}“ funktioniert nur zusammen mit „${anzeige}“${version ? ` (Version ${version.trim()})` : ''} – und der fehlt in dieser Instanz.`,
|
||||||
|
{ typ: 'mod-installieren', projekt: projektFuer(fehlt), name: anzeige });
|
||||||
|
}
|
||||||
|
// Lösungsvorschlag von Fabric: "- Install fabric-api, any version."
|
||||||
|
for (const m of t.matchAll(/^\s*-\s*Install ([\w.-]+), (?:any version|version [^\n.]+)/gim)) {
|
||||||
|
const fehlt = m[1];
|
||||||
|
if (KEINE_MODS.has(fehlt.toLowerCase())) continue;
|
||||||
|
fund(`fehlt:${fehlt}`, `Es fehlt „${fehlt}“`,
|
||||||
|
`Ein installierter Mod braucht „${fehlt}“, der in dieser Instanz nicht installiert ist.`,
|
||||||
|
{ typ: 'mod-installieren', projekt: projektFuer(fehlt), name: fehlt });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Fehlende Abhängigkeiten (Forge/NeoForge) ---
|
||||||
|
// "Mod ID: 'architectury', Requested by: 'rei', Expected range: '[9.1.12,)', Actual version: '[MISSING]'"
|
||||||
|
for (const m of t.matchAll(/Mod ID: '([\w.-]+)', Requested by: '([\w.-]+)', Expected range: '([^']*)', Actual version: '([^']*)'/gi)) {
|
||||||
|
const [, fehlt, von, bereich, ist] = m;
|
||||||
|
if (fehlt.toLowerCase() === 'minecraft') {
|
||||||
|
const mod = findeMod(mods, von);
|
||||||
|
fund(`mcversion:${von}`, `„${von}“ passt nicht zu dieser Minecraft-Version`,
|
||||||
|
`Der Mod „${von}“ ist für Minecraft ${bereich} gemacht, deine Instanz hat ${ctx.version || ist}.`,
|
||||||
|
mod ? { typ: 'mod-deaktivieren', dateiname: mod.filename, name: mod.title || von } : null);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (KEINE_MODS.has(fehlt.toLowerCase())) continue;
|
||||||
|
if (/MISSING/i.test(ist)) {
|
||||||
|
fund(`fehlt:${fehlt}`, `„${von}“ braucht ${fehlt}`,
|
||||||
|
`Der Mod „${von}“ funktioniert nur zusammen mit „${fehlt}“ – und der fehlt in dieser Instanz.`,
|
||||||
|
{ typ: 'mod-installieren', projekt: projektFuer(fehlt), name: fehlt });
|
||||||
|
} else {
|
||||||
|
fund(`alt:${fehlt}`, `„${fehlt}“ ist zu alt oder zu neu`,
|
||||||
|
`„${von}“ braucht „${fehlt}“ in der Version ${bereich}, installiert ist ${ist}. Aktualisiere „${fehlt}“ im Inhalts-Dialog („Nach Updates suchen“).`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Mod für eine andere Minecraft-Version (Fabric) ---
|
||||||
|
// "Mod 'X' (x) 1.0 requires version 1.20.4 of minecraft, but only the wrong version is present: 1.21!"
|
||||||
|
for (const m of t.matchAll(/Mod '([^']+)' \(([\w.-]+)\)[^\n]*? requires (?:version |any version of )?([^\n]*?) of (?:mod )?'?minecraft'?/gi)) {
|
||||||
|
const [, name, id, fuer] = m;
|
||||||
|
const mod = findeMod(mods, id, name);
|
||||||
|
fund(`mcversion:${id}`, `„${name}“ passt nicht zu dieser Minecraft-Version`,
|
||||||
|
`„${name}“ ist für Minecraft ${fuer.replace(/^version\s+/i, '').trim() || 'eine andere Version'} gemacht, deine Instanz hat ${ctx.version || 'eine andere'}. Abschalten oder eine passende Version des Mods installieren.`,
|
||||||
|
mod ? { typ: 'mod-deaktivieren', dateiname: mod.filename, name } : null);
|
||||||
|
}
|
||||||
|
// Lösungsvorschlag: "- Remove mod 'X' (x)."
|
||||||
|
for (const m of t.matchAll(/^\s*-\s*Remove mod '([^']+)' \(([\w.-]+)\)/gim)) {
|
||||||
|
const [, name, id] = m;
|
||||||
|
const mod = findeMod(mods, id, name);
|
||||||
|
fund(`entfernen:${id}`, `„${name}“ verursacht den Fehler`,
|
||||||
|
`Der Mod-Loader schlägt vor, „${name}“ zu entfernen – er verträgt sich nicht mit den anderen Mods oder dieser Version.`,
|
||||||
|
mod ? { typ: 'mod-deaktivieren', dateiname: mod.filename, name } : null);
|
||||||
|
}
|
||||||
|
// "Mod 'A' (a) is incompatible with any version of mod 'B' (b)"
|
||||||
|
for (const m of t.matchAll(/Mod '([^']+)' \(([\w.-]+)\)[^\n]*? is incompatible with [^\n]*?mod '([^']+)' \(([\w.-]+)\)/gi)) {
|
||||||
|
const [, a, idA, b] = m;
|
||||||
|
const mod = findeMod(mods, idA, a);
|
||||||
|
fund(`inkompatibel:${idA}`, `„${a}“ und „${b}“ vertragen sich nicht`,
|
||||||
|
`Diese beiden Mods können nicht zusammen installiert sein. Schalte einen der beiden ab.`,
|
||||||
|
mod ? { typ: 'mod-deaktivieren', dateiname: mod.filename, name: a } : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Doppelte Mods ---
|
||||||
|
if (/Duplicate mods found|Found duplicate mods|is provided by multiple mods|Mod ID ['"]?[\w.-]+['"]? is (?:provided|present) multiple times/i.test(t)) {
|
||||||
|
fund('doppelt', 'Ein Mod ist doppelt installiert',
|
||||||
|
'Mindestens ein Mod liegt zweimal im Mods-Ordner (z. B. zwei Versionen desselben Mods). Lösche die ältere Datei.',
|
||||||
|
{ typ: 'mods-ordner' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Falsche Java-Version ---
|
||||||
|
const klasse = t.match(/UnsupportedClassVersionError[\s\S]{0,300}?class file version (\d+)\.\d+/i);
|
||||||
|
const javaPflicht = t.match(/requires (?:at least )?Java (\d+)|Java (\d+) (?:or (?:newer|later|above)) is required|Unsupported Java detected \((\d+)/i);
|
||||||
|
if (klasse || javaPflicht) {
|
||||||
|
const noetig = klasse ? javaAusKlassenversion(klasse[1]) : Number(javaPflicht[1] || javaPflicht[2] || 0);
|
||||||
|
fund('java', noetig ? `Diese Instanz braucht Java ${noetig}` : 'Falsche Java-Version',
|
||||||
|
`Minecraft oder ein Mod wurde für ${noetig ? `Java ${noetig}` : 'eine andere Java-Version'} gebaut, gestartet wurde mit einer älteren. AeroMC kann das passende Java automatisch wählen und bei Bedarf herunterladen.`,
|
||||||
|
{ typ: 'java-auto' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Zu wenig Arbeitsspeicher ---
|
||||||
|
if (/java\.lang\.OutOfMemoryError|There is insufficient memory for the Java Runtime/i.test(t)) {
|
||||||
|
const jetzt = Number(ctx.maxMemMb) || 2048;
|
||||||
|
const grenze = Math.max(2048, Math.floor((Number(ctx.gesamtMb) || 8192) * 0.6));
|
||||||
|
const neu = Math.min(grenze, jetzt + 2048);
|
||||||
|
fund('ram', 'Minecraft hatte zu wenig Arbeitsspeicher',
|
||||||
|
`Der Instanz waren ${Math.round(jetzt / 102.4) / 10} GB zugewiesen – zu wenig für diese Mods oder Welt.`
|
||||||
|
+ (neu > jetzt ? ` Empfohlen: ${Math.round(neu / 102.4) / 10} GB.` : ' Mehr lässt dein PC kaum zu – weniger Mods oder eine kleinere Sichtweite helfen.'),
|
||||||
|
neu > jetzt ? { typ: 'ram', mb: neu } : null);
|
||||||
|
}
|
||||||
|
if (/Could not reserve enough space for (?:\d+KB )?object heap|Invalid maximum heap size|Initial heap size set to a larger value than the maximum/i.test(t)) {
|
||||||
|
const jetzt = Number(ctx.maxMemMb) || 4096;
|
||||||
|
const neu = Math.max(2048, Math.min(jetzt - 1024, Math.floor((Number(ctx.gesamtMb) || 8192) * 0.5)));
|
||||||
|
fund('ram-zuviel', 'Zu viel Arbeitsspeicher eingestellt',
|
||||||
|
`Java konnte die eingestellten ${Math.round(jetzt / 102.4) / 10} GB nicht reservieren – entweder ist gerade nicht genug frei oder es läuft ein 32-Bit-Java.`,
|
||||||
|
neu < jetzt ? { typ: 'ram', mb: neu } : { typ: 'java-auto' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Mixin-Fehler: ein bestimmter Mod passt nicht ---
|
||||||
|
for (const m of t.matchAll(/Mixin (?:apply|transformation)[^\n]*? (?:for|from) mod ([\w.-]+) failed|from mod ([\w.-]+) failed injection check|mixins?\.json:[^\n]*? from mod ([\w.-]+)/gi)) {
|
||||||
|
const id = m[1] || m[2] || m[3];
|
||||||
|
if (!id || KEINE_MODS.has(id.toLowerCase())) continue;
|
||||||
|
const mod = findeMod(mods, id);
|
||||||
|
fund(`mixin:${id}`, `„${mod ? (mod.title || id) : id}“ passt nicht zu dieser Version`,
|
||||||
|
`Der Mod „${id}“ greift tief in Minecraft ein und ist mit dieser Minecraft-Version oder einem anderen Mod nicht kompatibel. Schalte ihn ab oder installiere eine passende Version.`,
|
||||||
|
mod ? { typ: 'mod-deaktivieren', dateiname: mod.filename, name: mod.title || id } : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Grafiktreiber ---
|
||||||
|
if (/Pixel format not accelerated|WGL: The driver does not appear to support OpenGL|GLFW error 65542|OpenGL 3\.2 (?:is )?not supported|Couldn't set pixel format|No OpenGL context found/i.test(t)) {
|
||||||
|
fund('treiber', 'Problem mit dem Grafiktreiber',
|
||||||
|
'Minecraft konnte OpenGL nicht starten. Meist hilft es, den Grafiktreiber direkt vom Hersteller (NVIDIA, AMD oder Intel) zu aktualisieren. Bei Laptops: in den Windows-Grafikeinstellungen Java auf „Hohe Leistung“ stellen.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Bekannte Unverträglichkeiten anhand der installierten Mods ---
|
||||||
|
const aktive = mods.filter((m) => m.enabled !== false);
|
||||||
|
const hat = (re) => aktive.find((m) => re.test(String(m.filename || '') + ' ' + String(m.title || '')));
|
||||||
|
const optifine = hat(/optifine|optifabric/i);
|
||||||
|
const sodium = hat(/sodium|embeddium|rubidium/i);
|
||||||
|
if (optifine && sodium) {
|
||||||
|
fund('optifine-sodium', 'OptiFine und Sodium zusammen installiert',
|
||||||
|
'OptiFine und Sodium (bzw. Embeddium/Rubidium) machen dasselbe und vertragen sich nicht. Schalte OptiFine ab – für Shader gibt es Iris als Ergänzung zu Sodium.',
|
||||||
|
{ typ: 'mod-deaktivieren', dateiname: optifine.filename, name: optifine.title || 'OptiFine' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Forge nennt einen Verdächtigen, aber sonst nichts Eindeutiges ---
|
||||||
|
if (!funde.length) {
|
||||||
|
const verdacht = t.match(/^Suspected Mods?:\s*([^\n]+)$/im);
|
||||||
|
if (verdacht && !/^\s*NONE\s*$/i.test(verdacht[1])) {
|
||||||
|
const name = verdacht[1].split(/[,(]/)[0].trim();
|
||||||
|
const mod = findeMod(mods, '', name);
|
||||||
|
fund('verdacht', `Vermutlich verursacht von „${name}“`,
|
||||||
|
`Laut Absturzbericht steckt wahrscheinlich „${verdacht[1].trim()}“ dahinter. Schalte den Mod testweise ab und starte erneut.`,
|
||||||
|
mod ? { typ: 'mod-deaktivieren', dateiname: mod.filename, name: mod.title || name } : null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return funde;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { analysiere, projektFuer };
|
||||||
+207
-70
@@ -1,70 +1,207 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* friends.js – Anbindung an das AeroMC-Freundesystem (WordPress-Plugin)
|
* friends.js – Anbindung an das AeroMC-Freundesystem (WordPress-Plugin)
|
||||||
* -----------------------------------------------------------------------------
|
* -----------------------------------------------------------------------------
|
||||||
* Schlanke Fetch-Wrapper gegen die REST-Routen des "AeroMC Freunde"-Plugins
|
* Schlanke Fetch-Wrapper gegen die REST-Routen des "AeroMC Freunde"-Plugins
|
||||||
* (siehe wordpress-plugin/aeromc-friends/). Keine eigene Anmeldung - jeder
|
* (siehe wordpress-plugin/aeromc-friends/). Jeder Aufruf trägt die
|
||||||
* Aufruf trägt einfach die Minecraft-UUID des aktiven Kontos.
|
* Minecraft-UUID des Kontos und – sobald angemeldet – ein Sitzungs-Token.
|
||||||
*
|
*
|
||||||
* Genau wie bei discordpresence.js: jeder Fehler (Plugin nicht installiert,
|
* Anmeldung wie bei einem Minecraft-Server, ohne eigenes Passwort: Aufgabe
|
||||||
* kein Internet, Seite down) wird abgefangen und nach oben als { ok:false,
|
* vom Plugin holen, mit dem Minecraft-Zugang des Kontos bei Mojang "beitreten",
|
||||||
* message } gereicht statt den Launcher zu stören - ein soziales Extra darf
|
* Plugin prüft bei Mojang nach und gibt ein Token aus. Läuft im Hintergrund
|
||||||
* nie den Spielstart oder die Bedienung blockieren.
|
* vor der ersten Anfrage eines Kontos; kennt das Plugin die Anmeldung noch
|
||||||
*/
|
* nicht (ältere Fassung), geht es ohne Token weiter wie bisher.
|
||||||
|
*
|
||||||
const BASIS_URL = 'https://viper-network.de/wp-json/aeromc/v1';
|
* Genau wie bei discordpresence.js: jeder Fehler (Plugin nicht installiert,
|
||||||
|
* kein Internet, Seite down) wird abgefangen und nach oben als { ok:false,
|
||||||
async function anfrage(pfad, { method = 'GET', body } = {}) {
|
* message } gereicht statt den Launcher zu stören - ein soziales Extra darf
|
||||||
try {
|
* nie den Spielstart oder die Bedienung blockieren.
|
||||||
const url = method === 'GET' && body
|
*/
|
||||||
? `${BASIS_URL}${pfad}?${new URLSearchParams(body).toString()}`
|
|
||||||
: `${BASIS_URL}${pfad}`;
|
const BASIS_URL = 'https://viper-network.de/wp-json/aeromc/v1';
|
||||||
const res = await fetch(url, {
|
const MOJANG_JOIN = 'https://sessionserver.mojang.com/session/minecraft/join';
|
||||||
method,
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
// liefert für eine Konto-UUID { accessToken, name } (vom Hauptprozess gesetzt)
|
||||||
body: method === 'GET' ? undefined : JSON.stringify(body || {}),
|
let zugangsHelfer = null;
|
||||||
});
|
const sitzungen = new Map(); // uuid -> Token
|
||||||
const daten = await res.json().catch(() => null);
|
const laufendeAnmeldung = new Map(); // uuid -> Promise (nicht doppelt anmelden)
|
||||||
if (!res.ok) return { ok: false, message: (daten && daten.message) || `Fehler ${res.status}` };
|
const fehlversuch = new Map(); // uuid -> Zeitpunkt, um nicht ständig neu zu versuchen
|
||||||
return { ok: true, data: daten };
|
const NEUVERSUCH_MS = 5 * 60 * 1000;
|
||||||
} catch (err) {
|
|
||||||
return { ok: false, message: 'Freundesystem nicht erreichbar: ' + err.message };
|
function setzeZugangsHelfer(fn) { zugangsHelfer = fn; }
|
||||||
}
|
|
||||||
}
|
async function roheAnfrage(pfad, { method = 'GET', body, token } = {}) {
|
||||||
|
const url = method === 'GET' && body
|
||||||
function heartbeat({ uuid, username, instance, server }) {
|
? `${BASIS_URL}${pfad}?${new URLSearchParams(body).toString()}`
|
||||||
return anfrage('/presence', { method: 'POST', body: { uuid, username, instance, server } });
|
: `${BASIS_URL}${pfad}`;
|
||||||
}
|
const headers = { 'Content-Type': 'application/json' };
|
||||||
|
if (token) headers['X-AeroMC-Token'] = token;
|
||||||
function getFriends(uuid) {
|
const res = await fetch(url, {
|
||||||
return anfrage('/friends', { method: 'GET', body: { uuid } });
|
method,
|
||||||
}
|
headers,
|
||||||
|
body: method === 'GET' ? undefined : JSON.stringify(body || {}),
|
||||||
function getRequests(uuid) {
|
});
|
||||||
return anfrage('/friends/requests', { method: 'GET', body: { uuid } });
|
const daten = await res.json().catch(() => null);
|
||||||
}
|
return { res, daten };
|
||||||
|
}
|
||||||
function getSentRequests(uuid) {
|
|
||||||
return anfrage('/friends/sent', { method: 'GET', body: { uuid } });
|
// Mojang-Sitzungsprüfung durchlaufen und ein Token holen; null bei Fehlschlag
|
||||||
}
|
async function anmelden(uuid) {
|
||||||
|
if (!zugangsHelfer) return null;
|
||||||
function sendRequest(uuid, username, targetUsername) {
|
const letzter = fehlversuch.get(uuid);
|
||||||
return anfrage('/friends/request', { method: 'POST', body: { uuid, username, targetUsername } });
|
if (letzter && Date.now() - letzter < NEUVERSUCH_MS) return null;
|
||||||
}
|
if (laufendeAnmeldung.has(uuid)) return laufendeAnmeldung.get(uuid);
|
||||||
|
const ablauf = (async () => {
|
||||||
function respond(uuid, otherUuid, accept) {
|
try {
|
||||||
return anfrage('/friends/respond', { method: 'POST', body: { uuid, otherUuid, accept } });
|
const zugang = await zugangsHelfer(uuid);
|
||||||
}
|
if (!zugang || !zugang.accessToken || !zugang.name) throw new Error('kein Minecraft-Zugang');
|
||||||
|
const aufgabe = await roheAnfrage('/auth/challenge', { method: 'POST', body: { uuid, username: zugang.name } });
|
||||||
function cancelRequest(uuid, otherUuid) {
|
if (!aufgabe.res.ok || !aufgabe.daten || !aufgabe.daten.serverId) throw new Error('keine Aufgabe');
|
||||||
return anfrage('/friends/cancel', { method: 'POST', body: { uuid, otherUuid } });
|
const join = await fetch(MOJANG_JOIN, {
|
||||||
}
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
function removeFriend(uuid, otherUuid) {
|
body: JSON.stringify({
|
||||||
return anfrage('/friends/remove', { method: 'POST', body: { uuid, otherUuid } });
|
accessToken: zugang.accessToken,
|
||||||
}
|
selectedProfile: String(uuid).replace(/-/g, ''),
|
||||||
|
serverId: aufgabe.daten.serverId,
|
||||||
module.exports = {
|
}),
|
||||||
heartbeat, getFriends, getRequests, getSentRequests, sendRequest, respond, cancelRequest, removeFriend,
|
});
|
||||||
};
|
if (!join.ok) throw new Error('Mojang ' + join.status);
|
||||||
|
const bestaetigt = await roheAnfrage('/auth/verify', { method: 'POST', body: { uuid } });
|
||||||
|
if (!bestaetigt.res.ok || !bestaetigt.daten || !bestaetigt.daten.token) throw new Error('nicht bestätigt');
|
||||||
|
sitzungen.set(uuid, bestaetigt.daten.token);
|
||||||
|
fehlversuch.delete(uuid);
|
||||||
|
return bestaetigt.daten.token;
|
||||||
|
} catch {
|
||||||
|
fehlversuch.set(uuid, Date.now());
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
laufendeAnmeldung.delete(uuid);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
laufendeAnmeldung.set(uuid, ablauf);
|
||||||
|
return ablauf;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function anfrage(pfad, { method = 'GET', body } = {}) {
|
||||||
|
try {
|
||||||
|
const uuid = body && body.uuid;
|
||||||
|
let token = uuid ? sitzungen.get(uuid) : null;
|
||||||
|
if (uuid && !token) token = await anmelden(uuid);
|
||||||
|
let { res, daten } = await roheAnfrage(pfad, { method, body, token });
|
||||||
|
// Sitzung abgelaufen oder Anmeldung jetzt Pflicht -> einmal neu anmelden
|
||||||
|
if (res.status === 401 && daten && daten.code === 'aeromc_auth' && uuid) {
|
||||||
|
sitzungen.delete(uuid);
|
||||||
|
fehlversuch.delete(uuid);
|
||||||
|
token = await anmelden(uuid);
|
||||||
|
if (token) ({ res, daten } = await roheAnfrage(pfad, { method, body, token }));
|
||||||
|
}
|
||||||
|
if (!res.ok) return { ok: false, message: (daten && daten.message) || `Fehler ${res.status}`, code: daten && daten.code };
|
||||||
|
return { ok: true, data: daten };
|
||||||
|
} catch (err) {
|
||||||
|
return { ok: false, message: 'Freundesystem nicht erreichbar: ' + err.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function heartbeat({ uuid, username, instance, server, version }) {
|
||||||
|
return anfrage('/presence', { method: 'POST', body: { uuid, username, instance, server, version } });
|
||||||
|
}
|
||||||
|
|
||||||
|
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 } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Einladung "Komm auf meinen Server" (Adresse + Minecraft-Version des Einladenden)
|
||||||
|
function sendInvite(uuid, targetUuid, server, version) {
|
||||||
|
return anfrage('/friends/invite', { method: 'POST', body: { uuid, targetUuid, server, version } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// an mich gerichtete, noch gültige Einladungen
|
||||||
|
function getInvites(uuid) {
|
||||||
|
return anfrage('/friends/invites', { method: 'GET', body: { uuid } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismissInvite(uuid, id) {
|
||||||
|
return anfrage('/friends/invites/dismiss', { method: 'POST', body: { uuid, id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instanz per Code teilen: Beschreibung (Version, Loader, Mod-Liste) hinterlegen -> { code, days }
|
||||||
|
function createShare(uuid, manifest) {
|
||||||
|
return anfrage('/share', { method: 'POST', body: { uuid, manifest } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// geteilte Instanz abrufen (ohne Anmeldung – wer den Code hat, darf sie sehen)
|
||||||
|
async function getShare(code) {
|
||||||
|
try {
|
||||||
|
const { res, daten } = await roheAnfrage('/share', { method: 'GET', body: { code } });
|
||||||
|
if (!res.ok) return { ok: false, message: (daten && daten.message) || `Fehler ${res.status}` };
|
||||||
|
return { ok: true, data: daten };
|
||||||
|
} catch (err) {
|
||||||
|
return { ok: false, message: 'Server nicht erreichbar: ' + err.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Kurznachrichten ----
|
||||||
|
// image: optional base64-JPEG (geteilter Screenshot)
|
||||||
|
function sendMessage(uuid, targetUuid, text, image) {
|
||||||
|
const body = { uuid, targetUuid, text: text || '' };
|
||||||
|
if (image) body.image = image;
|
||||||
|
return anfrage('/friends/message', { method: 'POST', body });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unterhaltung mit einem Freund (älteste zuerst)
|
||||||
|
function getMessages(uuid, withUuid) {
|
||||||
|
return anfrage('/friends/messages', { method: 'GET', body: { uuid, with: withUuid } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ungelesene Nachrichten an mich
|
||||||
|
function getUnreadMessages(uuid) {
|
||||||
|
return anfrage('/friends/messages/unread', { method: 'GET', body: { uuid } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function markMessagesRead(uuid, fromUuid) {
|
||||||
|
return anfrage('/friends/messages/read', { method: 'POST', body: { uuid, fromUuid } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fehlerbericht aus dem Absturz-Assistenten; mit uuid wird angemeldet gesendet, sonst anonym
|
||||||
|
async function sendReport(daten) {
|
||||||
|
if (daten && daten.uuid) return anfrage('/report', { method: 'POST', body: daten });
|
||||||
|
try {
|
||||||
|
const { res, daten: antwort } = await roheAnfrage('/report', { method: 'POST', body: daten });
|
||||||
|
if (!res.ok) return { ok: false, message: (antwort && antwort.message) || `Fehler ${res.status}` };
|
||||||
|
return { ok: true, data: antwort };
|
||||||
|
} catch (err) {
|
||||||
|
return { ok: false, message: 'Server nicht erreichbar: ' + err.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
heartbeat, getFriends, getRequests, getSentRequests, sendRequest, respond, cancelRequest, removeFriend,
|
||||||
|
sendInvite, getInvites, dismissInvite, createShare, getShare, setzeZugangsHelfer,
|
||||||
|
sendMessage, getMessages, getUnreadMessages, markMessagesRead, sendReport,
|
||||||
|
};
|
||||||
|
|||||||
+1660
-1473
File diff suppressed because it is too large
Load Diff
+17
-2
@@ -357,6 +357,13 @@ function buildArgs(vj, ctx) {
|
|||||||
* auth: null (kein Login) ODER { name, uuid, accessToken, userType }
|
* auth: null (kein Login) ODER { name, uuid, accessToken, userType }
|
||||||
* onProgress: (evt) => void
|
* onProgress: (evt) => void
|
||||||
*/
|
*/
|
||||||
|
// true für Release-Versionen vor 1.20 (kein Quick Play). Snapshots und
|
||||||
|
// Unbekanntes gelten als neu – die meisten heutigen Snapshots sind > 1.20.
|
||||||
|
function vorQuickPlay(versionId) {
|
||||||
|
const m = /^1\.(\d+)(?:\.\d+)?(?:-(?:pre|rc)\d+)?$/.exec(String(versionId || '').trim());
|
||||||
|
return !!m && Number(m[1]) < 20;
|
||||||
|
}
|
||||||
|
|
||||||
async function prepareAndLaunch(opts, onProgress) {
|
async function prepareAndLaunch(opts, onProgress) {
|
||||||
const emit = (phase, extra) => onProgress && onProgress(Object.assign({ phase }, extra));
|
const emit = (phase, extra) => onProgress && onProgress(Object.assign({ phase }, extra));
|
||||||
|
|
||||||
@@ -526,9 +533,17 @@ async function prepareAndLaunch(opts, onProgress) {
|
|||||||
// Fenstergröße bzw. maximiert starten (Minecraft-Argumente)
|
// Fenstergröße bzw. maximiert starten (Minecraft-Argumente)
|
||||||
if (opts.mcMaximized) args.push('--fullscreen');
|
if (opts.mcMaximized) args.push('--fullscreen');
|
||||||
else if (opts.mcWidth && opts.mcHeight) args.push('--width', String(opts.mcWidth), '--height', String(opts.mcHeight));
|
else if (opts.mcWidth && opts.mcHeight) args.push('--width', String(opts.mcWidth), '--height', String(opts.mcHeight));
|
||||||
// Ab 1.20: direkt einem Server beitreten (Quick Play)
|
// Direkt einem Server beitreten: ab 1.20 per Quick Play, davor über die
|
||||||
|
// älteren Startparameter --server/--port (z. B. für "Beitreten" bei Freunden)
|
||||||
if (opts.quickPlayServer) {
|
if (opts.quickPlayServer) {
|
||||||
args.push('--quickPlayMultiplayer', String(opts.quickPlayServer));
|
const adresse = String(opts.quickPlayServer).trim();
|
||||||
|
if (vorQuickPlay(opts.versionId)) {
|
||||||
|
const m = /^(.*?)(?::(\d+))?$/.exec(adresse);
|
||||||
|
args.push('--server', m[1]);
|
||||||
|
if (m[2]) args.push('--port', m[2]);
|
||||||
|
} else {
|
||||||
|
args.push('--quickPlayMultiplayer', adresse);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureDir(opts.gameDir);
|
ensureDir(opts.gameDir);
|
||||||
|
|||||||
+3912
-3015
File diff suppressed because it is too large
Load Diff
+374
-327
@@ -1,327 +1,374 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* preload.js – stellt dem Renderer eine schmale, sichere API bereit.
|
* preload.js – stellt dem Renderer eine schmale, sichere API bereit.
|
||||||
* contextIsolation ist aktiv; der Renderer hat KEINEN direkten Node-Zugriff.
|
* contextIsolation ist aktiv; der Renderer hat KEINEN direkten Node-Zugriff.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { contextBridge, ipcRenderer } = require('electron');
|
const { contextBridge, ipcRenderer } = require('electron');
|
||||||
|
|
||||||
// Debug-Hilfe: JS-Fehler im Renderer landen im Terminal des Hauptprozesses,
|
// Debug-Hilfe: JS-Fehler im Renderer landen im Terminal des Hauptprozesses,
|
||||||
// damit man sie auch ohne offene DevTools sieht
|
// damit man sie auch ohne offene DevTools sieht
|
||||||
window.addEventListener('error', (e) => {
|
window.addEventListener('error', (e) => {
|
||||||
ipcRenderer.send('debug:rendererError', (e.error && e.error.stack) || e.message);
|
ipcRenderer.send('debug:rendererError', (e.error && e.error.stack) || e.message);
|
||||||
});
|
});
|
||||||
window.addEventListener('unhandledrejection', (e) => {
|
window.addEventListener('unhandledrejection', (e) => {
|
||||||
ipcRenderer.send('debug:rendererError', 'unhandledrejection: ' + ((e.reason && e.reason.stack) || e.reason));
|
ipcRenderer.send('debug:rendererError', 'unhandledrejection: ' + ((e.reason && e.reason.stack) || e.reason));
|
||||||
});
|
});
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('api', {
|
contextBridge.exposeInMainWorld('api', {
|
||||||
// Einstellungen
|
// Einstellungen
|
||||||
getSettings: () => ipcRenderer.invoke('settings:get'),
|
getSettings: () => ipcRenderer.invoke('settings:get'),
|
||||||
saveSettings: (patch) => ipcRenderer.invoke('settings:save', patch),
|
saveSettings: (patch) => ipcRenderer.invoke('settings:save', patch),
|
||||||
getInstancesDir: () => ipcRenderer.invoke('settings:instancesDir'),
|
getInstancesDir: () => ipcRenderer.invoke('settings:instancesDir'),
|
||||||
detectJava: (javaPath) => ipcRenderer.invoke('java:detect', javaPath),
|
detectJava: (javaPath) => ipcRenderer.invoke('java:detect', javaPath),
|
||||||
scanJava: () => ipcRenderer.invoke('java:scan'),
|
scanJava: () => ipcRenderer.invoke('java:scan'),
|
||||||
listJava: () => ipcRenderer.invoke('java:list'),
|
listJava: () => ipcRenderer.invoke('java:list'),
|
||||||
|
|
||||||
// Minecraft-Versionen (Mojang)
|
// Minecraft-Versionen (Mojang)
|
||||||
mcVersions: () => ipcRenderer.invoke('mc:versions'),
|
mcVersions: () => ipcRenderer.invoke('mc:versions'),
|
||||||
loaderVersions: (loader, mcVersion) => ipcRenderer.invoke('loaders:versions', loader, mcVersion),
|
loaderVersions: (loader, mcVersion) => ipcRenderer.invoke('loaders:versions', loader, mcVersion),
|
||||||
optifineFind: (mcVersion) => ipcRenderer.invoke('optifine:find', mcVersion),
|
optifineFind: (mcVersion) => ipcRenderer.invoke('optifine:find', mcVersion),
|
||||||
optifineSetup: (id) => ipcRenderer.invoke('optifine:setup', id),
|
optifineSetup: (id) => ipcRenderer.invoke('optifine:setup', id),
|
||||||
optifineConfirm: (id) => ipcRenderer.invoke('optifine:confirm', id),
|
optifineConfirm: (id) => ipcRenderer.invoke('optifine:confirm', id),
|
||||||
|
|
||||||
// Mods (Modrinth)
|
// Mods (Modrinth)
|
||||||
modSearch: (opts) => ipcRenderer.invoke('mods:search', opts),
|
modSearch: (opts) => ipcRenderer.invoke('mods:search', opts),
|
||||||
curseforgeTestKey: (apiKey) => ipcRenderer.invoke('curseforge:testKey', apiKey),
|
curseforgeTestKey: (apiKey) => ipcRenderer.invoke('curseforge:testKey', apiKey),
|
||||||
// art: 'mod' | 'resourcepack' | 'shader'
|
// art: 'mod' | 'resourcepack' | 'shader'
|
||||||
modList: (id, art) => ipcRenderer.invoke('mods:list', id, art),
|
modList: (id, art) => ipcRenderer.invoke('mods:list', id, art),
|
||||||
modInstall: (id, project, art) => ipcRenderer.invoke('mods:install', id, project, art),
|
modInstall: (id, project, art) => ipcRenderer.invoke('mods:install', id, project, art),
|
||||||
modRemove: (id, filename, art) => ipcRenderer.invoke('mods:remove', id, filename, art),
|
modRemove: (id, filename, art) => ipcRenderer.invoke('mods:remove', id, filename, art),
|
||||||
modDependents: (id, filename, art) => ipcRenderer.invoke('mods:dependents', id, filename, art),
|
modDependents: (id, filename, art) => ipcRenderer.invoke('mods:dependents', id, filename, art),
|
||||||
modAddFile: (id, art) => ipcRenderer.invoke('mods:addFile', id, art),
|
modAddFile: (id, art) => ipcRenderer.invoke('mods:addFile', id, art),
|
||||||
modSetEnabled: (id, filename, enabled, art) => ipcRenderer.invoke('mods:setEnabled', id, filename, enabled, art),
|
modSetEnabled: (id, filename, enabled, art) => ipcRenderer.invoke('mods:setEnabled', id, filename, enabled, art),
|
||||||
systemMemory: () => ipcRenderer.invoke('system:memory'),
|
systemMemory: () => ipcRenderer.invoke('system:memory'),
|
||||||
checkMemory: (minMb, maxMb) => ipcRenderer.invoke('system:checkMemory', minMb, maxMb),
|
checkMemory: (minMb, maxMb) => ipcRenderer.invoke('system:checkMemory', minMb, maxMb),
|
||||||
crashLatest: (id, seit) => ipcRenderer.invoke('crash:latest', id, seit),
|
crashLatest: (id, seit) => ipcRenderer.invoke('crash:latest', id, seit),
|
||||||
onAutoBackup: (cb) => {
|
crashAnalyze: (id, seit) => ipcRenderer.invoke('crash:analyze', id, seit),
|
||||||
const l = (_e, d) => cb(d);
|
crashFix: (id, aktion) => ipcRenderer.invoke('crash:fix', id, aktion),
|
||||||
ipcRenderer.on('backup:auto', l);
|
crashRelaunch: (id) => ipcRenderer.invoke('crash:relaunch', id),
|
||||||
return () => ipcRenderer.removeListener('backup:auto', l);
|
shareCreate: (id) => ipcRenderer.invoke('share:create', id),
|
||||||
},
|
shareFetch: (code) => ipcRenderer.invoke('share:fetch', code),
|
||||||
crashOpenFolder: (id) => ipcRenderer.invoke('crash:openFolder', id),
|
shareInstall: (code) => ipcRenderer.invoke('share:install', code),
|
||||||
serversList: (id) => ipcRenderer.invoke('servers:list', id),
|
onShareProgress: (cb) => {
|
||||||
serversAdd: (id, eintrag) => ipcRenderer.invoke('servers:add', id, eintrag),
|
const l = (_e, p) => cb(p);
|
||||||
serversRemove: (id, ip) => ipcRenderer.invoke('servers:remove', id, ip),
|
ipcRenderer.on('share:progress', l);
|
||||||
serversRename: (id, ip, neuerName) => ipcRenderer.invoke('servers:rename', id, ip, neuerName),
|
return () => ipcRenderer.removeListener('share:progress', l);
|
||||||
serversCopyTo: (zielIds, eintrag) => ipcRenderer.invoke('servers:copyTo', zielIds, eintrag),
|
},
|
||||||
serversPing: (ip) => ipcRenderer.invoke('servers:ping', ip),
|
onAutoBackup: (cb) => {
|
||||||
logsList: (id) => ipcRenderer.invoke('logs:list', id),
|
const l = (_e, d) => cb(d);
|
||||||
logsRead: (id, name) => ipcRenderer.invoke('logs:read', id, name),
|
ipcRenderer.on('backup:auto', l);
|
||||||
logsOpenFolder: (id) => ipcRenderer.invoke('logs:openFolder', id),
|
return () => ipcRenderer.removeListener('backup:auto', l);
|
||||||
modCheckUpdates: (id, art) => ipcRenderer.invoke('mods:checkUpdates', id, art),
|
},
|
||||||
modApplyUpdates: (id, projectIds, art) => ipcRenderer.invoke('mods:applyUpdates', id, projectIds, art),
|
crashOpenFolder: (id) => ipcRenderer.invoke('crash:openFolder', id),
|
||||||
onModUpdateProgress: (cb) => {
|
serversList: (id) => ipcRenderer.invoke('servers:list', id),
|
||||||
const l = (_e, d) => cb(d);
|
serversAdd: (id, eintrag) => ipcRenderer.invoke('servers:add', id, eintrag),
|
||||||
ipcRenderer.on('mods:updateProgress', l);
|
serversRemove: (id, ip) => ipcRenderer.invoke('servers:remove', id, ip),
|
||||||
return () => ipcRenderer.removeListener('mods:updateProgress', l);
|
serversRename: (id, ip, neuerName) => ipcRenderer.invoke('servers:rename', id, ip, neuerName),
|
||||||
},
|
serversCopyTo: (zielIds, eintrag) => ipcRenderer.invoke('servers:copyTo', zielIds, eintrag),
|
||||||
|
serversPing: (ip) => ipcRenderer.invoke('servers:ping', ip),
|
||||||
// Instanzen
|
logsList: (id) => ipcRenderer.invoke('logs:list', id),
|
||||||
listInstances: () => ipcRenderer.invoke('instances:list'),
|
logsRead: (id, name) => ipcRenderer.invoke('logs:read', id, name),
|
||||||
getInstance: (id) => ipcRenderer.invoke('instances:get', id),
|
logsOpenFolder: (id) => ipcRenderer.invoke('logs:openFolder', id),
|
||||||
createInstance: (data) => ipcRenderer.invoke('instances:create', data),
|
modCheckUpdates: (id, art) => ipcRenderer.invoke('mods:checkUpdates', id, art),
|
||||||
updateInstance: (id, patch) => ipcRenderer.invoke('instances:update', id, patch),
|
modApplyUpdates: (id, projectIds, art) => ipcRenderer.invoke('mods:applyUpdates', id, projectIds, art),
|
||||||
deleteInstance: (id) => ipcRenderer.invoke('instances:delete', id),
|
onModUpdateProgress: (cb) => {
|
||||||
duplicateInstance: (id) => ipcRenderer.invoke('instances:duplicate', id),
|
const l = (_e, d) => cb(d);
|
||||||
openInstanceFolder: (id) => ipcRenderer.invoke('instances:openFolder', id),
|
ipcRenderer.on('mods:updateProgress', l);
|
||||||
openGameDir: (id) => ipcRenderer.invoke('instances:openGameDir', id),
|
return () => ipcRenderer.removeListener('mods:updateProgress', l);
|
||||||
|
},
|
||||||
// Welten & Ressourcenpakete
|
|
||||||
worlds: (id) => ipcRenderer.invoke('instance:worlds', id),
|
// Instanzen
|
||||||
resourcepacks: (id) => ipcRenderer.invoke('instance:resourcepacks', id),
|
listInstances: () => ipcRenderer.invoke('instances:list'),
|
||||||
datapacks: (id) => ipcRenderer.invoke('instance:datapacks', id),
|
getInstance: (id) => ipcRenderer.invoke('instances:get', id),
|
||||||
deleteContent: (id, sub, name) => ipcRenderer.invoke('instance:deleteContent', id, sub, name),
|
createInstance: (data) => ipcRenderer.invoke('instances:create', data),
|
||||||
backupWorld: (id, worldName) => ipcRenderer.invoke('world:backup', id, worldName),
|
installPerformancePack: (id) => ipcRenderer.invoke('instances:installPerformancePack', id),
|
||||||
openSubdir: (id, sub) => ipcRenderer.invoke('instance:openSubdir', id, sub),
|
updateInstance: (id, patch) => ipcRenderer.invoke('instances:update', id, patch),
|
||||||
screenshots: (id) => ipcRenderer.invoke('instance:screenshots', id),
|
deleteInstance: (id) => ipcRenderer.invoke('instances:delete', id),
|
||||||
screenshotData: (id, name) => ipcRenderer.invoke('instance:screenshotData', id, name),
|
duplicateInstance: (id) => ipcRenderer.invoke('instances:duplicate', id),
|
||||||
openScreenshot: (id, name) => ipcRenderer.invoke('instance:openScreenshot', id, name),
|
openInstanceFolder: (id) => ipcRenderer.invoke('instances:openFolder', id),
|
||||||
launchInstance: (id, accountId, opts) => ipcRenderer.invoke('instances:launch', id, accountId, opts),
|
openGameDir: (id) => ipcRenderer.invoke('instances:openGameDir', id),
|
||||||
exportInstance: (id) => ipcRenderer.invoke('instances:export', id),
|
|
||||||
createGlobalBackup: () => ipcRenderer.invoke('backup:global'),
|
// Welten & Ressourcenpakete
|
||||||
listGlobalBackups: () => ipcRenderer.invoke('backup:list'),
|
worlds: (id) => ipcRenderer.invoke('instance:worlds', id),
|
||||||
restoreGlobalBackup: (sourcePath) => ipcRenderer.invoke('backup:restore', sourcePath),
|
resourcepacks: (id) => ipcRenderer.invoke('instance:resourcepacks', id),
|
||||||
onBackupProgress: (cb) => {
|
datapacks: (id) => ipcRenderer.invoke('instance:datapacks', id),
|
||||||
const l = (_e, d) => cb(d);
|
deleteContent: (id, sub, name) => ipcRenderer.invoke('instance:deleteContent', id, sub, name),
|
||||||
ipcRenderer.on('backup:progress', l);
|
backupWorld: (id, worldName) => ipcRenderer.invoke('world:backup', id, worldName),
|
||||||
return () => ipcRenderer.removeListener('backup:progress', l);
|
worldCopyTo: (id, worldName, zielId) => ipcRenderer.invoke('world:copyTo', id, worldName, zielId),
|
||||||
},
|
friendsSendScreenshot: (otherUuid, instanceId, datei, text) => ipcRenderer.invoke('friends:sendScreenshot', otherUuid, instanceId, datei, text),
|
||||||
importInstance: () => ipcRenderer.invoke('instances:import'),
|
openSubdir: (id, sub) => ipcRenderer.invoke('instance:openSubdir', id, sub),
|
||||||
createShortcut: (id, customName) => ipcRenderer.invoke('instances:shortcut', id, customName),
|
screenshots: (id) => ipcRenderer.invoke('instance:screenshots', id),
|
||||||
serversShortcut: (id, ip, name, customName) => ipcRenderer.invoke('servers:shortcut', id, ip, name, customName),
|
screenshotData: (id, name) => ipcRenderer.invoke('instance:screenshotData', id, name),
|
||||||
// Start über eine Desktop-Verknüpfung ("--launch=<id>", optional "--server=<ip>")
|
openScreenshot: (id, name) => ipcRenderer.invoke('instance:openScreenshot', id, name),
|
||||||
onLaunchRequest: (cb) => {
|
launchInstance: (id, accountId, opts) => ipcRenderer.invoke('instances:launch', id, accountId, opts),
|
||||||
const l = (_e, payload) => cb(payload);
|
exportInstance: (id) => ipcRenderer.invoke('instances:export', id),
|
||||||
ipcRenderer.on('instance:launch-request', l);
|
createGlobalBackup: () => ipcRenderer.invoke('backup:global'),
|
||||||
return () => ipcRenderer.removeListener('instance:launch-request', l);
|
listGlobalBackups: () => ipcRenderer.invoke('backup:list'),
|
||||||
},
|
restoreGlobalBackup: (sourcePath) => ipcRenderer.invoke('backup:restore', sourcePath),
|
||||||
// Signalisiert dem Hauptprozess, dass onLaunchRequest jetzt registriert ist –
|
onBackupProgress: (cb) => {
|
||||||
// verhindert, dass ein Start-Wunsch aus einer Verknüpfung verloren geht,
|
const l = (_e, d) => cb(d);
|
||||||
// weil er ankommt, bevor die Oberfläche fertig eingerichtet ist
|
ipcRenderer.on('backup:progress', l);
|
||||||
notifyReady: () => ipcRenderer.send('renderer:ready'),
|
return () => ipcRenderer.removeListener('backup:progress', l);
|
||||||
importModpack: () => ipcRenderer.invoke('modpack:import'),
|
},
|
||||||
// Datei per Drag&Drop aufs Fenster gezogen (.jar/.mrpack/.zip)
|
importInstance: () => ipcRenderer.invoke('instances:import'),
|
||||||
filesDrop: (filePath, targetId, targetArt) => ipcRenderer.invoke('files:drop', filePath, targetId, targetArt),
|
createShortcut: (id, customName) => ipcRenderer.invoke('instances:shortcut', id, customName),
|
||||||
modpackSearch: (opts) => ipcRenderer.invoke('modpack:search', opts),
|
serversShortcut: (id, ip, name, customName) => ipcRenderer.invoke('servers:shortcut', id, ip, name, customName),
|
||||||
modpackInstallFromModrinth: (project) => ipcRenderer.invoke('modpack:installFromModrinth', project),
|
// Start über eine Desktop-Verknüpfung ("--launch=<id>", optional "--server=<ip>")
|
||||||
modpackInstallFromCurseForge: (project) => ipcRenderer.invoke('modpack:installFromCurseForge', project),
|
onLaunchRequest: (cb) => {
|
||||||
modpackCheck: (id) => ipcRenderer.invoke('modpack:check', id),
|
const l = (_e, payload) => cb(payload);
|
||||||
modpackUpdate: (id, versionId) => ipcRenderer.invoke('modpack:update', id, versionId),
|
ipcRenderer.on('instance:launch-request', l);
|
||||||
onLaunchProgress: (cb) => {
|
return () => ipcRenderer.removeListener('instance:launch-request', l);
|
||||||
const listener = (_e, data) => cb(data);
|
},
|
||||||
ipcRenderer.on('launch:progress', listener);
|
// Signalisiert dem Hauptprozess, dass onLaunchRequest jetzt registriert ist –
|
||||||
return () => ipcRenderer.removeListener('launch:progress', listener);
|
// 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'),
|
||||||
// eigenes Instanz-Bild
|
importModpack: () => ipcRenderer.invoke('modpack:import'),
|
||||||
pickImage: () => ipcRenderer.invoke('dialog:pickImage'),
|
// Datei per Drag&Drop aufs Fenster gezogen (.jar/.mrpack/.zip)
|
||||||
setInstanceImage: (id, sourcePath) => ipcRenderer.invoke('instances:setImage', id, sourcePath),
|
filesDrop: (filePath, targetId, targetArt) => ipcRenderer.invoke('files:drop', filePath, targetId, targetArt),
|
||||||
clearInstanceImage: (id) => ipcRenderer.invoke('instances:clearImage', id),
|
modpackSearch: (opts) => ipcRenderer.invoke('modpack:search', opts),
|
||||||
instanceIcon: (id) => ipcRenderer.invoke('instances:icon', id),
|
modpackInstallFromModrinth: (project) => ipcRenderer.invoke('modpack:installFromModrinth', project),
|
||||||
fileDataUri: (p) => ipcRenderer.invoke('file:dataUri', p),
|
modpackInstallFromCurseForge: (project) => ipcRenderer.invoke('modpack:installFromCurseForge', project),
|
||||||
|
modpackCheck: (id) => ipcRenderer.invoke('modpack:check', id),
|
||||||
// Microsoft-Login (Phase 3) + Mehrkonten
|
modpackUpdate: (id, versionId) => ipcRenderer.invoke('modpack:update', id, versionId),
|
||||||
authStatus: () => ipcRenderer.invoke('auth:status'),
|
onLaunchProgress: (cb) => {
|
||||||
authAccounts: () => ipcRenderer.invoke('auth:accounts'),
|
const listener = (_e, data) => cb(data);
|
||||||
authSetActive: (id) => ipcRenderer.invoke('auth:setActive', id),
|
ipcRenderer.on('launch:progress', listener);
|
||||||
skinProfile: () => ipcRenderer.invoke('skin:profile'),
|
return () => ipcRenderer.removeListener('launch:progress', listener);
|
||||||
skinChoose: () => ipcRenderer.invoke('skin:choose'),
|
},
|
||||||
skinApply: (pfad, variante) => ipcRenderer.invoke('skin:apply', pfad, variante),
|
|
||||||
skinReset: () => ipcRenderer.invoke('skin:reset'),
|
// eigenes Instanz-Bild
|
||||||
authRemove: (id) => ipcRenderer.invoke('auth:remove', id),
|
pickImage: () => ipcRenderer.invoke('dialog:pickImage'),
|
||||||
authLogin: () => ipcRenderer.invoke('auth:login'),
|
setInstanceImage: (id, sourcePath) => ipcRenderer.invoke('instances:setImage', id, sourcePath),
|
||||||
authLogout: () => ipcRenderer.invoke('auth:logout'),
|
clearInstanceImage: (id) => ipcRenderer.invoke('instances:clearImage', id),
|
||||||
authHealth: () => ipcRenderer.invoke('auth:health'),
|
instanceIcon: (id) => ipcRenderer.invoke('instances:icon', id),
|
||||||
authRecheck: () => ipcRenderer.invoke('auth:recheck'),
|
fileDataUri: (p) => ipcRenderer.invoke('file:dataUri', p),
|
||||||
onAuthHealth: (cb) => {
|
|
||||||
const listener = (_e, data) => cb(data);
|
// Microsoft-Login (Phase 3) + Mehrkonten
|
||||||
ipcRenderer.on('auth:health', listener);
|
authStatus: () => ipcRenderer.invoke('auth:status'),
|
||||||
return () => ipcRenderer.removeListener('auth:health', listener);
|
authAccounts: () => ipcRenderer.invoke('auth:accounts'),
|
||||||
},
|
authSetActive: (id) => ipcRenderer.invoke('auth:setActive', id),
|
||||||
// meldet sich, wenn ein Spiel startet oder endet -> Kontenliste auffrischen
|
skinProfile: () => ipcRenderer.invoke('skin:profile'),
|
||||||
onAccountsChanged: (cb) => {
|
skinChoose: () => ipcRenderer.invoke('skin:choose'),
|
||||||
const listener = () => cb();
|
skinApply: (pfad, variante) => ipcRenderer.invoke('skin:apply', pfad, variante),
|
||||||
ipcRenderer.on('accounts:changed', listener);
|
skinReset: () => ipcRenderer.invoke('skin:reset'),
|
||||||
return () => ipcRenderer.removeListener('accounts:changed', listener);
|
authRemove: (id) => ipcRenderer.invoke('auth:remove', id),
|
||||||
},
|
authLogin: () => ipcRenderer.invoke('auth:login'),
|
||||||
onAuthRefreshed: (cb) => {
|
authLogout: () => ipcRenderer.invoke('auth:logout'),
|
||||||
const listener = () => cb();
|
authHealth: () => ipcRenderer.invoke('auth:health'),
|
||||||
ipcRenderer.on('auth:refreshed', listener);
|
authRecheck: () => ipcRenderer.invoke('auth:recheck'),
|
||||||
return () => ipcRenderer.removeListener('auth:refreshed', listener);
|
onAuthHealth: (cb) => {
|
||||||
},
|
const listener = (_e, data) => cb(data);
|
||||||
onAuthCode: (cb) => {
|
ipcRenderer.on('auth:health', listener);
|
||||||
const listener = (_e, data) => cb(data);
|
return () => ipcRenderer.removeListener('auth:health', listener);
|
||||||
ipcRenderer.on('auth:code', listener);
|
},
|
||||||
return () => ipcRenderer.removeListener('auth:code', listener);
|
// meldet sich, wenn ein Spiel startet oder endet -> Kontenliste auffrischen
|
||||||
},
|
onAccountsChanged: (cb) => {
|
||||||
|
const listener = () => cb();
|
||||||
// Dialoge / App
|
ipcRenderer.on('accounts:changed', listener);
|
||||||
// MultiMC-Import
|
return () => ipcRenderer.removeListener('accounts:changed', listener);
|
||||||
mmcPickFolder: () => ipcRenderer.invoke('mmc:pickFolder'),
|
},
|
||||||
mmcScan: (folder) => ipcRenderer.invoke('mmc:scan', folder),
|
onAuthRefreshed: (cb) => {
|
||||||
mmcImport: (entries, copyData) => ipcRenderer.invoke('mmc:import', entries, copyData),
|
const listener = () => cb();
|
||||||
onMmcProgress: (cb) => {
|
ipcRenderer.on('auth:refreshed', listener);
|
||||||
const l = (_e, d) => cb(d);
|
return () => ipcRenderer.removeListener('auth:refreshed', listener);
|
||||||
ipcRenderer.on('mmc:progress', l);
|
},
|
||||||
return () => ipcRenderer.removeListener('mmc:progress', l);
|
onAuthCode: (cb) => {
|
||||||
},
|
const listener = (_e, data) => cb(data);
|
||||||
cfPickFolder: () => ipcRenderer.invoke('cf:pickFolder'),
|
ipcRenderer.on('auth:code', listener);
|
||||||
cfScan: (folder) => ipcRenderer.invoke('cf:scan', folder),
|
return () => ipcRenderer.removeListener('auth:code', listener);
|
||||||
cfImport: (entries, copyData) => ipcRenderer.invoke('cf:import', entries, copyData),
|
},
|
||||||
onCfProgress: (cb) => {
|
|
||||||
const l = (_e, d) => cb(d);
|
// Dialoge / App
|
||||||
ipcRenderer.on('cf:progress', l);
|
// MultiMC-Import
|
||||||
return () => ipcRenderer.removeListener('cf:progress', l);
|
mmcPickFolder: () => ipcRenderer.invoke('mmc:pickFolder'),
|
||||||
},
|
mmcScan: (folder) => ipcRenderer.invoke('mmc:scan', folder),
|
||||||
mcPickFolder: () => ipcRenderer.invoke('mc:pickFolder'),
|
mmcImport: (entries, copyData) => ipcRenderer.invoke('mmc:import', entries, copyData),
|
||||||
mcScan: (folder) => ipcRenderer.invoke('mc:scan', folder),
|
onMmcProgress: (cb) => {
|
||||||
mcImport: (entries, copyData) => ipcRenderer.invoke('mc:import', entries, copyData),
|
const l = (_e, d) => cb(d);
|
||||||
onMcProgress: (cb) => {
|
ipcRenderer.on('mmc:progress', l);
|
||||||
const l = (_e, d) => cb(d);
|
return () => ipcRenderer.removeListener('mmc:progress', l);
|
||||||
ipcRenderer.on('mc:progress', l);
|
},
|
||||||
return () => ipcRenderer.removeListener('mc:progress', l);
|
cfPickFolder: () => ipcRenderer.invoke('cf:pickFolder'),
|
||||||
},
|
cfScan: (folder) => ipcRenderer.invoke('cf:scan', folder),
|
||||||
technicPickFolder: () => ipcRenderer.invoke('technic:pickFolder'),
|
cfImport: (entries, copyData) => ipcRenderer.invoke('cf:import', entries, copyData),
|
||||||
technicScan: (folder) => ipcRenderer.invoke('technic:scan', folder),
|
onCfProgress: (cb) => {
|
||||||
technicImport: (entries, copyData) => ipcRenderer.invoke('technic:import', entries, copyData),
|
const l = (_e, d) => cb(d);
|
||||||
onTechnicProgress: (cb) => {
|
ipcRenderer.on('cf:progress', l);
|
||||||
const l = (_e, d) => cb(d);
|
return () => ipcRenderer.removeListener('cf:progress', l);
|
||||||
ipcRenderer.on('technic:progress', l);
|
},
|
||||||
return () => ipcRenderer.removeListener('technic:progress', l);
|
mcPickFolder: () => ipcRenderer.invoke('mc:pickFolder'),
|
||||||
},
|
mcScan: (folder) => ipcRenderer.invoke('mc:scan', folder),
|
||||||
technicPlatformSearch: (query) => ipcRenderer.invoke('technicPlatform:search', query),
|
mcImport: (entries, copyData) => ipcRenderer.invoke('mc:import', entries, copyData),
|
||||||
technicPlatformInstall: (slug) => ipcRenderer.invoke('technicPlatform:install', slug),
|
onMcProgress: (cb) => {
|
||||||
onTechnicPlatformProgress: (cb) => {
|
const l = (_e, d) => cb(d);
|
||||||
const l = (_e, d) => cb(d);
|
ipcRenderer.on('mc:progress', l);
|
||||||
ipcRenderer.on('technicPlatform:progress', l);
|
return () => ipcRenderer.removeListener('mc:progress', l);
|
||||||
return () => ipcRenderer.removeListener('technicPlatform:progress', l);
|
},
|
||||||
},
|
technicPickFolder: () => ipcRenderer.invoke('technic:pickFolder'),
|
||||||
|
technicScan: (folder) => ipcRenderer.invoke('technic:scan', folder),
|
||||||
// Bedrock (Windows-Store-Ausgabe)
|
technicImport: (entries, copyData) => ipcRenderer.invoke('technic:import', entries, copyData),
|
||||||
bedrockDetect: () => ipcRenderer.invoke('bedrock:detect'),
|
onTechnicProgress: (cb) => {
|
||||||
bedrockLaunch: () => ipcRenderer.invoke('bedrock:launch'),
|
const l = (_e, d) => cb(d);
|
||||||
bedrockWorlds: () => ipcRenderer.invoke('bedrock:worlds'),
|
ipcRenderer.on('technic:progress', l);
|
||||||
bedrockList: (kind) => ipcRenderer.invoke('bedrock:list', kind),
|
return () => ipcRenderer.removeListener('technic:progress', l);
|
||||||
bedrockRemove: (kind, name) => ipcRenderer.invoke('bedrock:remove', kind, name),
|
},
|
||||||
bedrockOpenFolder: (kind) => ipcRenderer.invoke('bedrock:openFolder', kind),
|
technicPlatformSearch: (query) => ipcRenderer.invoke('technicPlatform:search', query),
|
||||||
bedrockInstallAddon: () => ipcRenderer.invoke('bedrock:installAddon'),
|
technicPlatformInstall: (slug) => ipcRenderer.invoke('technicPlatform:install', slug),
|
||||||
bedrockOpenWorld: (dir) => ipcRenderer.invoke('bedrock:openWorld', dir),
|
onTechnicPlatformProgress: (cb) => {
|
||||||
|
const l = (_e, d) => cb(d);
|
||||||
// echte Minecraft-Icons
|
ipcRenderer.on('technicPlatform:progress', l);
|
||||||
iconsList: () => ipcRenderer.invoke('icons:list'),
|
return () => ipcRenderer.removeListener('technicPlatform:progress', l);
|
||||||
iconsExtract: () => ipcRenderer.invoke('icons:extract'),
|
},
|
||||||
|
|
||||||
// Updates
|
// Bedrock (Windows-Store-Ausgabe)
|
||||||
updateCheck: () => ipcRenderer.invoke('update:check'),
|
bedrockDetect: () => ipcRenderer.invoke('bedrock:detect'),
|
||||||
updateInstall: (asset) => ipcRenderer.invoke('update:install', asset),
|
bedrockLaunch: () => ipcRenderer.invoke('bedrock:launch'),
|
||||||
onUpdateAvailable: (cb) => {
|
bedrockWorlds: () => ipcRenderer.invoke('bedrock:worlds'),
|
||||||
const l = (_e, d) => cb(d);
|
bedrockList: (kind) => ipcRenderer.invoke('bedrock:list', kind),
|
||||||
ipcRenderer.on('update:available', l);
|
bedrockRemove: (kind, name) => ipcRenderer.invoke('bedrock:remove', kind, name),
|
||||||
return () => ipcRenderer.removeListener('update:available', l);
|
bedrockOpenFolder: (kind) => ipcRenderer.invoke('bedrock:openFolder', kind),
|
||||||
},
|
bedrockInstallAddon: () => ipcRenderer.invoke('bedrock:installAddon'),
|
||||||
// Ergebnis des automatischen Checks beim Start – auch wenn kein Update
|
bedrockOpenWorld: (dir) => ipcRenderer.invoke('bedrock:openWorld', dir),
|
||||||
// verfügbar ist (im Unterschied zu onUpdateAvailable, das nur bei
|
|
||||||
// verfügbarem Update feuert)
|
// echte Minecraft-Icons
|
||||||
onUpdateChecked: (cb) => {
|
iconsList: () => ipcRenderer.invoke('icons:list'),
|
||||||
const l = (_e, d) => cb(d);
|
iconsExtract: () => ipcRenderer.invoke('icons:extract'),
|
||||||
ipcRenderer.on('update:checked', l);
|
|
||||||
return () => ipcRenderer.removeListener('update:checked', l);
|
// Updates
|
||||||
},
|
updateCheck: () => ipcRenderer.invoke('update:check'),
|
||||||
onUpdateProgress: (cb) => {
|
updateLastCheck: () => ipcRenderer.invoke('update:lastCheck'),
|
||||||
const l = (_e, d) => cb(d);
|
updateInstall: (asset) => ipcRenderer.invoke('update:install', asset),
|
||||||
ipcRenderer.on('update:progress', l);
|
onUpdateAvailable: (cb) => {
|
||||||
return () => ipcRenderer.removeListener('update:progress', l);
|
const l = (_e, d) => cb(d);
|
||||||
},
|
ipcRenderer.on('update:available', l);
|
||||||
|
return () => ipcRenderer.removeListener('update:available', l);
|
||||||
openExternal: (url) => ipcRenderer.invoke('app:openExternal', url),
|
},
|
||||||
pickFolder: (title) => ipcRenderer.invoke('dialog:pickFolder', title),
|
// Ergebnis des automatischen Checks beim Start – auch wenn kein Update
|
||||||
pickJava: () => ipcRenderer.invoke('dialog:pickJava'),
|
// verfügbar ist (im Unterschied zu onUpdateAvailable, das nur bei
|
||||||
appInfo: () => ipcRenderer.invoke('app:info'),
|
// verfügbarem Update feuert)
|
||||||
appLicense: () => ipcRenderer.invoke('app:license'),
|
onUpdateChecked: (cb) => {
|
||||||
|
const l = (_e, d) => cb(d);
|
||||||
// Einstellungen/Konsole als eigenes, frei verschiebbares Fenster
|
ipcRenderer.on('update:checked', l);
|
||||||
popoutOpen: (modalId, params) => ipcRenderer.send('popout:open', modalId, params),
|
return () => ipcRenderer.removeListener('update:checked', l);
|
||||||
popoutHide: () => ipcRenderer.send('popout:hide'),
|
},
|
||||||
onPopoutReopen: (cb) => {
|
onUpdateProgress: (cb) => {
|
||||||
const l = (_e, params) => cb(params);
|
const l = (_e, d) => cb(d);
|
||||||
ipcRenderer.on('popout:reopen', l);
|
ipcRenderer.on('update:progress', l);
|
||||||
return () => ipcRenderer.removeListener('popout:reopen', l);
|
return () => ipcRenderer.removeListener('update:progress', l);
|
||||||
},
|
},
|
||||||
onSettingsChanged: (cb) => {
|
|
||||||
const l = (_e, s) => cb(s);
|
openExternal: (url) => ipcRenderer.invoke('app:openExternal', url),
|
||||||
ipcRenderer.on('settings:changed', l);
|
pickFolder: (title) => ipcRenderer.invoke('dialog:pickFolder', title),
|
||||||
return () => ipcRenderer.removeListener('settings:changed', l);
|
pickJava: () => ipcRenderer.invoke('dialog:pickJava'),
|
||||||
},
|
appInfo: () => ipcRenderer.invoke('app:info'),
|
||||||
onInstancesChanged: (cb) => {
|
appLicense: () => ipcRenderer.invoke('app:license'),
|
||||||
const l = () => cb();
|
|
||||||
ipcRenderer.on('instances:changed', l);
|
// Einstellungen/Konsole als eigenes, frei verschiebbares Fenster
|
||||||
return () => ipcRenderer.removeListener('instances:changed', l);
|
popoutOpen: (modalId, params) => ipcRenderer.send('popout:open', modalId, params),
|
||||||
},
|
popoutHide: () => ipcRenderer.send('popout:hide'),
|
||||||
onSkinChanged: (cb) => {
|
onPopoutReopen: (cb) => {
|
||||||
const l = () => cb();
|
const l = (_e, params) => cb(params);
|
||||||
ipcRenderer.on('skin:changed', l);
|
ipcRenderer.on('popout:reopen', l);
|
||||||
return () => ipcRenderer.removeListener('skin:changed', l);
|
return () => ipcRenderer.removeListener('popout:reopen', l);
|
||||||
},
|
},
|
||||||
updateGetPending: () => ipcRenderer.invoke('update:getPending'),
|
onSettingsChanged: (cb) => {
|
||||||
|
const l = (_e, s) => cb(s);
|
||||||
// Spielaufzeichnung
|
ipcRenderer.on('settings:changed', l);
|
||||||
recordingListWindows: () => ipcRenderer.invoke('recording:listWindows'),
|
return () => ipcRenderer.removeListener('settings:changed', l);
|
||||||
recordingGetAudioSourceId: () => ipcRenderer.invoke('recording:getAudioSourceId'),
|
},
|
||||||
recordingSave: (instanceName, buffer, serverLabel, fps) => ipcRenderer.invoke('recording:save', instanceName, buffer, serverLabel, fps),
|
onInstancesChanged: (cb) => {
|
||||||
recordingOpenFolder: (instanceName) => ipcRenderer.invoke('recording:openFolder', instanceName),
|
const l = () => cb();
|
||||||
recordingList: (instanceName) => ipcRenderer.invoke('recording:list', instanceName),
|
ipcRenderer.on('instances:changed', l);
|
||||||
recordingThumbnail: (instanceName, dateiname) => ipcRenderer.invoke('recording:thumbnail', instanceName, dateiname),
|
return () => ipcRenderer.removeListener('instances:changed', l);
|
||||||
recordingDelete: (instanceName, dateiname) => ipcRenderer.invoke('recording:delete', instanceName, dateiname),
|
},
|
||||||
recordingOpen: (instanceName, dateiname) => ipcRenderer.invoke('recording:open', instanceName, dateiname),
|
onSkinChanged: (cb) => {
|
||||||
|
const l = () => cb();
|
||||||
// Freundesystem
|
ipcRenderer.on('skin:changed', l);
|
||||||
friendsEnabled: () => ipcRenderer.invoke('friends:enabled'),
|
return () => ipcRenderer.removeListener('skin:changed', l);
|
||||||
friendsList: () => ipcRenderer.invoke('friends:list'),
|
},
|
||||||
friendsRequests: () => ipcRenderer.invoke('friends:requests'),
|
updateGetPending: () => ipcRenderer.invoke('update:getPending'),
|
||||||
friendsAdd: (targetUsername) => ipcRenderer.invoke('friends:add', targetUsername),
|
|
||||||
friendsRespond: (otherUuid, accept) => ipcRenderer.invoke('friends:respond', otherUuid, accept),
|
// Spielaufzeichnung
|
||||||
friendsRemove: (otherUuid) => ipcRenderer.invoke('friends:remove', otherUuid),
|
recordingListWindows: () => ipcRenderer.invoke('recording:listWindows'),
|
||||||
friendsSentRequests: () => ipcRenderer.invoke('friends:sentRequests'),
|
recordingGetAudioSourceId: () => ipcRenderer.invoke('recording:getAudioSourceId'),
|
||||||
friendsCancel: (otherUuid) => ipcRenderer.invoke('friends:cancel', otherUuid),
|
recordingSave: (instanceName, buffer, serverLabel, fps) => ipcRenderer.invoke('recording:save', instanceName, buffer, serverLabel, fps),
|
||||||
friendsSetNickname: (uuid, nickname) => ipcRenderer.invoke('friends:setNickname', uuid, nickname),
|
recordingOpenFolder: (instanceName) => ipcRenderer.invoke('recording:openFolder', instanceName),
|
||||||
friendsRequestCount: () => ipcRenderer.invoke('friends:requestCount'),
|
recordingList: (instanceName) => ipcRenderer.invoke('recording:list', instanceName),
|
||||||
onFriendsRequestCount: (cb) => {
|
recordingThumbnail: (instanceName, dateiname) => ipcRenderer.invoke('recording:thumbnail', instanceName, dateiname),
|
||||||
const l = (_e, zahl) => cb(zahl);
|
recordingDelete: (instanceName, dateiname) => ipcRenderer.invoke('recording:delete', instanceName, dateiname),
|
||||||
ipcRenderer.on('friends:requestCount', l);
|
recordingOpen: (instanceName, dateiname) => ipcRenderer.invoke('recording:open', instanceName, dateiname),
|
||||||
return () => ipcRenderer.removeListener('friends:requestCount', l);
|
|
||||||
},
|
// Freundesystem
|
||||||
recordingGetServer: (instanceId) => ipcRenderer.invoke('recording:getServer', instanceId),
|
friendsEnabled: () => ipcRenderer.invoke('friends:enabled'),
|
||||||
onRecordingServerDetected: (cb) => {
|
friendsList: () => ipcRenderer.invoke('friends:list'),
|
||||||
const l = (_e, payload) => cb(payload);
|
friendsRequests: () => ipcRenderer.invoke('friends:requests'),
|
||||||
ipcRenderer.on('recording:serverDetected', l);
|
friendsAdd: (targetUsername) => ipcRenderer.invoke('friends:add', targetUsername),
|
||||||
return () => ipcRenderer.removeListener('recording:serverDetected', l);
|
friendsRespond: (otherUuid, accept) => ipcRenderer.invoke('friends:respond', otherUuid, accept),
|
||||||
},
|
friendsRemove: (otherUuid) => ipcRenderer.invoke('friends:remove', otherUuid),
|
||||||
recordingGetPid: (instanceId) => ipcRenderer.invoke('recording:getPid', instanceId),
|
friendsSentRequests: () => ipcRenderer.invoke('friends:sentRequests'),
|
||||||
recordingSetCaptureTarget: (sourceId) => ipcRenderer.invoke('recording:setCaptureTarget', sourceId),
|
friendsCancel: (otherUuid) => ipcRenderer.invoke('friends:cancel', otherUuid),
|
||||||
recordingGetWindowBounds: (instanceId, fensterTitel) => ipcRenderer.invoke('recording:getWindowBounds', instanceId, fensterTitel),
|
friendsSetNickname: (uuid, nickname) => ipcRenderer.invoke('friends:setNickname', uuid, nickname),
|
||||||
recordingStartGameAudio: (pid) => ipcRenderer.invoke('recording:startGameAudio', pid),
|
friendsRequestCount: () => ipcRenderer.invoke('friends:requestCount'),
|
||||||
recordingStopGameAudio: () => ipcRenderer.invoke('recording:stopGameAudio'),
|
friendsMessages: (otherUuid) => ipcRenderer.invoke('friends:messages', otherUuid),
|
||||||
onGameAudioChunk: (cb) => {
|
friendsSendMessage: (otherUuid, text) => ipcRenderer.invoke('friends:sendMessage', otherUuid, text),
|
||||||
const l = (_e, chunk) => cb(chunk);
|
friendsUnreadByFriend: () => ipcRenderer.invoke('friends:unreadByFriend'),
|
||||||
ipcRenderer.on('recording:gameAudioChunk', l);
|
onFriendsMessage: (cb) => {
|
||||||
return () => ipcRenderer.removeListener('recording:gameAudioChunk', l);
|
const l = (_e, n) => cb(n);
|
||||||
},
|
ipcRenderer.on('friends:message', l);
|
||||||
recordingSetGuard: (active) => ipcRenderer.send('recording:guard', active),
|
return () => ipcRenderer.removeListener('friends:message', l);
|
||||||
onRecordCloseAttempt: (cb) => {
|
},
|
||||||
const l = () => cb();
|
crashReport: (id, seit, wahl) => ipcRenderer.invoke('crash:report', id, seit, wahl),
|
||||||
ipcRenderer.on('record:closeAttempt', l);
|
clipSetActive: (active, instanceId) => ipcRenderer.invoke('clip:setActive', active, instanceId),
|
||||||
return () => ipcRenderer.removeListener('record:closeAttempt', l);
|
clipSave: (data) => ipcRenderer.invoke('clip:save', data),
|
||||||
},
|
onClipHotkey: (cb) => {
|
||||||
});
|
const l = () => cb();
|
||||||
|
ipcRenderer.on('clip:hotkey', l);
|
||||||
|
return () => ipcRenderer.removeListener('clip:hotkey', l);
|
||||||
|
},
|
||||||
|
onUiToast: (cb) => {
|
||||||
|
const l = (_e, t) => cb(t);
|
||||||
|
ipcRenderer.on('ui:toast', l);
|
||||||
|
return () => ipcRenderer.removeListener('ui:toast', l);
|
||||||
|
},
|
||||||
|
friendsJoin: (server, version) => ipcRenderer.invoke('friends:join', server, version),
|
||||||
|
friendsJoinNew: (server, version) => ipcRenderer.invoke('friends:joinNew', server, version),
|
||||||
|
friendsMyServer: () => ipcRenderer.invoke('friends:myServer'),
|
||||||
|
friendsInvite: (targetUuid) => ipcRenderer.invoke('friends:invite', targetUuid),
|
||||||
|
friendsInvites: () => ipcRenderer.invoke('friends:invites'),
|
||||||
|
friendsDismissInvite: (id) => ipcRenderer.invoke('friends:dismissInvite', id),
|
||||||
|
onFriendsInvite: (cb) => {
|
||||||
|
const l = (_e, einladung) => cb(einladung);
|
||||||
|
ipcRenderer.on('friends:invite', l);
|
||||||
|
return () => ipcRenderer.removeListener('friends:invite', l);
|
||||||
|
},
|
||||||
|
onFriendsRequestCount: (cb) => {
|
||||||
|
const l = (_e, zahl) => cb(zahl);
|
||||||
|
ipcRenderer.on('friends:requestCount', l);
|
||||||
|
return () => ipcRenderer.removeListener('friends:requestCount', l);
|
||||||
|
},
|
||||||
|
recordingGetServer: (instanceId) => ipcRenderer.invoke('recording:getServer', instanceId),
|
||||||
|
onRecordingServerDetected: (cb) => {
|
||||||
|
const l = (_e, payload) => cb(payload);
|
||||||
|
ipcRenderer.on('recording:serverDetected', l);
|
||||||
|
return () => ipcRenderer.removeListener('recording:serverDetected', l);
|
||||||
|
},
|
||||||
|
recordingGetPid: (instanceId) => ipcRenderer.invoke('recording:getPid', instanceId),
|
||||||
|
recordingSetCaptureTarget: (sourceId) => ipcRenderer.invoke('recording:setCaptureTarget', sourceId),
|
||||||
|
recordingGetWindowBounds: (instanceId, fensterTitel) => ipcRenderer.invoke('recording:getWindowBounds', instanceId, fensterTitel),
|
||||||
|
recordingStartGameAudio: (pid) => ipcRenderer.invoke('recording:startGameAudio', pid),
|
||||||
|
recordingStopGameAudio: () => ipcRenderer.invoke('recording:stopGameAudio'),
|
||||||
|
onGameAudioChunk: (cb) => {
|
||||||
|
const l = (_e, chunk) => cb(chunk);
|
||||||
|
ipcRenderer.on('recording:gameAudioChunk', l);
|
||||||
|
return () => ipcRenderer.removeListener('recording:gameAudioChunk', l);
|
||||||
|
},
|
||||||
|
recordingSetGuard: (active) => ipcRenderer.send('recording:guard', active),
|
||||||
|
onRecordCloseAttempt: (cb) => {
|
||||||
|
const l = () => cb();
|
||||||
|
ipcRenderer.on('record:closeAttempt', l);
|
||||||
|
return () => ipcRenderer.removeListener('record:closeAttempt', l);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
+6422
-5607
File diff suppressed because it is too large
Load Diff
+854
-832
File diff suppressed because it is too large
Load Diff
+502
-495
@@ -1,495 +1,502 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* store.js – Persistenz für AeroMC
|
* store.js – Persistenz für AeroMC
|
||||||
* ------------------------------------
|
* ------------------------------------
|
||||||
* Jede Instanz ist ein isolierter Ordner (MultiMC-Prinzip):
|
* Jede Instanz ist ein isolierter Ordner (MultiMC-Prinzip):
|
||||||
* <instancesDir>/<id>/instance.json – Metadaten
|
* <instancesDir>/<id>/instance.json – Metadaten
|
||||||
* <instancesDir>/<id>/minecraft/ – das eigentliche Spielverzeichnis (.minecraft-Ersatz)
|
* <instancesDir>/<id>/minecraft/ – das eigentliche Spielverzeichnis (.minecraft-Ersatz)
|
||||||
*
|
*
|
||||||
* Globale Einstellungen liegen in <userData>/settings.json.
|
* Globale Einstellungen liegen in <userData>/settings.json.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
|
|
||||||
let USER_DATA = null; // wird von main.js via init() gesetzt
|
let USER_DATA = null; // wird von main.js via init() gesetzt
|
||||||
|
|
||||||
function init(userDataPath) {
|
function init(userDataPath) {
|
||||||
USER_DATA = userDataPath;
|
USER_DATA = userDataPath;
|
||||||
ensureDir(USER_DATA);
|
ensureDir(USER_DATA);
|
||||||
ensureDir(getInstancesDir());
|
ensureDir(getInstancesDir());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- kleine Helfer ----------
|
// ---------- kleine Helfer ----------
|
||||||
|
|
||||||
function ensureDir(dir) {
|
function ensureDir(dir) {
|
||||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
function readJson(file, fallback) {
|
function readJson(file, fallback) {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||||
} catch {
|
} catch {
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeJson(file, data) {
|
function writeJson(file, data) {
|
||||||
ensureDir(path.dirname(file));
|
ensureDir(path.dirname(file));
|
||||||
fs.writeFileSync(file, JSON.stringify(data, null, 2), 'utf8');
|
fs.writeFileSync(file, JSON.stringify(data, null, 2), 'utf8');
|
||||||
}
|
}
|
||||||
|
|
||||||
function newId() {
|
function newId() {
|
||||||
return crypto.randomBytes(8).toString('hex');
|
return crypto.randomBytes(8).toString('hex');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Einstellungen ----------
|
// ---------- Einstellungen ----------
|
||||||
|
|
||||||
const DEFAULT_SETTINGS = {
|
const DEFAULT_SETTINGS = {
|
||||||
instancesDir: null, // null => Standard (userData/instances)
|
instancesDir: null, // null => Standard (userData/instances)
|
||||||
javaPath: '', // leer => "java" aus PATH
|
javaPath: '', // leer => "java" aus PATH
|
||||||
autoJava: true, // fehlende Java-Version bei Bedarf selbst herunterladen
|
autoJava: true, // fehlende Java-Version bei Bedarf selbst herunterladen
|
||||||
autoBackupEnabled: false, // Sicherungen nach Zeitplan – standardmäßig aus
|
autoBackupEnabled: false, // Sicherungen nach Zeitplan – standardmäßig aus
|
||||||
autoBackupEvery: 'daily', // 'daily' | 'weekly'
|
autoBackupEvery: 'daily', // 'daily' | 'weekly'
|
||||||
autoBackupBeforeLaunch: false, // zusätzlich vor jedem Spielstart
|
autoBackupBeforeLaunch: false, // zusätzlich vor jedem Spielstart
|
||||||
autoBackupKeep: 5, // wie viele automatische Sicherungen behalten werden
|
autoBackupKeep: 5, // wie viele automatische Sicherungen behalten werden
|
||||||
autoBackupLast: '', // Zeitpunkt der letzten automatischen Sicherung
|
autoBackupLast: '', // Zeitpunkt der letzten automatischen Sicherung
|
||||||
defaultMinMemMb: 2048,
|
defaultMinMemMb: 2048,
|
||||||
defaultMaxMemMb: 4096,
|
defaultMaxMemMb: 4096,
|
||||||
azureClientId: '', // Azure-App-ID für den Microsoft-Login (Phase 3)
|
azureClientId: '', // Azure-App-ID für den Microsoft-Login (Phase 3)
|
||||||
curseforgeApiKey: '', // eigener CurseForge-API-Key (hat Vorrang)
|
curseforgeApiKey: '', // eigener CurseForge-API-Key (hat Vorrang)
|
||||||
curseforgeProxyUrl: '', // sonst: Adresse der eigenen CurseForge-Weiterleitung
|
curseforgeProxyUrl: '', // sonst: Adresse der eigenen CurseForge-Weiterleitung
|
||||||
curseforgeProxyToken: '', // optionales Geheimnis für diese Weiterleitung
|
curseforgeProxyToken: '', // optionales Geheimnis für diese Weiterleitung
|
||||||
globalPreLaunch: '',
|
globalPreLaunch: '',
|
||||||
globalPostExit: '',
|
globalPostExit: '',
|
||||||
lastSelectedId: '', // zuletzt ausgewählte Instanz (beim Start wieder markieren)
|
lastSelectedId: '', // zuletzt ausgewählte Instanz (beim Start wieder markieren)
|
||||||
showConsoleOnLaunch: false, // Konsole beim Spielstart anzeigen
|
showConsoleOnLaunch: false, // Konsole beim Spielstart anzeigen
|
||||||
autoCloseConsole: false, // Konsole nach Spielende automatisch schließen
|
autoCloseConsole: false, // Konsole nach Spielende automatisch schließen
|
||||||
showConsoleOnCrash: true, // Konsole anzeigen, wenn das Spiel abstürzt
|
showConsoleOnCrash: true, // Konsole anzeigen, wenn das Spiel abstürzt
|
||||||
consoleMaxLines: 100000, // Zeilenbegrenzung der Konsole
|
consoleMaxLines: 100000, // Zeilenbegrenzung der Konsole
|
||||||
mcMaximized: false, // Minecraft maximiert starten
|
mcMaximized: false, // Minecraft maximiert starten
|
||||||
mcWidth: 854,
|
mcWidth: 854,
|
||||||
mcHeight: 480,
|
mcHeight: 480,
|
||||||
sortBy: 'lastPlayed', // lastPlayed | name
|
sortBy: 'lastPlayed', // lastPlayed | name
|
||||||
iso3dIcons: true, // Block-Symbole räumlich darstellen
|
iso3dIcons: true, // Block-Symbole räumlich darstellen
|
||||||
bedrockEnabled: true, // Bedrock-Bereich anzeigen (nur wenn auch installiert)
|
bedrockEnabled: true, // Bedrock-Bereich anzeigen (nur wenn auch installiert)
|
||||||
desktopNotifications: true, // Windows-Meldung bei fertigem Hintergrund-Download/-Backup
|
desktopNotifications: true, // Windows-Meldung bei fertigem Hintergrund-Download/-Backup
|
||||||
friendsEnabled: true, // Freundesystem: eigene Online-Meldung + auffindbar für Anfragen
|
friendsEnabled: true,
|
||||||
friendNicknames: {}, // rein lokal: uuid -> eigener Spitzname für einen Freund
|
friendsShareServer: true, // Freunde sehen, auf welchem Server man spielt (für "Beitreten")
|
||||||
autostart: false, // mit Windows starten, im Infobereich statt als Fenster
|
autoModUpdates: false, // vor jedem Start Mods über Modrinth aktualisieren
|
||||||
discordClientId: '', // optional, nur per Hand in settings.json; leer => eingebaute AeroMC-App-ID
|
schnellstart: {}, // Strg+1 … Strg+5 -> Instanz-ID ({ "1": "Survival", … })
|
||||||
bgSkin: '', // '' = aus, 'active' = aktives Konto, sonst Konto-UUID
|
autoWorldBackup: true, // nach jeder Spielsitzung die zuletzt gespielte Welt sichern (letzte 5 Stände) // Freundesystem: eigene Online-Meldung + auffindbar für Anfragen
|
||||||
bgSkinOpacity: 55, // Deckkraft des Hintergrund-Skins in %
|
friendNicknames: {}, // rein lokal: uuid -> eigener Spitzname für einen Freund
|
||||||
bgSkinPose: 'walking', // Pose des 3D-Renders (falls der Pose-Dienst erreichbar ist)
|
autostart: false, // mit Windows starten, im Infobereich statt als Fenster
|
||||||
bgSkinCycle: false, // bei 2+ angemeldeten Konten automatisch durchwechseln
|
discordClientId: '', // optional, nur per Hand in settings.json; leer => eingebaute AeroMC-App-ID
|
||||||
bgSkinCycleSeconds: 30, // Sekunden zwischen zwei Kontowechseln
|
bgSkin: '', // '' = aus, 'active' = aktives Konto, sonst Konto-UUID
|
||||||
wrapperCommand: '', // Externe Programme: Startprogramm, das Java umschließt
|
bgSkinOpacity: 55, // Deckkraft des Hintergrund-Skins in %
|
||||||
proxyEnabled: false, // HTTP-Proxy für Downloads/Netzwerk
|
bgSkinPose: 'walking', // Pose des 3D-Renders (falls der Pose-Dienst erreichbar ist)
|
||||||
proxyHost: '',
|
bgSkinCycle: false, // bei 2+ angemeldeten Konten automatisch durchwechseln
|
||||||
proxyPort: '',
|
bgSkinCycleSeconds: 30, // Sekunden zwischen zwei Kontowechseln
|
||||||
proxyUser: '',
|
wrapperCommand: '', // Externe Programme: Startprogramm, das Java umschließt
|
||||||
proxyPass: '',
|
proxyEnabled: false, // HTTP-Proxy für Downloads/Netzwerk
|
||||||
theme: 'dark',
|
proxyHost: '',
|
||||||
// Spielaufzeichnung - bleibt über Neustarts erhalten (letzte Wahl je Feld)
|
proxyPort: '',
|
||||||
recordingResolution: 'native',
|
proxyUser: '',
|
||||||
recordingQuality: 'medium',
|
proxyPass: '',
|
||||||
recordingFramerate: 30,
|
theme: 'dark',
|
||||||
recordingAudioSystem: true,
|
// Spielaufzeichnung - bleibt über Neustarts erhalten (letzte Wahl je Feld)
|
||||||
recordingAudioGame: false,
|
recordingResolution: 'native',
|
||||||
recordingVolSystem: 100,
|
recordingQuality: 'medium',
|
||||||
recordingVolGame: 100,
|
recordingFramerate: 30,
|
||||||
recordingMicSelected: [], // Liste gewählter Mikrofon-deviceIds
|
recordingAudioSystem: true,
|
||||||
recordingMicVolumes: {}, // deviceId -> Lautstärke (0-100)
|
recordingAudioGame: false,
|
||||||
};
|
recordingVolSystem: 100,
|
||||||
|
recordingVolGame: 100,
|
||||||
function settingsFile() {
|
recordingMicSelected: [], // Liste gewählter Mikrofon-deviceIds
|
||||||
return path.join(USER_DATA, 'settings.json');
|
recordingMicVolumes: {}, // deviceId -> Lautstärke (0-100)
|
||||||
}
|
};
|
||||||
|
|
||||||
function getSettings() {
|
function settingsFile() {
|
||||||
return Object.assign({}, DEFAULT_SETTINGS, readJson(settingsFile(), {}));
|
return path.join(USER_DATA, 'settings.json');
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveSettings(patch) {
|
function getSettings() {
|
||||||
const merged = Object.assign({}, getSettings(), patch || {});
|
return Object.assign({}, DEFAULT_SETTINGS, readJson(settingsFile(), {}));
|
||||||
writeJson(settingsFile(), merged);
|
}
|
||||||
// Falls sich das Instanzverzeichnis geändert hat: sicherstellen, dass es existiert
|
|
||||||
ensureDir(getInstancesDir());
|
function saveSettings(patch) {
|
||||||
return merged;
|
const merged = Object.assign({}, getSettings(), patch || {});
|
||||||
}
|
writeJson(settingsFile(), merged);
|
||||||
|
// Falls sich das Instanzverzeichnis geändert hat: sicherstellen, dass es existiert
|
||||||
// ---------- Instanzen ----------
|
ensureDir(getInstancesDir());
|
||||||
|
return merged;
|
||||||
function getInstancesDir() {
|
}
|
||||||
const s = getSettings();
|
|
||||||
return s.instancesDir && s.instancesDir.trim()
|
// ---------- Instanzen ----------
|
||||||
? s.instancesDir
|
|
||||||
: path.join(USER_DATA, 'instances');
|
function getInstancesDir() {
|
||||||
}
|
const s = getSettings();
|
||||||
|
return s.instancesDir && s.instancesDir.trim()
|
||||||
function instancePath(id) {
|
? s.instancesDir
|
||||||
return path.join(getInstancesDir(), id);
|
: path.join(USER_DATA, 'instances');
|
||||||
}
|
}
|
||||||
|
|
||||||
function instanceFile(id) {
|
function instancePath(id) {
|
||||||
return path.join(instancePath(id), 'instance.json');
|
return path.join(getInstancesDir(), id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function gameDir(id) {
|
function instanceFile(id) {
|
||||||
return path.join(instancePath(id), 'minecraft');
|
return path.join(instancePath(id), 'instance.json');
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
function gameDir(id) {
|
||||||
* Inhalte je Art: Mods, Ressourcenpakete und Shader liegen in verschiedenen
|
return path.join(instancePath(id), 'minecraft');
|
||||||
* Ordnern des Spielverzeichnisses und werden getrennt verzeichnet.
|
}
|
||||||
* "mod" behält die bisherigen Namen, damit vorhandene Instanzen unverändert
|
|
||||||
* weiterlaufen.
|
/*
|
||||||
*/
|
* Inhalte je Art: Mods, Ressourcenpakete und Shader liegen in verschiedenen
|
||||||
const INHALT_ORDNER = {
|
* Ordnern des Spielverzeichnisses und werden getrennt verzeichnet.
|
||||||
mod: 'mods', resourcepack: 'resourcepacks', shader: 'shaderpacks', datapack: 'datapacks',
|
* "mod" behält die bisherigen Namen, damit vorhandene Instanzen unverändert
|
||||||
};
|
* weiterlaufen.
|
||||||
const INHALT_INDEX = {
|
*/
|
||||||
mod: 'mods.json', resourcepack: 'resourcepacks.json', shader: 'shaders.json', datapack: 'datapacks.json',
|
const INHALT_ORDNER = {
|
||||||
};
|
mod: 'mods', resourcepack: 'resourcepacks', shader: 'shaderpacks', datapack: 'datapacks',
|
||||||
|
};
|
||||||
function inhaltsDir(id, art = 'mod') {
|
const INHALT_INDEX = {
|
||||||
return path.join(gameDir(id), INHALT_ORDNER[art] || INHALT_ORDNER.mod);
|
mod: 'mods.json', resourcepack: 'resourcepacks.json', shader: 'shaders.json', datapack: 'datapacks.json',
|
||||||
}
|
};
|
||||||
|
|
||||||
function modsDir(id, art = 'mod') {
|
function inhaltsDir(id, art = 'mod') {
|
||||||
return inhaltsDir(id, art);
|
return path.join(gameDir(id), INHALT_ORDNER[art] || INHALT_ORDNER.mod);
|
||||||
}
|
}
|
||||||
|
|
||||||
function modsIndexFile(id, art = 'mod') {
|
function modsDir(id, art = 'mod') {
|
||||||
return path.join(instancePath(id), INHALT_INDEX[art] || INHALT_INDEX.mod);
|
return inhaltsDir(id, art);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- installierte Mods (Metadaten je Instanz) ----------
|
function modsIndexFile(id, art = 'mod') {
|
||||||
|
return path.join(instancePath(id), INHALT_INDEX[art] || INHALT_INDEX.mod);
|
||||||
/*
|
}
|
||||||
* Abgeschaltete Mods heißen "<name>.jar.disabled" – so übergeht Minecraft sie,
|
|
||||||
* ohne dass die Datei verloren geht. In mods.json steht immer der eingeschaltete
|
// ---------- installierte Mods (Metadaten je Instanz) ----------
|
||||||
* Name; ob eine Mod an ist, entscheidet allein die Datei auf der Platte.
|
|
||||||
*/
|
/*
|
||||||
const MOD_ENDUNG = /\.(jar|zip)$/i;
|
* Abgeschaltete Mods heißen "<name>.jar.disabled" – so übergeht Minecraft sie,
|
||||||
const AUS_ENDUNG = /\.(jar|zip)\.disabled$/i;
|
* ohne dass die Datei verloren geht. In mods.json steht immer der eingeschaltete
|
||||||
|
* Name; ob eine Mod an ist, entscheidet allein die Datei auf der Platte.
|
||||||
function scanModsDir(id, art = 'mod') {
|
*/
|
||||||
const dir = modsDir(id, art);
|
const MOD_ENDUNG = /\.(jar|zip)$/i;
|
||||||
if (!fs.existsSync(dir)) return [];
|
const AUS_ENDUNG = /\.(jar|zip)\.disabled$/i;
|
||||||
const gefunden = [];
|
|
||||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
function scanModsDir(id, art = 'mod') {
|
||||||
if (!entry.isFile()) continue;
|
const dir = modsDir(id, art);
|
||||||
const aus = AUS_ENDUNG.test(entry.name);
|
if (!fs.existsSync(dir)) return [];
|
||||||
if (!aus && !MOD_ENDUNG.test(entry.name)) continue;
|
const gefunden = [];
|
||||||
const filename = aus ? entry.name.replace(/\.disabled$/i, '') : entry.name;
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||||
gefunden.push({
|
if (!entry.isFile()) continue;
|
||||||
projectId: null,
|
const aus = AUS_ENDUNG.test(entry.name);
|
||||||
title: filename.replace(MOD_ENDUNG, ''),
|
if (!aus && !MOD_ENDUNG.test(entry.name)) continue;
|
||||||
filename,
|
const filename = aus ? entry.name.replace(/\.disabled$/i, '') : entry.name;
|
||||||
versionNumber: 'Importiert',
|
gefunden.push({
|
||||||
enabled: !aus,
|
projectId: null,
|
||||||
});
|
title: filename.replace(MOD_ENDUNG, ''),
|
||||||
}
|
filename,
|
||||||
gefunden.sort((a, b) => a.filename.localeCompare(b.filename));
|
versionNumber: 'Importiert',
|
||||||
return gefunden;
|
enabled: !aus,
|
||||||
}
|
});
|
||||||
|
}
|
||||||
function listMods(id, art = 'mod') {
|
gefunden.sort((a, b) => a.filename.localeCompare(b.filename));
|
||||||
const aufDerPlatte = new Map(scanModsDir(id, art).map((m) => [m.filename, m]));
|
return gefunden;
|
||||||
const indexed = readJson(modsIndexFile(id, art), []);
|
}
|
||||||
const merged = new Map();
|
|
||||||
|
function listMods(id, art = 'mod') {
|
||||||
for (const entry of indexed) {
|
const aufDerPlatte = new Map(scanModsDir(id, art).map((m) => [m.filename, m]));
|
||||||
if (!entry || !entry.filename) continue;
|
const indexed = readJson(modsIndexFile(id, art), []);
|
||||||
const platte = aufDerPlatte.get(entry.filename);
|
const merged = new Map();
|
||||||
merged.set(entry.filename, Object.assign({}, entry, {
|
|
||||||
enabled: platte ? platte.enabled : true,
|
for (const entry of indexed) {
|
||||||
fehlt: !platte, // steht im Verzeichnis, Datei ist aber weg
|
if (!entry || !entry.filename) continue;
|
||||||
}));
|
const platte = aufDerPlatte.get(entry.filename);
|
||||||
}
|
merged.set(entry.filename, Object.assign({}, entry, {
|
||||||
for (const [name, entry] of aufDerPlatte) {
|
enabled: platte ? platte.enabled : true,
|
||||||
if (!merged.has(name)) merged.set(name, Object.assign({ fehlt: false }, entry));
|
fehlt: !platte, // steht im Verzeichnis, Datei ist aber weg
|
||||||
}
|
}));
|
||||||
return [...merged.values()];
|
}
|
||||||
}
|
for (const [name, entry] of aufDerPlatte) {
|
||||||
|
if (!merged.has(name)) merged.set(name, Object.assign({ fehlt: false }, entry));
|
||||||
/*
|
}
|
||||||
* Schaltet eine Mod an oder aus, indem die Datei umbenannt wird.
|
return [...merged.values()];
|
||||||
* -> { ok, enabled } oder { ok: false, reason }
|
}
|
||||||
*/
|
|
||||||
function setModEnabled(id, filename, enabled, art = 'mod') {
|
/*
|
||||||
const dir = modsDir(id, art);
|
* Schaltet eine Mod an oder aus, indem die Datei umbenannt wird.
|
||||||
const anPfad = path.join(dir, filename);
|
* -> { ok, enabled } oder { ok: false, reason }
|
||||||
const ausPfad = anPfad + '.disabled';
|
*/
|
||||||
// Pfad darf den mods-Ordner nicht verlassen
|
function setModEnabled(id, filename, enabled, art = 'mod') {
|
||||||
if (path.dirname(anPfad) !== dir) return { ok: false, reason: 'bad-name' };
|
const dir = modsDir(id, art);
|
||||||
|
const anPfad = path.join(dir, filename);
|
||||||
const vonPfad = enabled ? ausPfad : anPfad;
|
const ausPfad = anPfad + '.disabled';
|
||||||
const nachPfad = enabled ? anPfad : ausPfad;
|
// Pfad darf den mods-Ordner nicht verlassen
|
||||||
|
if (path.dirname(anPfad) !== dir) return { ok: false, reason: 'bad-name' };
|
||||||
if (fs.existsSync(anPfad) && fs.existsSync(ausPfad)) {
|
|
||||||
// beide Fassungen da – nichts anfassen, sonst geht eine verloren
|
const vonPfad = enabled ? ausPfad : anPfad;
|
||||||
return { ok: false, reason: 'both-exist' };
|
const nachPfad = enabled ? anPfad : ausPfad;
|
||||||
}
|
|
||||||
if (!fs.existsSync(vonPfad)) {
|
if (fs.existsSync(anPfad) && fs.existsSync(ausPfad)) {
|
||||||
if (fs.existsSync(nachPfad)) return { ok: true, enabled, unveraendert: true };
|
// beide Fassungen da – nichts anfassen, sonst geht eine verloren
|
||||||
return { ok: false, reason: 'not-found' };
|
return { ok: false, reason: 'both-exist' };
|
||||||
}
|
}
|
||||||
try {
|
if (!fs.existsSync(vonPfad)) {
|
||||||
fs.renameSync(vonPfad, nachPfad);
|
if (fs.existsSync(nachPfad)) return { ok: true, enabled, unveraendert: true };
|
||||||
return { ok: true, enabled };
|
return { ok: false, reason: 'not-found' };
|
||||||
} catch (err) {
|
}
|
||||||
return { ok: false, reason: err.message };
|
try {
|
||||||
}
|
fs.renameSync(vonPfad, nachPfad);
|
||||||
}
|
return { ok: true, enabled };
|
||||||
|
} catch (err) {
|
||||||
function addMod(id, entry, art = 'mod') {
|
return { ok: false, reason: err.message };
|
||||||
const list = listMods(id, art).filter((m) => m.filename !== entry.filename);
|
}
|
||||||
list.push(entry);
|
}
|
||||||
writeJson(modsIndexFile(id, art), list);
|
|
||||||
return list;
|
function addMod(id, entry, art = 'mod') {
|
||||||
}
|
const list = listMods(id, art).filter((m) => m.filename !== entry.filename);
|
||||||
|
list.push(entry);
|
||||||
/*
|
writeJson(modsIndexFile(id, art), list);
|
||||||
* Findet installierte Mods, die den per "filename" identifizierten Mod als
|
return list;
|
||||||
* Pflicht-Abhängigkeit brauchen (requiredDeps wird beim Installieren über
|
}
|
||||||
* AeroMC gesetzt – von Hand hinzugefügte oder vor diesem Feature installierte
|
|
||||||
* Mods haben keine requiredDeps und werden daher nicht als Ziel erkannt,
|
/*
|
||||||
* können aber selbst weiterhin als Abhängiger auftauchen).
|
* Findet installierte Mods, die den per "filename" identifizierten Mod als
|
||||||
* -> [Titel, Titel, ...] der abhängigen Mods
|
* Pflicht-Abhängigkeit brauchen (requiredDeps wird beim Installieren über
|
||||||
*/
|
* AeroMC gesetzt – von Hand hinzugefügte oder vor diesem Feature installierte
|
||||||
function findDependents(id, filename, art = 'mod') {
|
* Mods haben keine requiredDeps und werden daher nicht als Ziel erkannt,
|
||||||
const list = listMods(id, art);
|
* können aber selbst weiterhin als Abhängiger auftauchen).
|
||||||
const target = list.find((m) => m.filename === filename);
|
* -> [Titel, Titel, ...] der abhängigen Mods
|
||||||
if (!target || !target.projectId) return [];
|
*/
|
||||||
return list
|
function findDependents(id, filename, art = 'mod') {
|
||||||
.filter((m) => m.filename !== filename
|
const list = listMods(id, art);
|
||||||
&& Array.isArray(m.requiredDeps) && m.requiredDeps.includes(target.projectId))
|
const target = list.find((m) => m.filename === filename);
|
||||||
.map((m) => m.title || m.filename);
|
if (!target || !target.projectId) return [];
|
||||||
}
|
return list
|
||||||
|
.filter((m) => m.filename !== filename
|
||||||
function removeMod(id, filename, art = 'mod') {
|
&& Array.isArray(m.requiredDeps) && m.requiredDeps.includes(target.projectId))
|
||||||
const list = listMods(id, art).filter((m) => m.filename !== filename);
|
.map((m) => m.title || m.filename);
|
||||||
writeJson(modsIndexFile(id, art), list);
|
}
|
||||||
// zugehörige Datei löschen – auch, wenn sie gerade abgeschaltet ist
|
|
||||||
const jar = path.join(modsDir(id, art), filename);
|
function removeMod(id, filename, art = 'mod') {
|
||||||
for (const p of [jar, jar + '.disabled']) {
|
const list = listMods(id, art).filter((m) => m.filename !== filename);
|
||||||
try { if (fs.existsSync(p)) fs.unlinkSync(p); } catch { /* egal */ }
|
writeJson(modsIndexFile(id, art), list);
|
||||||
}
|
// zugehörige Datei löschen – auch, wenn sie gerade abgeschaltet ist
|
||||||
return list;
|
const jar = path.join(modsDir(id, art), filename);
|
||||||
}
|
for (const p of [jar, jar + '.disabled']) {
|
||||||
|
try { if (fs.existsSync(p)) fs.unlinkSync(p); } catch { /* egal */ }
|
||||||
function defaultInstance(name) {
|
}
|
||||||
const now = new Date().toISOString();
|
return list;
|
||||||
return {
|
}
|
||||||
id: newId(),
|
|
||||||
name: name || 'Neue Instanz',
|
function defaultInstance(name) {
|
||||||
icon: 'grass', // Schlüssel für ein Emoji/Icon im Renderer
|
const now = new Date().toISOString();
|
||||||
customImage: null, // Dateiname eines eigenen Bildes im Instanzordner (überschreibt Emoji)
|
return {
|
||||||
group: '', // optionale Gruppe (MultiMC-artig)
|
id: newId(),
|
||||||
preLaunchCommand: '', // Befehl vor dem Start
|
name: name || 'Neue Instanz',
|
||||||
postExitCommand: '', // Befehl nach dem Beenden
|
icon: 'grass', // Schlüssel für ein Emoji/Icon im Renderer
|
||||||
created: now,
|
customImage: null, // Dateiname eines eigenen Bildes im Instanzordner (überschreibt Emoji)
|
||||||
lastPlayed: null,
|
group: '', // optionale Gruppe (MultiMC-artig)
|
||||||
totalPlaySeconds: 0,
|
preLaunchCommand: '', // Befehl vor dem Start
|
||||||
minecraft: {
|
postExitCommand: '', // Befehl nach dem Beenden
|
||||||
version: '', // z. B. "1.21.4" – wird in Phase 2 aus dem Mojang-Manifest befüllt
|
created: now,
|
||||||
loader: 'vanilla', // vanilla | fabric | forge | neoforge | quilt
|
lastPlayed: null,
|
||||||
loaderVersion: '',
|
totalPlaySeconds: 0,
|
||||||
},
|
launchCount: 0, // Anzahl Spielstarts (Spielzeit-Statistik)
|
||||||
java: {
|
minecraft: {
|
||||||
path: '', // leer => globale Einstellung
|
version: '', // z. B. "1.21.4" – wird in Phase 2 aus dem Mojang-Manifest befüllt
|
||||||
minMemMb: null, // null => globale Einstellung
|
loader: 'vanilla', // vanilla | fabric | forge | neoforge | quilt
|
||||||
maxMemMb: null,
|
loaderVersion: '',
|
||||||
extraArgs: '',
|
},
|
||||||
},
|
java: {
|
||||||
notes: '',
|
path: '', // leer => globale Einstellung
|
||||||
};
|
minMemMb: null, // null => globale Einstellung
|
||||||
}
|
maxMemMb: null,
|
||||||
|
extraArgs: '',
|
||||||
// liest ein eigenes Instanz-Bild als data:-URI (für die Anzeige im Renderer)
|
},
|
||||||
function iconDataUri(id) {
|
notes: '',
|
||||||
const meta = readJson(instanceFile(id), null);
|
};
|
||||||
if (!meta || !meta.customImage) return null;
|
}
|
||||||
try {
|
|
||||||
const buf = fs.readFileSync(path.join(instancePath(id), meta.customImage));
|
// liest ein eigenes Instanz-Bild als data:-URI (für die Anzeige im Renderer)
|
||||||
let ext = path.extname(meta.customImage).slice(1).toLowerCase();
|
function iconDataUri(id) {
|
||||||
if (ext === 'jpg') ext = 'jpeg';
|
const meta = readJson(instanceFile(id), null);
|
||||||
return `data:image/${ext};base64,` + buf.toString('base64');
|
if (!meta || !meta.customImage) return null;
|
||||||
} catch { return null; }
|
try {
|
||||||
}
|
const buf = fs.readFileSync(path.join(instancePath(id), meta.customImage));
|
||||||
|
let ext = path.extname(meta.customImage).slice(1).toLowerCase();
|
||||||
function setCustomImage(id, sourcePath) {
|
if (ext === 'jpg') ext = 'jpeg';
|
||||||
let ext = path.extname(sourcePath).toLowerCase();
|
return `data:image/${ext};base64,` + buf.toString('base64');
|
||||||
if (!ext) ext = '.png';
|
} catch { return null; }
|
||||||
const dest = 'icon' + ext;
|
}
|
||||||
fs.copyFileSync(sourcePath, path.join(instancePath(id), dest));
|
|
||||||
return updateInstance(id, { customImage: dest });
|
function setCustomImage(id, sourcePath) {
|
||||||
}
|
let ext = path.extname(sourcePath).toLowerCase();
|
||||||
|
if (!ext) ext = '.png';
|
||||||
function clearCustomImage(id) {
|
const dest = 'icon' + ext;
|
||||||
const meta = getInstance(id);
|
fs.copyFileSync(sourcePath, path.join(instancePath(id), dest));
|
||||||
if (meta && meta.customImage) {
|
return updateInstance(id, { customImage: dest });
|
||||||
try { fs.unlinkSync(path.join(instancePath(id), meta.customImage)); } catch { /* egal */ }
|
}
|
||||||
}
|
|
||||||
return updateInstance(id, { customImage: null });
|
function clearCustomImage(id) {
|
||||||
}
|
const meta = getInstance(id);
|
||||||
|
if (meta && meta.customImage) {
|
||||||
function listInstances() {
|
try { fs.unlinkSync(path.join(instancePath(id), meta.customImage)); } catch { /* egal */ }
|
||||||
const dir = getInstancesDir();
|
}
|
||||||
ensureDir(dir);
|
return updateInstance(id, { customImage: null });
|
||||||
const result = [];
|
}
|
||||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
||||||
if (!entry.isDirectory()) continue;
|
function listInstances() {
|
||||||
const meta = readJson(path.join(dir, entry.name, 'instance.json'), null);
|
const dir = getInstancesDir();
|
||||||
if (meta && meta.id) {
|
ensureDir(dir);
|
||||||
// eigenes Bild direkt für die Anzeige mitliefern (nicht persistiert)
|
const result = [];
|
||||||
if (meta.customImage) meta.iconData = iconDataUri(meta.id);
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||||
result.push(meta);
|
if (!entry.isDirectory()) continue;
|
||||||
}
|
const meta = readJson(path.join(dir, entry.name, 'instance.json'), null);
|
||||||
}
|
if (meta && meta.id) {
|
||||||
// Sortierung laut Einstellung: zuletzt gespielt oder nach Name
|
// eigenes Bild direkt für die Anzeige mitliefern (nicht persistiert)
|
||||||
const byName = getSettings().sortBy === 'name';
|
if (meta.customImage) meta.iconData = iconDataUri(meta.id);
|
||||||
result.sort((a, b) => {
|
result.push(meta);
|
||||||
if (byName) return a.name.localeCompare(b.name);
|
}
|
||||||
if (a.lastPlayed && b.lastPlayed) return b.lastPlayed.localeCompare(a.lastPlayed);
|
}
|
||||||
if (a.lastPlayed) return -1;
|
// Sortierung laut Einstellung: zuletzt gespielt oder nach Name
|
||||||
if (b.lastPlayed) return 1;
|
const byName = getSettings().sortBy === 'name';
|
||||||
return a.name.localeCompare(b.name);
|
result.sort((a, b) => {
|
||||||
});
|
if (byName) return a.name.localeCompare(b.name);
|
||||||
return result;
|
if (a.lastPlayed && b.lastPlayed) return b.lastPlayed.localeCompare(a.lastPlayed);
|
||||||
}
|
if (a.lastPlayed) return -1;
|
||||||
|
if (b.lastPlayed) return 1;
|
||||||
function getInstance(id) {
|
return a.name.localeCompare(b.name);
|
||||||
return readJson(instanceFile(id), null);
|
});
|
||||||
}
|
return result;
|
||||||
|
}
|
||||||
function normalizeInstanceName(name) {
|
|
||||||
return String(name || '').trim().toLowerCase();
|
function getInstance(id) {
|
||||||
}
|
return readJson(instanceFile(id), null);
|
||||||
|
}
|
||||||
function findInstanceByName(name) {
|
|
||||||
const needle = normalizeInstanceName(name);
|
function normalizeInstanceName(name) {
|
||||||
if (!needle) return null;
|
return String(name || '').trim().toLowerCase();
|
||||||
return listInstances().find((inst) => normalizeInstanceName(inst.name) === needle) || null;
|
}
|
||||||
}
|
|
||||||
|
function findInstanceByName(name) {
|
||||||
// Ordnername aus dem Instanznamen ableiten (wie MultiMC), ungültige Zeichen raus
|
const needle = normalizeInstanceName(name);
|
||||||
function slugForName(name) {
|
if (!needle) return null;
|
||||||
const base = String(name || 'Instanz').trim()
|
return listInstances().find((inst) => normalizeInstanceName(inst.name) === needle) || null;
|
||||||
.replace(/[<>:"/\\|?*\u0000-\u001f]/g, '')
|
}
|
||||||
.replace(/[.\s]+$/, '')
|
|
||||||
.trim();
|
// Ordnername aus dem Instanznamen ableiten (wie MultiMC), ungültige Zeichen raus
|
||||||
return (base || 'Instanz').slice(0, 60);
|
function slugForName(name) {
|
||||||
}
|
const base = String(name || 'Instanz').trim()
|
||||||
// eindeutiger Ordnername (bei Kollision "Name 2", "Name 3", …)
|
.replace(/[<>:"/\\|?*\u0000-\u001f]/g, '')
|
||||||
function uniqueFolderId(name) {
|
.replace(/[.\s]+$/, '')
|
||||||
const dir = getInstancesDir();
|
.trim();
|
||||||
const base = slugForName(name);
|
return (base || 'Instanz').slice(0, 60);
|
||||||
let id = base;
|
}
|
||||||
let n = 2;
|
// eindeutiger Ordnername (bei Kollision "Name 2", "Name 3", …)
|
||||||
while (fs.existsSync(path.join(dir, id))) { id = base + ' ' + n; n += 1; }
|
function uniqueFolderId(name) {
|
||||||
return id;
|
const dir = getInstancesDir();
|
||||||
}
|
const base = slugForName(name);
|
||||||
|
let id = base;
|
||||||
function applyInstanceData(inst, data) {
|
let n = 2;
|
||||||
if (!data) return inst;
|
while (fs.existsSync(path.join(dir, id))) { id = base + ' ' + n; n += 1; }
|
||||||
if (data.name) inst.name = data.name;
|
return id;
|
||||||
if (data.icon) inst.icon = data.icon;
|
}
|
||||||
if (data.minecraft) Object.assign(inst.minecraft, data.minecraft);
|
|
||||||
if (data.notes !== undefined) inst.notes = data.notes;
|
function applyInstanceData(inst, data) {
|
||||||
if (data.group !== undefined) inst.group = data.group;
|
if (!data) return inst;
|
||||||
return inst;
|
if (data.name) inst.name = data.name;
|
||||||
}
|
if (data.icon) inst.icon = data.icon;
|
||||||
|
if (data.minecraft) Object.assign(inst.minecraft, data.minecraft);
|
||||||
function writeInstance(inst) {
|
if (data.notes !== undefined) inst.notes = data.notes;
|
||||||
ensureDir(gameDir(inst.id));
|
if (data.group !== undefined) inst.group = data.group;
|
||||||
writeJson(instanceFile(inst.id), inst);
|
return inst;
|
||||||
return inst;
|
}
|
||||||
}
|
|
||||||
|
function writeInstance(inst) {
|
||||||
function createInstance(data) {
|
ensureDir(gameDir(inst.id));
|
||||||
const inst = applyInstanceData(defaultInstance(data && data.name), data);
|
writeJson(instanceFile(inst.id), inst);
|
||||||
inst.id = uniqueFolderId(inst.name); // Ordner = Instanzname statt Zufalls-ID
|
return inst;
|
||||||
return writeInstance(inst);
|
}
|
||||||
}
|
|
||||||
|
function createInstance(data) {
|
||||||
function createOrReplaceImportedInstance(data) {
|
const inst = applyInstanceData(defaultInstance(data && data.name), data);
|
||||||
const existing = findInstanceByName(data && data.name);
|
inst.id = uniqueFolderId(inst.name); // Ordner = Instanzname statt Zufalls-ID
|
||||||
if (!existing) return createInstance(data);
|
return writeInstance(inst);
|
||||||
|
}
|
||||||
const inst = applyInstanceData(defaultInstance(data && data.name), data);
|
|
||||||
inst.id = existing.id;
|
function createOrReplaceImportedInstance(data) {
|
||||||
inst.created = existing.created || inst.created;
|
const existing = findInstanceByName(data && data.name);
|
||||||
inst.lastPlayed = existing.lastPlayed || null;
|
if (!existing) return createInstance(data);
|
||||||
inst.totalPlaySeconds = existing.totalPlaySeconds || 0;
|
|
||||||
|
const inst = applyInstanceData(defaultInstance(data && data.name), data);
|
||||||
const p = instancePath(existing.id);
|
inst.id = existing.id;
|
||||||
if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true });
|
inst.created = existing.created || inst.created;
|
||||||
return writeInstance(inst);
|
inst.lastPlayed = existing.lastPlayed || null;
|
||||||
}
|
inst.totalPlaySeconds = existing.totalPlaySeconds || 0;
|
||||||
|
inst.launchCount = existing.launchCount || 0;
|
||||||
function updateInstance(id, patch) {
|
|
||||||
const inst = getInstance(id);
|
const p = instancePath(existing.id);
|
||||||
if (!inst) throw new Error('Instanz nicht gefunden: ' + id);
|
if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true });
|
||||||
// flaches Merge mit Sonderbehandlung der verschachtelten Objekte
|
return writeInstance(inst);
|
||||||
const next = Object.assign({}, inst, patch);
|
}
|
||||||
if (patch.minecraft) next.minecraft = Object.assign({}, inst.minecraft, patch.minecraft);
|
|
||||||
if (patch.java) next.java = Object.assign({}, inst.java, patch.java);
|
function updateInstance(id, patch) {
|
||||||
writeJson(instanceFile(id), next);
|
const inst = getInstance(id);
|
||||||
return next;
|
if (!inst) throw new Error('Instanz nicht gefunden: ' + id);
|
||||||
}
|
// flaches Merge mit Sonderbehandlung der verschachtelten Objekte
|
||||||
|
const next = Object.assign({}, inst, patch);
|
||||||
function deleteInstance(id) {
|
if (patch.minecraft) next.minecraft = Object.assign({}, inst.minecraft, patch.minecraft);
|
||||||
const p = instancePath(id);
|
if (patch.java) next.java = Object.assign({}, inst.java, patch.java);
|
||||||
if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true });
|
writeJson(instanceFile(id), next);
|
||||||
return true;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
function duplicateInstance(id) {
|
function deleteInstance(id) {
|
||||||
const src = getInstance(id);
|
const p = instancePath(id);
|
||||||
if (!src) throw new Error('Instanz nicht gefunden: ' + id);
|
if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true });
|
||||||
const copy = JSON.parse(JSON.stringify(src));
|
return true;
|
||||||
copy.name = src.name + ' (Kopie)';
|
}
|
||||||
copy.id = uniqueFolderId(copy.name);
|
|
||||||
copy.created = new Date().toISOString();
|
function duplicateInstance(id) {
|
||||||
copy.lastPlayed = null;
|
const src = getInstance(id);
|
||||||
copy.totalPlaySeconds = 0;
|
if (!src) throw new Error('Instanz nicht gefunden: ' + id);
|
||||||
ensureDir(gameDir(copy.id));
|
const copy = JSON.parse(JSON.stringify(src));
|
||||||
// Spielinhalte mitkopieren (Mods/Configs) – ohne Fehler wenn leer
|
copy.name = src.name + ' (Kopie)';
|
||||||
try {
|
copy.id = uniqueFolderId(copy.name);
|
||||||
fs.cpSync(gameDir(id), gameDir(copy.id), { recursive: true });
|
copy.created = new Date().toISOString();
|
||||||
} catch { /* Quelle evtl. leer */ }
|
copy.lastPlayed = null;
|
||||||
writeJson(instanceFile(copy.id), copy);
|
copy.totalPlaySeconds = 0;
|
||||||
return copy;
|
copy.launchCount = 0;
|
||||||
}
|
ensureDir(gameDir(copy.id));
|
||||||
|
// Spielinhalte mitkopieren (Mods/Configs) – ohne Fehler wenn leer
|
||||||
module.exports = {
|
try {
|
||||||
init,
|
fs.cpSync(gameDir(id), gameDir(copy.id), { recursive: true });
|
||||||
getSettings,
|
} catch { /* Quelle evtl. leer */ }
|
||||||
saveSettings,
|
writeJson(instanceFile(copy.id), copy);
|
||||||
getInstancesDir,
|
return copy;
|
||||||
instancePath,
|
}
|
||||||
gameDir,
|
|
||||||
modsDir,
|
module.exports = {
|
||||||
inhaltsDir,
|
init,
|
||||||
iconDataUri,
|
getSettings,
|
||||||
setCustomImage,
|
saveSettings,
|
||||||
clearCustomImage,
|
getInstancesDir,
|
||||||
listMods,
|
instancePath,
|
||||||
addMod,
|
gameDir,
|
||||||
removeMod,
|
modsDir,
|
||||||
findDependents,
|
inhaltsDir,
|
||||||
setModEnabled,
|
iconDataUri,
|
||||||
listInstances,
|
setCustomImage,
|
||||||
getInstance,
|
clearCustomImage,
|
||||||
findInstanceByName,
|
listMods,
|
||||||
createInstance,
|
addMod,
|
||||||
createOrReplaceImportedInstance,
|
removeMod,
|
||||||
updateInstance,
|
findDependents,
|
||||||
deleteInstance,
|
setModEnabled,
|
||||||
duplicateInstance,
|
listInstances,
|
||||||
};
|
getInstance,
|
||||||
|
findInstanceByName,
|
||||||
|
createInstance,
|
||||||
|
createOrReplaceImportedInstance,
|
||||||
|
updateInstance,
|
||||||
|
deleteInstance,
|
||||||
|
duplicateInstance,
|
||||||
|
};
|
||||||
|
|||||||
@@ -391,6 +391,97 @@ img.account-av{object-fit:cover;image-rendering:auto}
|
|||||||
.tb-badge{display:inline-block;min-width:16px;height:16px;padding:0 4px;margin-left:6px;border-radius:8px;background:#d9453c;color:#fff;font-size:11px;font-weight:600;line-height:16px;text-align:center;box-sizing:border-box}
|
.tb-badge{display:inline-block;min-width:16px;height:16px;padding:0 4px;margin-left:6px;border-radius:8px;background:#d9453c;color:#fff;font-size:11px;font-weight:600;line-height:16px;text-align:center;box-sizing:border-box}
|
||||||
#modal-friends .field-row{margin-bottom:14px}
|
#modal-friends .field-row{margin-bottom:14px}
|
||||||
#modal-friends .field-row input{flex:1}
|
#modal-friends .field-row input{flex:1}
|
||||||
|
/* ---- Spielzeit-Statistik: Kennzahlen + Balken pro Instanz ---- */
|
||||||
|
:root{--stat-bar:#3a6ea5}
|
||||||
|
:root[data-theme="dark"]{--stat-bar:#3d8fe0}
|
||||||
|
.stats-tiles{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:6px}
|
||||||
|
.stat-tile{border:1px solid var(--border);border-radius:4px;padding:8px 10px;min-width:0}
|
||||||
|
.stat-tile .stat-label{font-size:11px;color:var(--dim)}
|
||||||
|
.stat-tile .stat-value{font-size:18px;font-weight:600;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
.stat-tile .stat-sub{font-size:11px;color:var(--dim);line-height:1.35}
|
||||||
|
.stats-list{display:flex;flex-direction:column;gap:2px}
|
||||||
|
.stat-row{display:grid;grid-template-columns:minmax(0,38%) 1fr auto;align-items:center;gap:10px;padding:5px 4px;border-radius:3px}
|
||||||
|
.stat-row:hover{background:var(--sel)}
|
||||||
|
.stat-name{min-width:0}
|
||||||
|
.stat-name b{display:block;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
.stat-name span{font-size:11px;color:var(--dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block}
|
||||||
|
.stat-track{height:8px}
|
||||||
|
.stat-bar{height:8px;background:var(--stat-bar);border-radius:0 4px 4px 0;min-width:2px}
|
||||||
|
.stat-value-cell{font-variant-numeric:tabular-nums;white-space:nowrap;text-align:right;min-width:92px}
|
||||||
|
.popout-host .stats-list{max-height:none}
|
||||||
|
|
||||||
|
/* Clip-Modus-Leiste im Aufzeichnungsfenster */
|
||||||
|
.clip-bar{display:flex;align-items:center;gap:14px;flex-wrap:wrap;padding:8px 12px;border:1px solid var(--border);border-radius:4px;background:var(--area)}
|
||||||
|
.clip-bar.aktiv{border-color:var(--accent);box-shadow:inset 3px 0 0 var(--accent)}
|
||||||
|
.clip-switch{display:inline-flex;align-items:center;gap:8px;cursor:pointer;font-size:13px;font-weight:600;user-select:none}
|
||||||
|
.clip-switch input{position:absolute;opacity:0;width:1px;height:1px;pointer-events:none}
|
||||||
|
.clip-switch-track{position:relative;width:32px;height:18px;border-radius:9px;background:var(--border);transition:background .15s}
|
||||||
|
.clip-switch-knob{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.35);transition:left .15s}
|
||||||
|
.clip-switch input:checked + .clip-switch-track{background:var(--accent)}
|
||||||
|
.clip-switch input:checked + .clip-switch-track .clip-switch-knob{left:16px}
|
||||||
|
.clip-switch input:focus-visible + .clip-switch-track{outline:2px solid var(--accent);outline-offset:2px}
|
||||||
|
.clip-length{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--dim)}
|
||||||
|
.clip-length select{padding:3px 6px;border:1px solid var(--border);border-radius:3px;background:var(--input,#fff);color:var(--text);font-family:inherit;font-size:12px}
|
||||||
|
.clip-length select:disabled{opacity:.6}
|
||||||
|
.clip-status{flex:1;min-width:200px;font-size:12px;color:var(--dim)}
|
||||||
|
.clip-bar.aktiv .clip-status{color:var(--text)}
|
||||||
|
.clip-bar.aktiv .clip-status::before{content:"";display:inline-block;width:8px;height:8px;margin-right:7px;border-radius:50%;background:var(--danger);vertical-align:1px;animation:rec-pulse 1.6s infinite}
|
||||||
|
|
||||||
|
/* ---- Absturz-Assistent ---- */
|
||||||
|
.crash-hilfe{display:flex;flex-direction:column;gap:8px;margin-bottom:12px}
|
||||||
|
.crash-hilfe-titel{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.03em}
|
||||||
|
.crash-fund{display:flex;gap:12px;align-items:flex-start;padding:10px 12px;border:1px solid var(--border);border-left:3px solid var(--accent);border-radius:4px;background:var(--area)}
|
||||||
|
.crash-fund-text{flex:1;min-width:0}
|
||||||
|
.crash-fund-text b{display:block;font-size:13px;margin-bottom:3px}
|
||||||
|
.crash-fund-text span{font-size:12px;color:var(--dim);line-height:1.45}
|
||||||
|
.crash-fund .btn{flex:0 0 auto;align-self:center}
|
||||||
|
.crash-fund.erledigt{border-left-color:var(--play)}
|
||||||
|
.crash-unklar{padding:10px 12px;border:1px dashed var(--border);border-radius:4px;font-size:12px;color:var(--dim)}
|
||||||
|
|
||||||
|
/* ---- Instanz teilen ---- */
|
||||||
|
.share-code-box{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 16px;border:1px solid var(--accent);border-radius:4px;background:var(--area);margin:10px 0}
|
||||||
|
.share-code{font-family:var(--mono);font-size:24px;font-weight:600;letter-spacing:.12em;user-select:all}
|
||||||
|
.share-warn{color:var(--danger)}
|
||||||
|
.share-code-input{text-transform:uppercase;letter-spacing:.1em;font-family:var(--mono)}
|
||||||
|
.share-preview{margin-top:12px;padding:12px 14px;border:1px solid var(--border);border-radius:4px;background:var(--area)}
|
||||||
|
.share-preview b{font-size:14px}
|
||||||
|
.share-preview .hint{margin:4px 0 0}
|
||||||
|
|
||||||
|
/* ---- Kurznachrichten ---- */
|
||||||
|
.friends-chat{display:flex;flex-direction:column;gap:8px;height:100%;min-height:340px}
|
||||||
|
.chat-head{display:flex;align-items:center;gap:10px}
|
||||||
|
.chat-head b{font-size:13px}
|
||||||
|
.chat-verlauf{flex:1;min-height:200px;max-height:300px;overflow-y:auto;display:flex;flex-direction:column;gap:6px;padding:8px;border:1px solid var(--border);border-radius:4px;background:var(--area)}
|
||||||
|
.popout-host .chat-verlauf{max-height:none}
|
||||||
|
.chat-leer{margin:auto;font-size:12px;color:var(--dim);text-align:center}
|
||||||
|
.chat-blase{max-width:78%;padding:6px 10px;border-radius:10px;font-size:12.5px;line-height:1.4;word-break:break-word;background:var(--win);border:1px solid var(--border);align-self:flex-start}
|
||||||
|
.chat-blase.ich{align-self:flex-end;background:var(--accent);border-color:var(--accent);color:#fff}
|
||||||
|
.chat-zeit{display:block;font-size:10px;opacity:.65;margin-top:2px}
|
||||||
|
.chat-schnell{display:flex;flex-wrap:wrap;gap:6px}
|
||||||
|
.chat-btn{position:relative}
|
||||||
|
.chat-badge{position:absolute;top:-6px;right:-6px;min-width:16px;height:16px;padding:0 4px;border-radius:8px;background:#e5484d;color:#fff;font-size:10px;font-weight:600;line-height:16px;text-align:center}
|
||||||
|
.report-liste{margin:6px 0 10px;padding-left:20px;font-size:12px;line-height:1.6}
|
||||||
|
|
||||||
|
/* ---- Auswahl-Dialog ---- */
|
||||||
|
.wahl-liste{display:flex;flex-direction:column;gap:4px;max-height:320px;overflow-y:auto}
|
||||||
|
.popout-host .wahl-liste{max-height:none}
|
||||||
|
.wahl-option{display:flex;align-items:center;gap:10px;width:100%;padding:7px 10px;border:1px solid var(--border);border-radius:4px;background:var(--area);color:var(--text);font-family:inherit;text-align:left;cursor:pointer}
|
||||||
|
.wahl-option:hover{border-color:var(--accent)}
|
||||||
|
.wahl-option.an{border-color:var(--accent);background:var(--sel)}
|
||||||
|
.wahl-option:disabled{opacity:.5;cursor:default}
|
||||||
|
.wahl-bild{width:26px;height:26px;flex:0 0 auto}
|
||||||
|
.wahl-label{display:flex;flex-direction:column;min-width:0}
|
||||||
|
.wahl-label b{font-size:13px;font-weight:600}
|
||||||
|
.wahl-label small{font-size:11px;color:var(--dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
|
||||||
|
/* ---- Schnellstart-Taste auf der Kachel ---- */
|
||||||
|
.card{position:relative}
|
||||||
|
.card-taste{position:absolute;top:4px;right:4px;padding:1px 5px;border-radius:3px;background:var(--accent);color:#fff;font-size:10px;font-weight:600;line-height:14px}
|
||||||
|
|
||||||
|
/* ---- Bilder im Chat ---- */
|
||||||
|
.chat-bild{display:block;max-width:100%;max-height:180px;border-radius:6px;margin-bottom:3px;cursor:zoom-in}
|
||||||
|
.screen-thumb .screen-teilen{right:32px;background:var(--accent)}
|
||||||
|
|
||||||
.friends-section-title{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.03em;margin:10px 0 6px}
|
.friends-section-title{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.03em;margin:10px 0 6px}
|
||||||
.friend-row{display:flex;align-items:center;gap:10px;padding:6px 4px;border-radius:3px}
|
.friend-row{display:flex;align-items:center;gap:10px;padding:6px 4px;border-radius:3px}
|
||||||
.friend-row:hover{background:var(--sel,rgba(0,0,0,.04))}
|
.friend-row:hover{background:var(--sel,rgba(0,0,0,.04))}
|
||||||
@@ -495,6 +586,7 @@ img.account-av{object-fit:cover;image-rendering:auto}
|
|||||||
und soll hier nicht greifen (sonst z. B. Version/Icon im Instanz-Fenster
|
und soll hier nicht greifen (sonst z. B. Version/Icon im Instanz-Fenster
|
||||||
untereinander statt nebeneinander) */
|
untereinander statt nebeneinander) */
|
||||||
.is-popout .field-row{flex-direction:row}
|
.is-popout .field-row{flex-direction:row}
|
||||||
|
.is-popout .path-row{flex-direction:row}
|
||||||
|
|
||||||
/* Konto-Button mit 3D-Skin-Kopf */
|
/* Konto-Button mit 3D-Skin-Kopf */
|
||||||
.acct-btn{gap:8px}
|
.acct-btn{gap:8px}
|
||||||
|
|||||||
+4
-3
@@ -42,7 +42,7 @@ function computeInstallKind() {
|
|||||||
// 2. Notnagel über den Dateinamen, falls die Variablen einmal fehlen
|
// 2. Notnagel über den Dateinamen, falls die Variablen einmal fehlen
|
||||||
if (/portable/i.test(path.basename(exe))) return 'portable';
|
if (/portable/i.test(path.basename(exe))) return 'portable';
|
||||||
|
|
||||||
// 3. Setup: NSIS legt neben der EXE immer einen Uninstaller ab. Nur wenn der
|
// 3. Setup: das Setup legt neben der EXE immer "Uninstall AeroMC.exe" ab. Nur wenn der
|
||||||
// da ist, ist es wirklich eine Installation.
|
// da ist, ist es wirklich eine Installation.
|
||||||
if (hasUninstaller(path.dirname(exe))) return 'setup';
|
if (hasUninstaller(path.dirname(exe))) return 'setup';
|
||||||
|
|
||||||
@@ -319,7 +319,7 @@ exit /b 1
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* Wendet das geladene Update an.
|
* Wendet das geladene Update an.
|
||||||
* setup -> NSIS-Installer starten, er überschreibt die Installation selbst
|
* setup -> AeroMC-Setup mit --update starten, es überschreibt die Installation selbst
|
||||||
* portable -> Skript starten, das die laufende EXE nach dem Beenden ersetzt
|
* portable -> Skript starten, das die laufende EXE nach dem Beenden ersetzt
|
||||||
* Der Launcher muss sich danach beenden.
|
* Der Launcher muss sich danach beenden.
|
||||||
*/
|
*/
|
||||||
@@ -331,7 +331,8 @@ function applyUpdate(file) {
|
|||||||
const kind = installKind;
|
const kind = installKind;
|
||||||
|
|
||||||
if (kind !== 'portable') {
|
if (kind !== 'portable') {
|
||||||
const child = spawn(file.path, [], { detached: true, stdio: 'ignore' });
|
// --update: das AeroMC-Setup aktualisiert ohne Rückfragen und startet AeroMC danach neu
|
||||||
|
const child = spawn(file.path, ['--update'], { detached: true, stdio: 'ignore' });
|
||||||
child.unref();
|
child.unref();
|
||||||
return { started: 'installer' };
|
return { started: 'installer' };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Plugin Name: AeroMC Freunde
|
* Plugin Name: AeroMC Freunde
|
||||||
* Description: Backend für das Freundesystem des AeroMC Launchers - meldet, wer gerade online/im Spiel ist, und verwaltet Freundschaftsanfragen. Keine eigene Anmeldung: der Launcher meldet sich direkt mit der Minecraft-UUID des jeweiligen Kontos.
|
* Description: Backend für das Freundesystem des AeroMC Launchers - meldet, wer gerade online/im Spiel ist, verwaltet Freundschaftsanfragen, Einladungen, Kurznachrichten, per Code geteilte Instanzen und Fehlerberichte aus dem Absturz-Assistenten. Anmeldung über Mojangs Sitzungsprüfung (wie bei Minecraft-Servern) - ohne eigenes Passwort.
|
||||||
* Version: 1.0.0
|
* Version: 1.4.0
|
||||||
* Author: M_Viper
|
* Author: M_Viper
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -10,9 +10,23 @@ if (!defined('ABSPATH')) {
|
|||||||
exit; // kein direkter Aufruf der Datei
|
exit; // kein direkter Aufruf der Datei
|
||||||
}
|
}
|
||||||
|
|
||||||
define('AEROMC_FRIENDS_VERSION', '1.0.0');
|
define('AEROMC_FRIENDS_VERSION', '1.4.0');
|
||||||
define('AEROMC_FRIENDS_PRESENCE_TABLE', 'aeromc_presence');
|
define('AEROMC_FRIENDS_PRESENCE_TABLE', 'aeromc_presence');
|
||||||
define('AEROMC_FRIENDS_FRIENDS_TABLE', 'aeromc_friends');
|
define('AEROMC_FRIENDS_FRIENDS_TABLE', 'aeromc_friends');
|
||||||
|
define('AEROMC_FRIENDS_INVITES_TABLE', 'aeromc_invites');
|
||||||
|
define('AEROMC_FRIENDS_SESSIONS_TABLE', 'aeromc_sessions');
|
||||||
|
define('AEROMC_FRIENDS_SHARES_TABLE', 'aeromc_shares');
|
||||||
|
define('AEROMC_FRIENDS_MESSAGES_TABLE', 'aeromc_messages');
|
||||||
|
define('AEROMC_FRIENDS_REPORTS_TABLE', 'aeromc_reports');
|
||||||
|
|
||||||
|
// Übergangsmodus: false = Anfragen ohne Anmeldung werden noch angenommen, aber
|
||||||
|
// nur für Spieler, die sich noch NIE über die neue Anmeldung gemeldet haben
|
||||||
|
// (ältere Launcher-Versionen funktionieren weiter). Sobald alle Nutzer
|
||||||
|
// aktualisiert haben, in der wp-config.php auf true setzen:
|
||||||
|
// define('AEROMC_FRIENDS_REQUIRE_AUTH', true);
|
||||||
|
if (!defined('AEROMC_FRIENDS_REQUIRE_AUTH')) {
|
||||||
|
define('AEROMC_FRIENDS_REQUIRE_AUTH', false);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Legt die beiden Tabellen an (bzw. bringt sie bei einem Update auf den
|
* Legt die beiden Tabellen an (bzw. bringt sie bei einem Update auf den
|
||||||
@@ -23,6 +37,9 @@ function aeromc_friends_install() {
|
|||||||
$charset_collate = $wpdb->get_charset_collate();
|
$charset_collate = $wpdb->get_charset_collate();
|
||||||
$presence_table = $wpdb->prefix . AEROMC_FRIENDS_PRESENCE_TABLE;
|
$presence_table = $wpdb->prefix . AEROMC_FRIENDS_PRESENCE_TABLE;
|
||||||
$friends_table = $wpdb->prefix . AEROMC_FRIENDS_FRIENDS_TABLE;
|
$friends_table = $wpdb->prefix . AEROMC_FRIENDS_FRIENDS_TABLE;
|
||||||
|
$invites_table = $wpdb->prefix . AEROMC_FRIENDS_INVITES_TABLE;
|
||||||
|
$sessions_table = $wpdb->prefix . AEROMC_FRIENDS_SESSIONS_TABLE;
|
||||||
|
$shares_table = $wpdb->prefix . AEROMC_FRIENDS_SHARES_TABLE;
|
||||||
|
|
||||||
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||||
|
|
||||||
@@ -34,6 +51,7 @@ function aeromc_friends_install() {
|
|||||||
username varchar(32) NOT NULL,
|
username varchar(32) NOT NULL,
|
||||||
instance_name varchar(128) DEFAULT NULL,
|
instance_name varchar(128) DEFAULT NULL,
|
||||||
server_label varchar(128) DEFAULT NULL,
|
server_label varchar(128) DEFAULT NULL,
|
||||||
|
mc_version varchar(32) DEFAULT NULL,
|
||||||
last_seen datetime NOT NULL,
|
last_seen datetime NOT NULL,
|
||||||
PRIMARY KEY (uuid)
|
PRIMARY KEY (uuid)
|
||||||
) $charset_collate;";
|
) $charset_collate;";
|
||||||
@@ -50,7 +68,95 @@ function aeromc_friends_install() {
|
|||||||
KEY uuid_b (uuid_b)
|
KEY uuid_b (uuid_b)
|
||||||
) $charset_collate;";
|
) $charset_collate;";
|
||||||
dbDelta($sql_friends);
|
dbDelta($sql_friends);
|
||||||
|
|
||||||
|
// Einladungen "Komm auf meinen Server" - gelten nur ein paar Minuten
|
||||||
|
$sql_invites = "CREATE TABLE $invites_table (
|
||||||
|
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
from_uuid varchar(32) NOT NULL,
|
||||||
|
to_uuid varchar(32) NOT NULL,
|
||||||
|
server varchar(128) NOT NULL,
|
||||||
|
mc_version varchar(32) DEFAULT NULL,
|
||||||
|
created_at datetime NOT NULL,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY to_uuid (to_uuid)
|
||||||
|
) $charset_collate;";
|
||||||
|
dbDelta($sql_invites);
|
||||||
|
|
||||||
|
// Anmelde-Sitzungen: nur der SHA-256 des Tokens wird gespeichert
|
||||||
|
$sql_sessions = "CREATE TABLE $sessions_table (
|
||||||
|
token_hash char(64) NOT NULL,
|
||||||
|
uuid varchar(32) NOT NULL,
|
||||||
|
username varchar(32) NOT NULL,
|
||||||
|
created_at datetime NOT NULL,
|
||||||
|
last_used datetime NOT NULL,
|
||||||
|
PRIMARY KEY (token_hash),
|
||||||
|
KEY uuid (uuid)
|
||||||
|
) $charset_collate;";
|
||||||
|
dbDelta($sql_sessions);
|
||||||
|
|
||||||
|
// per Code geteilte Instanzen (Mod-Liste, keine Dateien)
|
||||||
|
$sql_shares = "CREATE TABLE $shares_table (
|
||||||
|
code varchar(9) NOT NULL,
|
||||||
|
uuid varchar(32) NOT NULL,
|
||||||
|
manifest longtext NOT NULL,
|
||||||
|
created_at datetime NOT NULL,
|
||||||
|
PRIMARY KEY (code),
|
||||||
|
KEY uuid (uuid)
|
||||||
|
) $charset_collate;";
|
||||||
|
dbDelta($sql_shares);
|
||||||
|
|
||||||
|
// Kurznachrichten zwischen Freunden (werden nach einigen Tagen gelöscht)
|
||||||
|
$messages_table = $wpdb->prefix . AEROMC_FRIENDS_MESSAGES_TABLE;
|
||||||
|
$sql_messages = "CREATE TABLE $messages_table (
|
||||||
|
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
from_uuid varchar(32) NOT NULL,
|
||||||
|
to_uuid varchar(32) NOT NULL,
|
||||||
|
text varchar(400) NOT NULL,
|
||||||
|
image_url varchar(255) DEFAULT NULL,
|
||||||
|
created_at datetime NOT NULL,
|
||||||
|
read_at datetime DEFAULT NULL,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY to_uuid (to_uuid),
|
||||||
|
KEY from_uuid (from_uuid)
|
||||||
|
) $charset_collate;";
|
||||||
|
dbDelta($sql_messages);
|
||||||
|
|
||||||
|
// Fehlerberichte aus dem Absturz-Assistenten (Ansicht: Werkzeuge → AeroMC Fehlerberichte)
|
||||||
|
$reports_table = $wpdb->prefix . AEROMC_FRIENDS_REPORTS_TABLE;
|
||||||
|
$sql_reports = "CREATE TABLE $reports_table (
|
||||||
|
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
created_at datetime NOT NULL,
|
||||||
|
uuid varchar(32) DEFAULT NULL,
|
||||||
|
username varchar(32) DEFAULT NULL,
|
||||||
|
launcher_version varchar(32) DEFAULT NULL,
|
||||||
|
mc_version varchar(32) DEFAULT NULL,
|
||||||
|
loader varchar(32) DEFAULT NULL,
|
||||||
|
loader_version varchar(64) DEFAULT NULL,
|
||||||
|
system varchar(128) DEFAULT NULL,
|
||||||
|
funde text,
|
||||||
|
mods longtext,
|
||||||
|
bericht longtext,
|
||||||
|
kommentar text,
|
||||||
|
status varchar(20) NOT NULL DEFAULT 'neu',
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY status (status)
|
||||||
|
) $charset_collate;";
|
||||||
|
dbDelta($sql_reports);
|
||||||
|
|
||||||
|
update_option('aeromc_friends_db_version', AEROMC_FRIENDS_VERSION);
|
||||||
}
|
}
|
||||||
register_activation_hook(__FILE__, 'aeromc_friends_install');
|
register_activation_hook(__FILE__, 'aeromc_friends_install');
|
||||||
|
|
||||||
|
// Beim Ersetzen der Plugin-Dateien (Update per Upload) läuft der
|
||||||
|
// Aktivierungs-Hook NICHT erneut - neue Spalten/Tabellen deshalb anlegen,
|
||||||
|
// sobald die gespeicherte DB-Version nicht mehr zur Plugin-Version passt.
|
||||||
|
add_action('plugins_loaded', function () {
|
||||||
|
if (get_option('aeromc_friends_db_version') !== AEROMC_FRIENDS_VERSION) {
|
||||||
|
aeromc_friends_install();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
require_once plugin_dir_path(__FILE__) . 'includes/routes.php';
|
require_once plugin_dir_path(__FILE__) . 'includes/routes.php';
|
||||||
|
if (is_admin()) {
|
||||||
|
require_once plugin_dir_path(__FILE__) . 'includes/admin.php';
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Admin-Seite "AeroMC Fehlerberichte": Liste, Detailansicht, Löschen.
|
||||||
|
* Nur für Administratoren (manage_options).
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
add_action('admin_menu', function () {
|
||||||
|
global $wpdb;
|
||||||
|
$table = $wpdb->prefix . AEROMC_FRIENDS_REPORTS_TABLE;
|
||||||
|
$neu = (int) $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE status = 'neu'");
|
||||||
|
$titel = 'AeroMC Fehler' . ($neu ? ' <span class="awaiting-mod">' . $neu . '</span>' : '');
|
||||||
|
add_menu_page('AeroMC Fehlerberichte', $titel, 'manage_options', 'aeromc-reports', 'aeromc_friends_reports_page', 'dashicons-sos', 81);
|
||||||
|
});
|
||||||
|
|
||||||
|
function aeromc_friends_reports_page() {
|
||||||
|
global $wpdb;
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$table = $wpdb->prefix . AEROMC_FRIENDS_REPORTS_TABLE;
|
||||||
|
$basis = admin_url('admin.php?page=aeromc-reports');
|
||||||
|
|
||||||
|
// Löschen (einzeln oder alle erledigten)
|
||||||
|
if (isset($_POST['aeromc_loeschen']) && check_admin_referer('aeromc_reports')) {
|
||||||
|
$id = (int) $_POST['aeromc_loeschen'];
|
||||||
|
$wpdb->delete($table, ['id' => $id]);
|
||||||
|
echo '<div class="notice notice-success"><p>Bericht #' . esc_html($id) . ' gelöscht.</p></div>';
|
||||||
|
}
|
||||||
|
if (isset($_POST['aeromc_status'], $_POST['aeromc_id']) && check_admin_referer('aeromc_reports')) {
|
||||||
|
$status = in_array($_POST['aeromc_status'], ['neu', 'gelesen', 'erledigt'], true) ? $_POST['aeromc_status'] : 'gelesen';
|
||||||
|
$wpdb->update($table, ['status' => $status], ['id' => (int) $_POST['aeromc_id']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '<div class="wrap"><h1>AeroMC Fehlerberichte</h1>';
|
||||||
|
|
||||||
|
// Detailansicht
|
||||||
|
if (isset($_GET['report'])) {
|
||||||
|
$id = (int) $_GET['report'];
|
||||||
|
$r = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE id = %d", $id));
|
||||||
|
if (!$r) {
|
||||||
|
echo '<p>Bericht nicht gefunden.</p></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ($r->status === 'neu') {
|
||||||
|
$wpdb->update($table, ['status' => 'gelesen'], ['id' => $id]);
|
||||||
|
$r->status = 'gelesen';
|
||||||
|
}
|
||||||
|
echo '<p><a href="' . esc_url($basis) . '">← Zur Liste</a></p>';
|
||||||
|
echo '<h2>Bericht #' . esc_html($r->id) . '</h2>';
|
||||||
|
echo '<table class="widefat striped" style="max-width:900px"><tbody>';
|
||||||
|
$zeile = function ($k, $v) {
|
||||||
|
echo '<tr><th style="width:180px">' . esc_html($k) . '</th><td>' . esc_html($v !== null && $v !== '' ? $v : '–') . '</td></tr>';
|
||||||
|
};
|
||||||
|
$zeile('Eingegangen (UTC)', $r->created_at);
|
||||||
|
$zeile('Spieler', $r->username ? $r->username . ' (' . $r->uuid . ')' : 'anonym');
|
||||||
|
$zeile('Launcher-Version', $r->launcher_version);
|
||||||
|
$zeile('Minecraft', $r->mc_version);
|
||||||
|
$zeile('Loader', trim($r->loader . ' ' . $r->loader_version));
|
||||||
|
$zeile('System', $r->system);
|
||||||
|
$zeile('Status', $r->status);
|
||||||
|
echo '</tbody></table>';
|
||||||
|
|
||||||
|
if ($r->kommentar) {
|
||||||
|
echo '<h3>Kommentar</h3><div style="max-width:900px;padding:10px 14px;background:#fff;border:1px solid #ccd0d4;white-space:pre-wrap">' . esc_html($r->kommentar) . '</div>';
|
||||||
|
}
|
||||||
|
$funde = json_decode((string) $r->funde, true) ?: [];
|
||||||
|
echo '<h3>Erkannte Ursachen (' . count($funde) . ')</h3>';
|
||||||
|
if ($funde) {
|
||||||
|
echo '<ul style="list-style:disc;padding-left:20px">';
|
||||||
|
foreach ($funde as $f) {
|
||||||
|
echo '<li><strong>' . esc_html($f['titel'] ?? '') . '</strong> – ' . esc_html($f['text'] ?? '') . '</li>';
|
||||||
|
}
|
||||||
|
echo '</ul>';
|
||||||
|
} else {
|
||||||
|
echo '<p>Keine – der Assistent konnte die Ursache nicht eindeutig erkennen.</p>';
|
||||||
|
}
|
||||||
|
$mods = json_decode((string) $r->mods, true) ?: [];
|
||||||
|
echo '<h3>Mods (' . count($mods) . ')</h3>';
|
||||||
|
if ($mods) {
|
||||||
|
echo '<table class="widefat striped" style="max-width:900px"><thead><tr><th>Datei</th><th>Name</th><th>Version</th><th>Aktiv</th></tr></thead><tbody>';
|
||||||
|
foreach ($mods as $m) {
|
||||||
|
echo '<tr><td>' . esc_html($m['datei'] ?? '') . '</td><td>' . esc_html($m['name'] ?? '') . '</td><td>'
|
||||||
|
. esc_html($m['version'] ?? '') . '</td><td>' . (!empty($m['aktiv']) ? 'ja' : 'nein') . '</td></tr>';
|
||||||
|
}
|
||||||
|
echo '</tbody></table>';
|
||||||
|
}
|
||||||
|
echo '<h3>Bericht / Protokoll</h3><textarea readonly style="width:100%;max-width:1200px;height:420px;font-family:monospace;font-size:12px">'
|
||||||
|
. esc_textarea($r->bericht) . '</textarea>';
|
||||||
|
|
||||||
|
echo '<form method="post" style="margin-top:14px;display:flex;gap:8px">';
|
||||||
|
wp_nonce_field('aeromc_reports');
|
||||||
|
echo '<input type="hidden" name="aeromc_id" value="' . esc_attr($r->id) . '" />';
|
||||||
|
echo '<button class="button" name="aeromc_status" value="erledigt">Als erledigt markieren</button>';
|
||||||
|
echo '<button class="button" name="aeromc_status" value="neu">Wieder als neu markieren</button>';
|
||||||
|
echo '<button class="button button-link-delete" name="aeromc_loeschen" value="' . esc_attr($r->id) . '" onclick="return confirm(\'Bericht wirklich löschen?\')">Löschen</button>';
|
||||||
|
echo '</form></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Liste
|
||||||
|
$rows = $wpdb->get_results("SELECT id, created_at, username, launcher_version, mc_version, loader, funde, kommentar, status FROM $table ORDER BY id DESC LIMIT 200");
|
||||||
|
if (!$rows) {
|
||||||
|
echo '<p>Noch keine Fehlerberichte eingegangen.</p></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
echo '<table class="widefat striped"><thead><tr><th>#</th><th>Eingegangen (UTC)</th><th>Spieler</th><th>Launcher</th><th>Minecraft</th><th>Erkannte Ursache</th><th>Kommentar</th><th>Status</th></tr></thead><tbody>';
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$funde = json_decode((string) $r->funde, true) ?: [];
|
||||||
|
$ursache = $funde ? ($funde[0]['titel'] ?? '') . (count($funde) > 1 ? ' (+' . (count($funde) - 1) . ')' : '') : '–';
|
||||||
|
$fett = $r->status === 'neu' ? 'font-weight:600' : '';
|
||||||
|
echo '<tr style="' . $fett . '"><td><a href="' . esc_url(add_query_arg('report', $r->id, $basis)) . '">#' . esc_html($r->id) . '</a></td>'
|
||||||
|
. '<td>' . esc_html($r->created_at) . '</td>'
|
||||||
|
. '<td>' . esc_html($r->username ?: 'anonym') . '</td>'
|
||||||
|
. '<td>' . esc_html($r->launcher_version) . '</td>'
|
||||||
|
. '<td>' . esc_html(trim($r->mc_version . ' ' . $r->loader)) . '</td>'
|
||||||
|
. '<td>' . esc_html($ursache) . '</td>'
|
||||||
|
. '<td>' . esc_html(mb_strimwidth((string) $r->kommentar, 0, 60, '…')) . '</td>'
|
||||||
|
. '<td>' . esc_html($r->status) . '</td></tr>';
|
||||||
|
}
|
||||||
|
echo '</tbody></table></div>';
|
||||||
|
}
|
||||||
@@ -2,11 +2,15 @@
|
|||||||
/**
|
/**
|
||||||
* REST-Routen unter /wp-json/aeromc/v1/... für das Freundesystem.
|
* REST-Routen unter /wp-json/aeromc/v1/... für das Freundesystem.
|
||||||
*
|
*
|
||||||
* Bewusst OHNE echte Authentifizierung (Absprache mit dem Nutzer): der
|
* Anmeldung wie bei Minecraft-Servern: Der Launcher holt sich eine Aufgabe
|
||||||
* Launcher meldet sich einfach mit der Minecraft-UUID des aktiven Kontos an.
|
* (/auth/challenge), meldet sie mit seinem Minecraft-Zugang bei Mojang an
|
||||||
* Wer eine fremde UUID kennt, könnte sich theoretisch als dieser Spieler
|
* (sessionserver .../join), und das Plugin fragt bei Mojang nach, ob genau
|
||||||
* ausgeben - für den privaten Rahmen dieses Projekts akzeptiert. Eingaben
|
* dieser Spieler das getan hat (hasJoined). Erst dann gibt es ein Sitzungs-
|
||||||
* werden trotzdem serverseitig geprüft (Format von UUID/Name) und jede
|
* Token (Kopfzeile X-AeroMC-Token), das jede Anfrage "als dieser Spieler"
|
||||||
|
* mitbringen muss - damit kann sich niemand mehr mit einer fremden UUID
|
||||||
|
* ausgeben. Übergangsmodus siehe AEROMC_FRIENDS_REQUIRE_AUTH.
|
||||||
|
*
|
||||||
|
* Eingaben werden serverseitig geprüft (Format von UUID/Name) und jede
|
||||||
* Datenbankabfrage läuft ausschließlich über $wpdb->prepare, um SQL-Injection
|
* Datenbankabfrage läuft ausschließlich über $wpdb->prepare, um SQL-Injection
|
||||||
* auszuschließen.
|
* auszuschließen.
|
||||||
*/
|
*/
|
||||||
@@ -20,7 +24,159 @@ if (!defined('ABSPATH')) {
|
|||||||
define('AEROMC_FRIENDS_ONLINE_SECONDS', 180);
|
define('AEROMC_FRIENDS_ONLINE_SECONDS', 180);
|
||||||
// Häufigere Herzschläge als das werden ignoriert (Schutz gegen Missbrauch/
|
// Häufigere Herzschläge als das werden ignoriert (Schutz gegen Missbrauch/
|
||||||
// versehentliche Anfrageflut) - der Launcher selbst sendet ohnehin nur alle 60s.
|
// versehentliche Anfrageflut) - der Launcher selbst sendet ohnehin nur alle 60s.
|
||||||
|
// Ausnahme: hat sich der Server geändert, wird sofort gespeichert ("Beitreten").
|
||||||
define('AEROMC_FRIENDS_HEARTBEAT_MIN_GAP', 20);
|
define('AEROMC_FRIENDS_HEARTBEAT_MIN_GAP', 20);
|
||||||
|
// So lange ist eine Einladung "Komm auf meinen Server" gültig.
|
||||||
|
define('AEROMC_FRIENDS_INVITE_SECONDS', 600);
|
||||||
|
// Frühestens nach so vielen Sekunden darf derselbe Freund erneut eingeladen werden.
|
||||||
|
define('AEROMC_FRIENDS_INVITE_MIN_GAP', 30);
|
||||||
|
|
||||||
|
// Sitzungen verfallen, wenn sie so lange nicht benutzt wurden.
|
||||||
|
define('AEROMC_FRIENDS_SESSION_DAYS', 30);
|
||||||
|
// So lange ist ein Teilen-Code gültig.
|
||||||
|
define('AEROMC_FRIENDS_SHARE_DAYS', 30);
|
||||||
|
// Höchstens so viele geteilte Instanzen pro Spieler und Tag.
|
||||||
|
define('AEROMC_FRIENDS_SHARES_PER_DAY', 30);
|
||||||
|
// Größe einer geteilten Mod-Liste (JSON) in Bytes.
|
||||||
|
define('AEROMC_FRIENDS_SHARE_MAX_BYTES', 262144);
|
||||||
|
// Kurznachrichten: Länge, Aufbewahrung, höchstens so viele pro Minute.
|
||||||
|
define('AEROMC_FRIENDS_MESSAGE_MAX_CHARS', 200);
|
||||||
|
define('AEROMC_FRIENDS_MESSAGE_DAYS', 7);
|
||||||
|
define('AEROMC_FRIENDS_MESSAGES_PER_MINUTE', 10);
|
||||||
|
// Bilder in Nachrichten (geteilte Screenshots): Höchstgröße und Ordner unter wp-content/uploads
|
||||||
|
define('AEROMC_FRIENDS_IMAGE_MAX_BYTES', 2500000);
|
||||||
|
define('AEROMC_FRIENDS_IMAGE_DIR', 'aeromc-bilder');
|
||||||
|
|
||||||
|
/** Speichert ein Bild (JPEG/PNG, base64) unter zufälligem Namen; gibt die URL zurück oder WP_Error. */
|
||||||
|
function aeromc_friends_save_image($base64) {
|
||||||
|
$daten = base64_decode(preg_replace('/^data:image\/\w+;base64,/', '', (string) $base64), true);
|
||||||
|
if ($daten === false || strlen($daten) < 100) {
|
||||||
|
return new WP_Error('aeromc_invalid', 'Ungültiges Bild.', ['status' => 400]);
|
||||||
|
}
|
||||||
|
if (strlen($daten) > AEROMC_FRIENDS_IMAGE_MAX_BYTES) {
|
||||||
|
return new WP_Error('aeromc_too_big', 'Das Bild ist zu groß.', ['status' => 413]);
|
||||||
|
}
|
||||||
|
// nur echte JPEG- oder PNG-Dateien annehmen (Dateikopf prüfen, nicht der Angabe trauen)
|
||||||
|
if (strncmp($daten, "\xFF\xD8\xFF", 3) === 0) {
|
||||||
|
$endung = 'jpg';
|
||||||
|
} elseif (strncmp($daten, "\x89PNG\r\n\x1A\n", 8) === 0) {
|
||||||
|
$endung = 'png';
|
||||||
|
} else {
|
||||||
|
return new WP_Error('aeromc_invalid', 'Nur JPEG- oder PNG-Bilder.', ['status' => 400]);
|
||||||
|
}
|
||||||
|
$uploads = wp_upload_dir();
|
||||||
|
$ordner = trailingslashit($uploads['basedir']) . AEROMC_FRIENDS_IMAGE_DIR;
|
||||||
|
if (!wp_mkdir_p($ordner)) {
|
||||||
|
return new WP_Error('aeromc_error', 'Bild konnte nicht gespeichert werden.', ['status' => 500]);
|
||||||
|
}
|
||||||
|
// Verzeichnisliste verhindern
|
||||||
|
if (!file_exists($ordner . '/index.html')) {
|
||||||
|
file_put_contents($ordner . '/index.html', '');
|
||||||
|
}
|
||||||
|
$name = bin2hex(random_bytes(16)) . '.' . $endung;
|
||||||
|
if (file_put_contents($ordner . '/' . $name, $daten) === false) {
|
||||||
|
return new WP_Error('aeromc_error', 'Bild konnte nicht gespeichert werden.', ['status' => 500]);
|
||||||
|
}
|
||||||
|
return trailingslashit($uploads['baseurl']) . AEROMC_FRIENDS_IMAGE_DIR . '/' . $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bilddatei zu einer gespeicherten URL löschen (nur aus unserem Ordner). */
|
||||||
|
function aeromc_friends_delete_image($url) {
|
||||||
|
if (!$url) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$name = basename((string) $url);
|
||||||
|
if (!preg_match('/^[a-f0-9]{32}\.(jpg|png)$/', $name)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$uploads = wp_upload_dir();
|
||||||
|
$datei = trailingslashit($uploads['basedir']) . AEROMC_FRIENDS_IMAGE_DIR . '/' . $name;
|
||||||
|
if (file_exists($datei)) {
|
||||||
|
@unlink($datei);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fehlerberichte: Größe des mitgeschickten Berichts, höchstens so viele pro Stunde und Absender.
|
||||||
|
define('AEROMC_FRIENDS_REPORT_MAX_BYTES', 200000);
|
||||||
|
define('AEROMC_FRIENDS_REPORTS_PER_HOUR', 5);
|
||||||
|
|
||||||
|
function aeromc_friends_messages_table() {
|
||||||
|
global $wpdb;
|
||||||
|
return $wpdb->prefix . AEROMC_FRIENDS_MESSAGES_TABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
function aeromc_friends_reports_table() {
|
||||||
|
global $wpdb;
|
||||||
|
return $wpdb->prefix . AEROMC_FRIENDS_REPORTS_TABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
function aeromc_friends_sessions_table() {
|
||||||
|
global $wpdb;
|
||||||
|
return $wpdb->prefix . AEROMC_FRIENDS_SESSIONS_TABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
function aeromc_friends_shares_table() {
|
||||||
|
global $wpdb;
|
||||||
|
return $wpdb->prefix . AEROMC_FRIENDS_SHARES_TABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft, ob die Anfrage wirklich von diesem Spieler kommt.
|
||||||
|
* Gibt die Sitzung (Objekt mit username) zurück, null im Übergangsmodus
|
||||||
|
* ohne Token, oder WP_Error (401), wenn die Anmeldung fehlt oder ungültig ist.
|
||||||
|
*/
|
||||||
|
function aeromc_friends_check_auth($request, $uuid) {
|
||||||
|
global $wpdb;
|
||||||
|
$table = aeromc_friends_sessions_table();
|
||||||
|
$token = (string) $request->get_header('x-aeromc-token');
|
||||||
|
if ($token !== '') {
|
||||||
|
$hash = hash('sha256', $token);
|
||||||
|
$grenze = gmdate('Y-m-d H:i:s', time() - AEROMC_FRIENDS_SESSION_DAYS * 86400);
|
||||||
|
$sitzung = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT uuid, username, last_used FROM $table WHERE token_hash = %s AND last_used >= %s", $hash, $grenze
|
||||||
|
));
|
||||||
|
if (!$sitzung || $sitzung->uuid !== $uuid) {
|
||||||
|
return new WP_Error('aeromc_auth', 'Anmeldung ungültig oder abgelaufen.', ['status' => 401]);
|
||||||
|
}
|
||||||
|
// "zuletzt benutzt" höchstens stündlich schreiben
|
||||||
|
if (time() - strtotime($sitzung->last_used) > 3600) {
|
||||||
|
$wpdb->update($table, ['last_used' => current_time('mysql', true)], ['token_hash' => $hash]);
|
||||||
|
}
|
||||||
|
return $sitzung;
|
||||||
|
}
|
||||||
|
// ohne Token: nur im Übergangsmodus und nur, solange dieser Spieler sich
|
||||||
|
// noch nie angemeldet hat - sonst könnte man sich weiter als er ausgeben
|
||||||
|
$schon_angemeldet = $wpdb->get_var($wpdb->prepare("SELECT 1 FROM $table WHERE uuid = %s LIMIT 1", $uuid));
|
||||||
|
if (AEROMC_FRIENDS_REQUIRE_AUTH || $schon_angemeldet) {
|
||||||
|
return new WP_Error('aeromc_auth', 'Anmeldung erforderlich.', ['status' => 401]);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function aeromc_friends_invites_table() {
|
||||||
|
global $wpdb;
|
||||||
|
return $wpdb->prefix . AEROMC_FRIENDS_INVITES_TABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Server-Adresse (Host oder IP, optional :Port) - nur harmlose Zeichen. */
|
||||||
|
function aeromc_friends_valid_server($server) {
|
||||||
|
return is_string($server) && preg_match('/^[A-Za-z0-9.\-_:\[\]]{1,128}$/', $server);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Minecraft-Version wie "1.21.1", "24w14a" oder "1.20.1-pre2". */
|
||||||
|
function aeromc_friends_clean_version($version) {
|
||||||
|
$version = (string) $version;
|
||||||
|
return preg_match('/^[A-Za-z0-9.\-_ ]{1,32}$/', $version) ? $version : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sind die beiden befreundet (Anfrage angenommen)? */
|
||||||
|
function aeromc_friends_are_friends($a, $b) {
|
||||||
|
global $wpdb;
|
||||||
|
$friends_table = aeromc_friends_friends_table();
|
||||||
|
return (bool) $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT id FROM $friends_table WHERE ((uuid_a = %s AND uuid_b = %s) OR (uuid_a = %s AND uuid_b = %s)) AND status = 'accepted'",
|
||||||
|
$a, $b, $b, $a
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
function aeromc_friends_presence_table() {
|
function aeromc_friends_presence_table() {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
@@ -49,6 +205,53 @@ function aeromc_friends_valid_username($name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
add_action('rest_api_init', function () {
|
add_action('rest_api_init', function () {
|
||||||
|
register_rest_route('aeromc/v1', '/auth/challenge', [
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => 'aeromc_friends_route_auth_challenge',
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
]);
|
||||||
|
register_rest_route('aeromc/v1', '/auth/verify', [
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => 'aeromc_friends_route_auth_verify',
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
]);
|
||||||
|
register_rest_route('aeromc/v1', '/share', [
|
||||||
|
[
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => 'aeromc_friends_route_share_create',
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => 'aeromc_friends_route_share_get',
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
register_rest_route('aeromc/v1', '/friends/message', [
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => 'aeromc_friends_route_message_send',
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
]);
|
||||||
|
register_rest_route('aeromc/v1', '/friends/messages', [
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => 'aeromc_friends_route_messages',
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
]);
|
||||||
|
register_rest_route('aeromc/v1', '/friends/messages/unread', [
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => 'aeromc_friends_route_messages_unread',
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
]);
|
||||||
|
register_rest_route('aeromc/v1', '/friends/messages/read', [
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => 'aeromc_friends_route_messages_read',
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
]);
|
||||||
|
register_rest_route('aeromc/v1', '/report', [
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => 'aeromc_friends_route_report',
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
]);
|
||||||
register_rest_route('aeromc/v1', '/presence', [
|
register_rest_route('aeromc/v1', '/presence', [
|
||||||
'methods' => 'POST',
|
'methods' => 'POST',
|
||||||
'callback' => 'aeromc_friends_route_presence',
|
'callback' => 'aeromc_friends_route_presence',
|
||||||
@@ -89,9 +292,24 @@ add_action('rest_api_init', function () {
|
|||||||
'callback' => 'aeromc_friends_route_remove',
|
'callback' => 'aeromc_friends_route_remove',
|
||||||
'permission_callback' => '__return_true',
|
'permission_callback' => '__return_true',
|
||||||
]);
|
]);
|
||||||
|
register_rest_route('aeromc/v1', '/friends/invite', [
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => 'aeromc_friends_route_invite',
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
]);
|
||||||
|
register_rest_route('aeromc/v1', '/friends/invites', [
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => 'aeromc_friends_route_invites',
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
]);
|
||||||
|
register_rest_route('aeromc/v1', '/friends/invites/dismiss', [
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => 'aeromc_friends_route_invite_dismiss',
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
/** POST /presence {uuid, username, instance, server} */
|
/** POST /presence {uuid, username, instance, server, version} */
|
||||||
function aeromc_friends_route_presence($request) {
|
function aeromc_friends_route_presence($request) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
||||||
@@ -99,14 +317,23 @@ function aeromc_friends_route_presence($request) {
|
|||||||
if (!$uuid || !aeromc_friends_valid_username($username)) {
|
if (!$uuid || !aeromc_friends_valid_username($username)) {
|
||||||
return new WP_Error('aeromc_invalid', 'Ungültige uuid/username.', ['status' => 400]);
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid/username.', ['status' => 400]);
|
||||||
}
|
}
|
||||||
|
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||||||
|
if (is_wp_error($sitzung)) {
|
||||||
|
return $sitzung;
|
||||||
|
}
|
||||||
|
if ($sitzung) {
|
||||||
|
$username = $sitzung->username; // von Mojang bestätigt
|
||||||
|
}
|
||||||
$instance = sanitize_text_field((string) $request->get_param('instance'));
|
$instance = sanitize_text_field((string) $request->get_param('instance'));
|
||||||
$server = sanitize_text_field((string) $request->get_param('server'));
|
$server = sanitize_text_field((string) $request->get_param('server'));
|
||||||
|
$version = aeromc_friends_clean_version($request->get_param('version'));
|
||||||
$table = aeromc_friends_presence_table();
|
$table = aeromc_friends_presence_table();
|
||||||
|
|
||||||
$letzte = $wpdb->get_var($wpdb->prepare(
|
$letzte = $wpdb->get_row($wpdb->prepare(
|
||||||
"SELECT last_seen FROM $table WHERE uuid = %s", $uuid
|
"SELECT last_seen, server_label FROM $table WHERE uuid = %s", $uuid
|
||||||
));
|
));
|
||||||
if ($letzte && (time() - strtotime($letzte)) < AEROMC_FRIENDS_HEARTBEAT_MIN_GAP) {
|
$server_gleich = $letzte && (string) $letzte->server_label === (string) $server;
|
||||||
|
if ($letzte && $server_gleich && (time() - strtotime($letzte->last_seen)) < AEROMC_FRIENDS_HEARTBEAT_MIN_GAP) {
|
||||||
return ['ok' => true]; // zu häufig - stillschweigend ignoriert
|
return ['ok' => true]; // zu häufig - stillschweigend ignoriert
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,11 +345,11 @@ function aeromc_friends_route_presence($request) {
|
|||||||
// Zeitzone praktisch nie als offline erkannt worden wäre.
|
// Zeitzone praktisch nie als offline erkannt worden wäre.
|
||||||
$jetzt_utc = current_time('mysql', true);
|
$jetzt_utc = current_time('mysql', true);
|
||||||
$wpdb->query($wpdb->prepare(
|
$wpdb->query($wpdb->prepare(
|
||||||
"INSERT INTO $table (uuid, username, instance_name, server_label, last_seen)
|
"INSERT INTO $table (uuid, username, instance_name, server_label, mc_version, last_seen)
|
||||||
VALUES (%s, %s, %s, %s, %s)
|
VALUES (%s, %s, %s, %s, %s, %s)
|
||||||
ON DUPLICATE KEY UPDATE username = %s, instance_name = %s, server_label = %s, last_seen = %s",
|
ON DUPLICATE KEY UPDATE username = %s, instance_name = %s, server_label = %s, mc_version = %s, last_seen = %s",
|
||||||
$uuid, $username, $instance ?: null, $server ?: null, $jetzt_utc,
|
$uuid, $username, $instance ?: null, $server ?: null, $version ?: null, $jetzt_utc,
|
||||||
$username, $instance ?: null, $server ?: null, $jetzt_utc
|
$username, $instance ?: null, $server ?: null, $version ?: null, $jetzt_utc
|
||||||
));
|
));
|
||||||
return ['ok' => true];
|
return ['ok' => true];
|
||||||
}
|
}
|
||||||
@@ -134,6 +361,10 @@ function aeromc_friends_route_list($request) {
|
|||||||
if (!$uuid) {
|
if (!$uuid) {
|
||||||
return new WP_Error('aeromc_invalid', 'Ungültige uuid.', ['status' => 400]);
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid.', ['status' => 400]);
|
||||||
}
|
}
|
||||||
|
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||||||
|
if (is_wp_error($sitzung)) {
|
||||||
|
return $sitzung;
|
||||||
|
}
|
||||||
$friends_table = aeromc_friends_friends_table();
|
$friends_table = aeromc_friends_friends_table();
|
||||||
$presence_table = aeromc_friends_presence_table();
|
$presence_table = aeromc_friends_presence_table();
|
||||||
|
|
||||||
@@ -147,7 +378,7 @@ function aeromc_friends_route_list($request) {
|
|||||||
$ergebnis = [];
|
$ergebnis = [];
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
$p = $wpdb->get_row($wpdb->prepare(
|
$p = $wpdb->get_row($wpdb->prepare(
|
||||||
"SELECT username, instance_name, server_label, last_seen FROM $presence_table WHERE uuid = %s",
|
"SELECT username, instance_name, server_label, mc_version, last_seen FROM $presence_table WHERE uuid = %s",
|
||||||
$row->other_uuid
|
$row->other_uuid
|
||||||
));
|
));
|
||||||
$online = $p && (time() - strtotime($p->last_seen)) < AEROMC_FRIENDS_ONLINE_SECONDS;
|
$online = $p && (time() - strtotime($p->last_seen)) < AEROMC_FRIENDS_ONLINE_SECONDS;
|
||||||
@@ -157,6 +388,7 @@ function aeromc_friends_route_list($request) {
|
|||||||
'online' => (bool) $online,
|
'online' => (bool) $online,
|
||||||
'instance' => $online ? $p->instance_name : null,
|
'instance' => $online ? $p->instance_name : null,
|
||||||
'server' => $online ? $p->server_label : null,
|
'server' => $online ? $p->server_label : null,
|
||||||
|
'version' => $online ? $p->mc_version : null,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return $ergebnis;
|
return $ergebnis;
|
||||||
@@ -169,6 +401,10 @@ function aeromc_friends_route_requests($request) {
|
|||||||
if (!$uuid) {
|
if (!$uuid) {
|
||||||
return new WP_Error('aeromc_invalid', 'Ungültige uuid.', ['status' => 400]);
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid.', ['status' => 400]);
|
||||||
}
|
}
|
||||||
|
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||||||
|
if (is_wp_error($sitzung)) {
|
||||||
|
return $sitzung;
|
||||||
|
}
|
||||||
$friends_table = aeromc_friends_friends_table();
|
$friends_table = aeromc_friends_friends_table();
|
||||||
$presence_table = aeromc_friends_presence_table();
|
$presence_table = aeromc_friends_presence_table();
|
||||||
|
|
||||||
@@ -191,6 +427,10 @@ function aeromc_friends_route_sent($request) {
|
|||||||
if (!$uuid) {
|
if (!$uuid) {
|
||||||
return new WP_Error('aeromc_invalid', 'Ungültige uuid.', ['status' => 400]);
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid.', ['status' => 400]);
|
||||||
}
|
}
|
||||||
|
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||||||
|
if (is_wp_error($sitzung)) {
|
||||||
|
return $sitzung;
|
||||||
|
}
|
||||||
$friends_table = aeromc_friends_friends_table();
|
$friends_table = aeromc_friends_friends_table();
|
||||||
$presence_table = aeromc_friends_presence_table();
|
$presence_table = aeromc_friends_presence_table();
|
||||||
|
|
||||||
@@ -214,6 +454,10 @@ function aeromc_friends_route_request($request) {
|
|||||||
if (!$uuid || !aeromc_friends_valid_username($targetUsername)) {
|
if (!$uuid || !aeromc_friends_valid_username($targetUsername)) {
|
||||||
return new WP_Error('aeromc_invalid', 'Ungültige uuid/targetUsername.', ['status' => 400]);
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid/targetUsername.', ['status' => 400]);
|
||||||
}
|
}
|
||||||
|
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||||||
|
if (is_wp_error($sitzung)) {
|
||||||
|
return $sitzung;
|
||||||
|
}
|
||||||
$presence_table = aeromc_friends_presence_table();
|
$presence_table = aeromc_friends_presence_table();
|
||||||
$friends_table = aeromc_friends_friends_table();
|
$friends_table = aeromc_friends_friends_table();
|
||||||
|
|
||||||
@@ -254,6 +498,10 @@ function aeromc_friends_route_respond($request) {
|
|||||||
if (!$uuid || !$other) {
|
if (!$uuid || !$other) {
|
||||||
return new WP_Error('aeromc_invalid', 'Ungültige uuid/otherUuid.', ['status' => 400]);
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid/otherUuid.', ['status' => 400]);
|
||||||
}
|
}
|
||||||
|
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||||||
|
if (is_wp_error($sitzung)) {
|
||||||
|
return $sitzung;
|
||||||
|
}
|
||||||
$friends_table = aeromc_friends_friends_table();
|
$friends_table = aeromc_friends_friends_table();
|
||||||
|
|
||||||
if ($accept) {
|
if ($accept) {
|
||||||
@@ -278,6 +526,10 @@ function aeromc_friends_route_cancel($request) {
|
|||||||
if (!$uuid || !$other) {
|
if (!$uuid || !$other) {
|
||||||
return new WP_Error('aeromc_invalid', 'Ungültige uuid/otherUuid.', ['status' => 400]);
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid/otherUuid.', ['status' => 400]);
|
||||||
}
|
}
|
||||||
|
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||||||
|
if (is_wp_error($sitzung)) {
|
||||||
|
return $sitzung;
|
||||||
|
}
|
||||||
$friends_table = aeromc_friends_friends_table();
|
$friends_table = aeromc_friends_friends_table();
|
||||||
$wpdb->query($wpdb->prepare(
|
$wpdb->query($wpdb->prepare(
|
||||||
"DELETE FROM $friends_table WHERE uuid_a = %s AND uuid_b = %s AND status = 'pending'",
|
"DELETE FROM $friends_table WHERE uuid_a = %s AND uuid_b = %s AND status = 'pending'",
|
||||||
@@ -294,6 +546,10 @@ function aeromc_friends_route_remove($request) {
|
|||||||
if (!$uuid || !$other) {
|
if (!$uuid || !$other) {
|
||||||
return new WP_Error('aeromc_invalid', 'Ungültige uuid/otherUuid.', ['status' => 400]);
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid/otherUuid.', ['status' => 400]);
|
||||||
}
|
}
|
||||||
|
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||||||
|
if (is_wp_error($sitzung)) {
|
||||||
|
return $sitzung;
|
||||||
|
}
|
||||||
$friends_table = aeromc_friends_friends_table();
|
$friends_table = aeromc_friends_friends_table();
|
||||||
$wpdb->query($wpdb->prepare(
|
$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'",
|
"DELETE FROM $friends_table WHERE ((uuid_a = %s AND uuid_b = %s) OR (uuid_a = %s AND uuid_b = %s)) AND status = 'accepted'",
|
||||||
@@ -301,3 +557,471 @@ function aeromc_friends_route_remove($request) {
|
|||||||
));
|
));
|
||||||
return ['ok' => true];
|
return ['ok' => true];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** POST /friends/invite {uuid, targetUuid, server, version} - "Komm auf meinen Server" */
|
||||||
|
function aeromc_friends_route_invite($request) {
|
||||||
|
global $wpdb;
|
||||||
|
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
||||||
|
$target = aeromc_friends_normalize_uuid($request->get_param('targetUuid'));
|
||||||
|
$server = (string) $request->get_param('server');
|
||||||
|
$version = aeromc_friends_clean_version($request->get_param('version'));
|
||||||
|
if (!$uuid || !$target || !aeromc_friends_valid_server($server)) {
|
||||||
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid/targetUuid/server.', ['status' => 400]);
|
||||||
|
}
|
||||||
|
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||||||
|
if (is_wp_error($sitzung)) {
|
||||||
|
return $sitzung;
|
||||||
|
}
|
||||||
|
if (!aeromc_friends_are_friends($uuid, $target)) {
|
||||||
|
return new WP_Error('aeromc_not_friends', 'Ihr seid nicht befreundet.', ['status' => 403]);
|
||||||
|
}
|
||||||
|
$table = aeromc_friends_invites_table();
|
||||||
|
|
||||||
|
$letzte = $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT created_at FROM $table WHERE from_uuid = %s AND to_uuid = %s ORDER BY id DESC LIMIT 1",
|
||||||
|
$uuid, $target
|
||||||
|
));
|
||||||
|
if ($letzte && (time() - strtotime($letzte)) < AEROMC_FRIENDS_INVITE_MIN_GAP) {
|
||||||
|
return new WP_Error('aeromc_too_fast', 'Du hast diesen Freund gerade erst eingeladen.', ['status' => 429]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ältere Einladung an denselben Freund ersetzen
|
||||||
|
$wpdb->query($wpdb->prepare(
|
||||||
|
"DELETE FROM $table WHERE from_uuid = %s AND to_uuid = %s", $uuid, $target
|
||||||
|
));
|
||||||
|
$wpdb->insert($table, [
|
||||||
|
'from_uuid' => $uuid,
|
||||||
|
'to_uuid' => $target,
|
||||||
|
'server' => $server,
|
||||||
|
'mc_version' => $version ?: null,
|
||||||
|
'created_at' => current_time('mysql', true),
|
||||||
|
]);
|
||||||
|
return ['ok' => true];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GET /friends/invites?uuid= - an mich gerichtete, noch gültige Einladungen */
|
||||||
|
function aeromc_friends_route_invites($request) {
|
||||||
|
global $wpdb;
|
||||||
|
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
||||||
|
if (!$uuid) {
|
||||||
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid.', ['status' => 400]);
|
||||||
|
}
|
||||||
|
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||||||
|
if (is_wp_error($sitzung)) {
|
||||||
|
return $sitzung;
|
||||||
|
}
|
||||||
|
$table = aeromc_friends_invites_table();
|
||||||
|
$presence_table = aeromc_friends_presence_table();
|
||||||
|
|
||||||
|
// abgelaufene Einladungen (aller Spieler) wegräumen
|
||||||
|
$grenze = gmdate('Y-m-d H:i:s', time() - AEROMC_FRIENDS_INVITE_SECONDS);
|
||||||
|
$wpdb->query($wpdb->prepare("DELETE FROM $table WHERE created_at < %s", $grenze));
|
||||||
|
|
||||||
|
$rows = $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT i.id, i.from_uuid, p.username AS from_username, i.server, i.mc_version, i.created_at
|
||||||
|
FROM $table i
|
||||||
|
LEFT JOIN $presence_table p ON p.uuid = i.from_uuid
|
||||||
|
WHERE i.to_uuid = %s
|
||||||
|
ORDER BY i.id DESC",
|
||||||
|
$uuid
|
||||||
|
));
|
||||||
|
return array_map(function ($r) {
|
||||||
|
return [
|
||||||
|
'id' => (int) $r->id,
|
||||||
|
'fromUuid' => $r->from_uuid,
|
||||||
|
'fromUsername' => $r->from_username,
|
||||||
|
'server' => $r->server,
|
||||||
|
'version' => $r->mc_version,
|
||||||
|
'createdAt' => $r->created_at,
|
||||||
|
];
|
||||||
|
}, $rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST /friends/invites/dismiss {uuid, id} - Einladung erledigt/abgelehnt */
|
||||||
|
function aeromc_friends_route_invite_dismiss($request) {
|
||||||
|
global $wpdb;
|
||||||
|
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
||||||
|
$id = (int) $request->get_param('id');
|
||||||
|
if (!$uuid || $id <= 0) {
|
||||||
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid/id.', ['status' => 400]);
|
||||||
|
}
|
||||||
|
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||||||
|
if (is_wp_error($sitzung)) {
|
||||||
|
return $sitzung;
|
||||||
|
}
|
||||||
|
$table = aeromc_friends_invites_table();
|
||||||
|
$wpdb->query($wpdb->prepare("DELETE FROM $table WHERE id = %d AND to_uuid = %s", $id, $uuid));
|
||||||
|
return ['ok' => true];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Anmeldung über Mojangs Sitzungsprüfung
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** POST /auth/challenge {uuid, username} - Aufgabe für die Anmeldung bei Mojang */
|
||||||
|
function aeromc_friends_route_auth_challenge($request) {
|
||||||
|
$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]);
|
||||||
|
}
|
||||||
|
$server_id = bin2hex(random_bytes(20));
|
||||||
|
set_transient('aeromc_ch_' . $uuid, ['sid' => $server_id, 'name' => $username], 120);
|
||||||
|
return ['serverId' => $server_id];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST /auth/verify {uuid} - fragt bei Mojang nach und gibt ein Sitzungs-Token aus */
|
||||||
|
function aeromc_friends_route_auth_verify($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]);
|
||||||
|
}
|
||||||
|
$aufgabe = get_transient('aeromc_ch_' . $uuid);
|
||||||
|
delete_transient('aeromc_ch_' . $uuid); // nur einmal verwendbar
|
||||||
|
if (!is_array($aufgabe) || empty($aufgabe['sid'])) {
|
||||||
|
return new WP_Error('aeromc_auth', 'Keine oder abgelaufene Anmelde-Aufgabe.', ['status' => 403]);
|
||||||
|
}
|
||||||
|
$url = 'https://sessionserver.mojang.com/session/minecraft/hasJoined?username='
|
||||||
|
. rawurlencode($aufgabe['name']) . '&serverId=' . rawurlencode($aufgabe['sid']);
|
||||||
|
$antwort = wp_remote_get($url, ['timeout' => 10]);
|
||||||
|
if (is_wp_error($antwort) || wp_remote_retrieve_response_code($antwort) !== 200) {
|
||||||
|
return new WP_Error('aeromc_auth', 'Mojang hat die Anmeldung nicht bestätigt.', ['status' => 403]);
|
||||||
|
}
|
||||||
|
$profil = json_decode(wp_remote_retrieve_body($antwort), true);
|
||||||
|
if (!is_array($profil) || aeromc_friends_normalize_uuid($profil['id'] ?? '') !== $uuid
|
||||||
|
|| !aeromc_friends_valid_username($profil['name'] ?? '')) {
|
||||||
|
return new WP_Error('aeromc_auth', 'Mojang hat einen anderen Spieler gemeldet.', ['status' => 403]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$table = aeromc_friends_sessions_table();
|
||||||
|
// abgelaufene Sitzungen (aller Spieler) wegräumen
|
||||||
|
$grenze = gmdate('Y-m-d H:i:s', time() - AEROMC_FRIENDS_SESSION_DAYS * 86400);
|
||||||
|
$wpdb->query($wpdb->prepare("DELETE FROM $table WHERE last_used < %s", $grenze));
|
||||||
|
|
||||||
|
$token = bin2hex(random_bytes(32));
|
||||||
|
$jetzt = current_time('mysql', true);
|
||||||
|
$wpdb->insert($table, [
|
||||||
|
'token_hash' => hash('sha256', $token),
|
||||||
|
'uuid' => $uuid,
|
||||||
|
'username' => $profil['name'],
|
||||||
|
'created_at' => $jetzt,
|
||||||
|
'last_used' => $jetzt,
|
||||||
|
]);
|
||||||
|
return ['token' => $token, 'username' => $profil['name']];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Instanzen per Code teilen
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Code ohne verwechselbare Zeichen (kein 0/O, 1/I/L), Form XXXX-XXXX. */
|
||||||
|
function aeromc_friends_new_share_code() {
|
||||||
|
$zeichen = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789';
|
||||||
|
$code = '';
|
||||||
|
for ($i = 0; $i < 8; $i++) {
|
||||||
|
$code .= $zeichen[random_int(0, strlen($zeichen) - 1)];
|
||||||
|
if ($i === 3) {
|
||||||
|
$code .= '-';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $code;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Eingegebenen Code vereinheitlichen ("ab12 cd34" -> "AB12-CD34"), false wenn ungültig. */
|
||||||
|
function aeromc_friends_normalize_share_code($code) {
|
||||||
|
$roh = strtoupper(preg_replace('/[^A-Za-z0-9]/', '', (string) $code));
|
||||||
|
if (strlen($roh) !== 8) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return substr($roh, 0, 4) . '-' . substr($roh, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST /share {uuid, manifest} - Mod-Liste einer Instanz hinterlegen, Code zurück */
|
||||||
|
function aeromc_friends_route_share_create($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]);
|
||||||
|
}
|
||||||
|
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||||||
|
if (is_wp_error($sitzung)) {
|
||||||
|
return $sitzung;
|
||||||
|
}
|
||||||
|
$manifest = $request->get_param('manifest');
|
||||||
|
if (!is_array($manifest) || empty($manifest['minecraft']) || !isset($manifest['inhalte'])) {
|
||||||
|
return new WP_Error('aeromc_invalid', 'Ungültige Instanz-Beschreibung.', ['status' => 400]);
|
||||||
|
}
|
||||||
|
$json = wp_json_encode($manifest);
|
||||||
|
if (!$json || strlen($json) > AEROMC_FRIENDS_SHARE_MAX_BYTES) {
|
||||||
|
return new WP_Error('aeromc_too_big', 'Die Instanz ist zu groß zum Teilen.', ['status' => 413]);
|
||||||
|
}
|
||||||
|
$table = aeromc_friends_shares_table();
|
||||||
|
|
||||||
|
$seit = gmdate('Y-m-d H:i:s', time() - 86400);
|
||||||
|
$heute = (int) $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT COUNT(*) FROM $table WHERE uuid = %s AND created_at >= %s", $uuid, $seit
|
||||||
|
));
|
||||||
|
if ($heute >= AEROMC_FRIENDS_SHARES_PER_DAY) {
|
||||||
|
return new WP_Error('aeromc_too_fast', 'Heute wurden schon sehr viele Instanzen geteilt – bitte morgen wieder.', ['status' => 429]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// abgelaufene Codes (aller Spieler) wegräumen
|
||||||
|
$grenze = gmdate('Y-m-d H:i:s', time() - AEROMC_FRIENDS_SHARE_DAYS * 86400);
|
||||||
|
$wpdb->query($wpdb->prepare("DELETE FROM $table WHERE created_at < %s", $grenze));
|
||||||
|
|
||||||
|
for ($versuch = 0; $versuch < 5; $versuch++) {
|
||||||
|
$code = aeromc_friends_new_share_code();
|
||||||
|
$ok = $wpdb->insert($table, [
|
||||||
|
'code' => $code,
|
||||||
|
'uuid' => $uuid,
|
||||||
|
'manifest' => $json,
|
||||||
|
'created_at' => current_time('mysql', true),
|
||||||
|
]);
|
||||||
|
if ($ok) {
|
||||||
|
return ['code' => $code, 'days' => AEROMC_FRIENDS_SHARE_DAYS];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new WP_Error('aeromc_error', 'Es konnte kein Code erzeugt werden.', ['status' => 500]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GET /share?code= - geteilte Instanz abrufen (ohne Anmeldung: wer den Code hat, darf sie sehen) */
|
||||||
|
function aeromc_friends_route_share_get($request) {
|
||||||
|
global $wpdb;
|
||||||
|
$code = aeromc_friends_normalize_share_code($request->get_param('code'));
|
||||||
|
if (!$code) {
|
||||||
|
return new WP_Error('aeromc_invalid', 'Ungültiger Code.', ['status' => 400]);
|
||||||
|
}
|
||||||
|
$table = aeromc_friends_shares_table();
|
||||||
|
$presence_table = aeromc_friends_presence_table();
|
||||||
|
$grenze = gmdate('Y-m-d H:i:s', time() - AEROMC_FRIENDS_SHARE_DAYS * 86400);
|
||||||
|
$row = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT s.code, s.uuid, s.manifest, s.created_at, p.username
|
||||||
|
FROM $table s LEFT JOIN $presence_table p ON p.uuid = s.uuid
|
||||||
|
WHERE s.code = %s AND s.created_at >= %s",
|
||||||
|
$code, $grenze
|
||||||
|
));
|
||||||
|
if (!$row) {
|
||||||
|
return new WP_Error('aeromc_not_found', 'Diesen Code gibt es nicht oder er ist abgelaufen.', ['status' => 404]);
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'code' => $row->code,
|
||||||
|
'fromUuid' => $row->uuid,
|
||||||
|
'fromUsername' => $row->username,
|
||||||
|
'createdAt' => $row->created_at,
|
||||||
|
'manifest' => json_decode($row->manifest, true),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Kurznachrichten zwischen Freunden
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** POST /friends/message {uuid, targetUuid, text} */
|
||||||
|
function aeromc_friends_route_message_send($request) {
|
||||||
|
global $wpdb;
|
||||||
|
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
||||||
|
$target = aeromc_friends_normalize_uuid($request->get_param('targetUuid'));
|
||||||
|
if (!$uuid || !$target) {
|
||||||
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid/targetUuid.', ['status' => 400]);
|
||||||
|
}
|
||||||
|
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||||||
|
if (is_wp_error($sitzung)) {
|
||||||
|
return $sitzung;
|
||||||
|
}
|
||||||
|
$text = trim(sanitize_text_field((string) $request->get_param('text')));
|
||||||
|
$bild = $request->get_param('image');
|
||||||
|
if ($text === '' && !$bild) {
|
||||||
|
return new WP_Error('aeromc_invalid', 'Leere Nachricht.', ['status' => 400]);
|
||||||
|
}
|
||||||
|
if (mb_strlen($text) > AEROMC_FRIENDS_MESSAGE_MAX_CHARS) {
|
||||||
|
$text = mb_substr($text, 0, AEROMC_FRIENDS_MESSAGE_MAX_CHARS);
|
||||||
|
}
|
||||||
|
if (!aeromc_friends_are_friends($uuid, $target)) {
|
||||||
|
return new WP_Error('aeromc_not_friends', 'Ihr seid nicht befreundet.', ['status' => 403]);
|
||||||
|
}
|
||||||
|
$table = aeromc_friends_messages_table();
|
||||||
|
|
||||||
|
$seit = gmdate('Y-m-d H:i:s', time() - 60);
|
||||||
|
$letzte_minute = (int) $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT COUNT(*) FROM $table WHERE from_uuid = %s AND created_at >= %s", $uuid, $seit
|
||||||
|
));
|
||||||
|
if ($letzte_minute >= AEROMC_FRIENDS_MESSAGES_PER_MINUTE) {
|
||||||
|
return new WP_Error('aeromc_too_fast', 'Zu viele Nachrichten – bitte kurz warten.', ['status' => 429]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// alte Nachrichten (aller Spieler) samt ihrer Bilder wegräumen
|
||||||
|
$grenze = gmdate('Y-m-d H:i:s', time() - AEROMC_FRIENDS_MESSAGE_DAYS * 86400);
|
||||||
|
$alte_bilder = $wpdb->get_col($wpdb->prepare(
|
||||||
|
"SELECT image_url FROM $table WHERE created_at < %s AND image_url IS NOT NULL", $grenze
|
||||||
|
));
|
||||||
|
foreach ($alte_bilder as $alt) {
|
||||||
|
aeromc_friends_delete_image($alt);
|
||||||
|
}
|
||||||
|
$wpdb->query($wpdb->prepare("DELETE FROM $table WHERE created_at < %s", $grenze));
|
||||||
|
|
||||||
|
$bild_url = null;
|
||||||
|
if ($bild) {
|
||||||
|
$bild_url = aeromc_friends_save_image($bild);
|
||||||
|
if (is_wp_error($bild_url)) {
|
||||||
|
return $bild_url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$wpdb->insert($table, [
|
||||||
|
'from_uuid' => $uuid,
|
||||||
|
'to_uuid' => $target,
|
||||||
|
'text' => $text,
|
||||||
|
'image_url' => $bild_url,
|
||||||
|
'created_at' => current_time('mysql', true),
|
||||||
|
]);
|
||||||
|
return ['ok' => true, 'id' => (int) $wpdb->insert_id];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GET /friends/messages?uuid=&with= - Unterhaltung mit einem Freund (älteste zuerst, höchstens 50) */
|
||||||
|
function aeromc_friends_route_messages($request) {
|
||||||
|
global $wpdb;
|
||||||
|
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
||||||
|
$other = aeromc_friends_normalize_uuid($request->get_param('with'));
|
||||||
|
if (!$uuid || !$other) {
|
||||||
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid/with.', ['status' => 400]);
|
||||||
|
}
|
||||||
|
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||||||
|
if (is_wp_error($sitzung)) {
|
||||||
|
return $sitzung;
|
||||||
|
}
|
||||||
|
$table = aeromc_friends_messages_table();
|
||||||
|
$rows = $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT id, from_uuid, text, image_url, created_at, read_at FROM $table
|
||||||
|
WHERE (from_uuid = %s AND to_uuid = %s) OR (from_uuid = %s AND to_uuid = %s)
|
||||||
|
ORDER BY id DESC LIMIT 50",
|
||||||
|
$uuid, $other, $other, $uuid
|
||||||
|
));
|
||||||
|
return array_reverse(array_map(function ($r) {
|
||||||
|
return [
|
||||||
|
'id' => (int) $r->id,
|
||||||
|
'fromUuid' => $r->from_uuid,
|
||||||
|
'text' => $r->text,
|
||||||
|
'imageUrl' => $r->image_url,
|
||||||
|
'createdAt' => $r->created_at,
|
||||||
|
'read' => $r->read_at !== null,
|
||||||
|
];
|
||||||
|
}, $rows));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GET /friends/messages/unread?uuid= - ungelesene Nachrichten an mich */
|
||||||
|
function aeromc_friends_route_messages_unread($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]);
|
||||||
|
}
|
||||||
|
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||||||
|
if (is_wp_error($sitzung)) {
|
||||||
|
return $sitzung;
|
||||||
|
}
|
||||||
|
$table = aeromc_friends_messages_table();
|
||||||
|
$presence_table = aeromc_friends_presence_table();
|
||||||
|
$rows = $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT m.id, m.from_uuid, p.username AS from_username, m.text, m.image_url, m.created_at
|
||||||
|
FROM $table m LEFT JOIN $presence_table p ON p.uuid = m.from_uuid
|
||||||
|
WHERE m.to_uuid = %s AND m.read_at IS NULL
|
||||||
|
ORDER BY m.id ASC LIMIT 100",
|
||||||
|
$uuid
|
||||||
|
));
|
||||||
|
return array_map(function ($r) {
|
||||||
|
return [
|
||||||
|
'id' => (int) $r->id,
|
||||||
|
'fromUuid' => $r->from_uuid,
|
||||||
|
'fromUsername' => $r->from_username,
|
||||||
|
'text' => $r->text,
|
||||||
|
'imageUrl' => $r->image_url,
|
||||||
|
'createdAt' => $r->created_at,
|
||||||
|
];
|
||||||
|
}, $rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST /friends/messages/read {uuid, fromUuid} - Nachrichten eines Freundes als gelesen markieren */
|
||||||
|
function aeromc_friends_route_messages_read($request) {
|
||||||
|
global $wpdb;
|
||||||
|
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
||||||
|
$from = aeromc_friends_normalize_uuid($request->get_param('fromUuid'));
|
||||||
|
if (!$uuid || !$from) {
|
||||||
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid/fromUuid.', ['status' => 400]);
|
||||||
|
}
|
||||||
|
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||||||
|
if (is_wp_error($sitzung)) {
|
||||||
|
return $sitzung;
|
||||||
|
}
|
||||||
|
$table = aeromc_friends_messages_table();
|
||||||
|
$wpdb->query($wpdb->prepare(
|
||||||
|
"UPDATE $table SET read_at = %s WHERE to_uuid = %s AND from_uuid = %s AND read_at IS NULL",
|
||||||
|
current_time('mysql', true), $uuid, $from
|
||||||
|
));
|
||||||
|
return ['ok' => true];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Fehlerberichte aus dem Absturz-Assistenten
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Kurzer Text: Steuerzeichen raus, auf Länge kürzen. */
|
||||||
|
function aeromc_friends_kurz($wert, $max) {
|
||||||
|
$text = trim(preg_replace('/[\x00-\x1F\x7F]+/u', ' ', (string) $wert));
|
||||||
|
return mb_substr($text, 0, $max);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /report {launcherVersion, mcVersion, loader, loaderVersion, system, funde[], mods[], bericht, kommentar, uuid?}
|
||||||
|
* Ohne Anmeldung möglich (der Nutzer kann anonym senden); mit uuid wird die
|
||||||
|
* Anmeldung geprüft, damit niemand Berichte unter fremdem Namen einreicht.
|
||||||
|
*/
|
||||||
|
function aeromc_friends_route_report($request) {
|
||||||
|
global $wpdb;
|
||||||
|
// Schutz gegen Fluten: pro Absender-Adresse begrenzt
|
||||||
|
$ip = isset($_SERVER['REMOTE_ADDR']) ? (string) $_SERVER['REMOTE_ADDR'] : '';
|
||||||
|
$schluessel = 'aeromc_rep_' . md5($ip);
|
||||||
|
$anzahl = (int) get_transient($schluessel);
|
||||||
|
if ($anzahl >= AEROMC_FRIENDS_REPORTS_PER_HOUR) {
|
||||||
|
return new WP_Error('aeromc_too_fast', 'Zu viele Berichte in kurzer Zeit – bitte später noch einmal.', ['status' => 429]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$uuid = null;
|
||||||
|
$username = null;
|
||||||
|
if ($request->get_param('uuid')) {
|
||||||
|
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
||||||
|
if (!$uuid) {
|
||||||
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid.', ['status' => 400]);
|
||||||
|
}
|
||||||
|
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||||||
|
if (is_wp_error($sitzung)) {
|
||||||
|
return $sitzung;
|
||||||
|
}
|
||||||
|
$username = $sitzung ? $sitzung->username : aeromc_friends_kurz($request->get_param('username'), 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
$bericht = (string) $request->get_param('bericht');
|
||||||
|
if (strlen($bericht) > AEROMC_FRIENDS_REPORT_MAX_BYTES) {
|
||||||
|
$bericht = substr($bericht, -AEROMC_FRIENDS_REPORT_MAX_BYTES);
|
||||||
|
}
|
||||||
|
$funde = $request->get_param('funde');
|
||||||
|
$mods = $request->get_param('mods');
|
||||||
|
|
||||||
|
$wpdb->insert(aeromc_friends_reports_table(), [
|
||||||
|
'created_at' => current_time('mysql', true),
|
||||||
|
'uuid' => $uuid,
|
||||||
|
'username' => $username ?: null,
|
||||||
|
'launcher_version' => aeromc_friends_kurz($request->get_param('launcherVersion'), 32),
|
||||||
|
'mc_version' => aeromc_friends_kurz($request->get_param('mcVersion'), 32),
|
||||||
|
'loader' => aeromc_friends_kurz($request->get_param('loader'), 32),
|
||||||
|
'loader_version' => aeromc_friends_kurz($request->get_param('loaderVersion'), 64),
|
||||||
|
'system' => aeromc_friends_kurz($request->get_param('system'), 128),
|
||||||
|
'funde' => wp_json_encode(is_array($funde) ? array_slice($funde, 0, 20) : []),
|
||||||
|
'mods' => wp_json_encode(is_array($mods) ? array_slice($mods, 0, 500) : []),
|
||||||
|
'bericht' => $bericht,
|
||||||
|
'kommentar' => mb_substr(sanitize_textarea_field((string) $request->get_param('kommentar')), 0, 2000),
|
||||||
|
'status' => 'neu',
|
||||||
|
]);
|
||||||
|
set_transient($schluessel, $anzahl + 1, HOUR_IN_SECONDS);
|
||||||
|
return ['ok' => true, 'id' => (int) $wpdb->insert_id];
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user