Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5ccf0b4cc | ||
|
|
10ee45cbc2 | ||
|
|
bc68395437 | ||
|
|
41c63ef0ed | ||
|
|
0595b7ab1a | ||
|
|
cbe7a6d81d | ||
|
|
75be217546 | ||
|
|
78246284d6 | ||
|
|
1d432d08ae | ||
|
|
47b8b88f6e | ||
|
|
906ac75443 | ||
|
|
d268e548f5 | ||
|
|
ee0e6dec32 | ||
|
|
3b54217989 | ||
|
|
425b94fd93 | ||
|
|
e0bdcb01fb | ||
|
|
c840a49f75 | ||
|
|
c535d8b830 | ||
|
|
248a387f01 | ||
|
|
87fded4cb6 | ||
|
|
33e533009e | ||
|
|
17f8726075 | ||
|
|
deec93e5fa | ||
|
|
fc57dfa186 | ||
|
|
bafad25059 | ||
|
|
ec4992c5c7 | ||
|
|
3e28b50f1b | ||
|
|
54d248133c | ||
|
|
129ab2d939 | ||
|
|
b250ef16fd | ||
|
|
9e601b1962 | ||
|
|
176d20ae7b | ||
|
|
eff6403dd5 | ||
|
|
a803f297ea | ||
|
|
dc55e64ca3 | ||
|
|
64b9c460af |
@@ -0,0 +1,8 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# beim Bauen erzeugt, enthält Adresse und Token der CurseForge-Weiterleitung
|
||||||
|
src/build-config.json
|
||||||
|
|
||||||
|
# Zugangsdaten des Weiterleitungs-Servers
|
||||||
|
server/curseforge-proxy/.env
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
Alle nennenswerten Änderungen an AeroMC werden hier festgehalten.
|
||||||
|
|
||||||
|
## [1.1.6] - 2026-10-02
|
||||||
|
|
||||||
|
### Neu
|
||||||
|
- Skin-Editor: Skins direkt im Launcher pixeln – erreichbar im Skin-Fenster über „Im Editor bearbeiten…“
|
||||||
|
- Zwei Schichten: Grundschicht und 2. Schicht getrennt bemalen – die 2. Schicht liegt wie eine Hülle über der Figur und lässt Haare, Kapuzen oder Ärmel plastisch wirken
|
||||||
|
- Wahlweise auf der aufgeklappten 2D-Vorlage oder direkt auf der drehbaren 3D-Figur malen – die andere Ansicht läuft als Live-Vorschau mit und ist ebenfalls bemalbar
|
||||||
|
- Werkzeuge: Stift, Radierer (nur 2. Schicht), Pipette (auch Alt+Klick), Füllen, Farbwähler mit den zuletzt benutzten Farben
|
||||||
|
- Rückgängig/Wiederholen mit Strg+Z und Strg+Y
|
||||||
|
- Spiegeln: malt gleichzeitig auf der anderen Körperseite (linker ↔ rechter Arm/Bein)
|
||||||
|
- Ebenen-Werkzeuge: 2. Schicht ein-/ausblenden, Grundschicht durchscheinen lassen, 2. Schicht leeren
|
||||||
|
- Aktuellen Skin laden, PNG öffnen oder speichern und das Ergebnis mit einem Klick als Skin übernehmen – klassische und schmale Arme, alte 64×32-Skins werden automatisch umgewandelt
|
||||||
|
- Skin-Hintergrund: Armform wählbar (Optionen → Darstellung) – „Automatisch“ übernimmt die Armform des Kontos, alternativ fest „Normal“ oder „Schmal“
|
||||||
|
- Designs zur Auswahl (Optionen → Darstellung → Design, mit Vorschau-Kacheln): „Klassisch“ hell oder dunkel wie bisher, „AeroMC“ im Look der Webseite (Nachtblau, Cyan-Verläufe, runde Knöpfe), „Nachtlila“ (wie AeroMC, nur in dunklem Lila) und „Minecraft“ mit Block-Optik, Stein-Knöpfen, grünem Starten-Knopf und eigener Pixel-Schrift
|
||||||
|
- Ein gewähltes Design gilt sofort in allen offenen Fenstern
|
||||||
|
- Eigene Titelleiste im Design statt der weißen Windows-Leiste – Minimieren, Maximieren und Schließen bleiben echte Windows-Knöpfe (Andocken, Fensterlayouts) und werden passend eingefärbt
|
||||||
|
- Import aus der Modrinth App (Importieren → „Modrinth App“, auch im Installer): übernimmt Name, Gruppe, Symbol, Minecraft-Version, Loader, Arbeitsspeicher, Java-Einstellungen, Spielzeit sowie Mods, Welten und Einstellungen – auch bei verlegtem Datenordner und während die Modrinth App läuft
|
||||||
|
- Aus der Modrinth App übernommene Modrinth-Modpacks bleiben mit ihrem Paket verknüpft und lassen sich in AeroMC aktualisieren
|
||||||
|
|
||||||
|
### Behoben
|
||||||
|
- Das Symbol für „Optionen“ sah aus wie eine Sonne – jetzt ein Zahnrad
|
||||||
|
- Der Skin-Hintergrund zeigte schmale (Alex-)Skins mit breiten Armen – dadurch erschienen falsche Streifen an den Armen
|
||||||
|
|
||||||
|
## [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
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
AeroMC – Lizenz- und Nutzungsbedingungen
|
||||||
|
|
||||||
|
Copyright (c) M_Viper. Alle Rechte vorbehalten.
|
||||||
|
|
||||||
|
Diese Software („AeroMC") sowie sämtlicher zugehöriger Quellcode, Ressourcen und
|
||||||
|
Dokumentation sind urheberrechtlich geschützt. Mit der Nutzung dieser Software
|
||||||
|
erkennst du die folgenden Bedingungen an.
|
||||||
|
|
||||||
|
1. Erlaubte Nutzung
|
||||||
|
Die Nutzung von AeroMC ist ausschließlich für den privaten, nicht-kommerziellen
|
||||||
|
Gebrauch gestattet.
|
||||||
|
|
||||||
|
2. Untersagt ist insbesondere
|
||||||
|
- Die Weitergabe dieses Launchers oder von Teilen davon an Dritte
|
||||||
|
- Das Verändern, Dekompilieren, Zurückentwickeln (Reverse Engineering) oder
|
||||||
|
Bearbeiten des Codes
|
||||||
|
- Die Veröffentlichung – auch in veränderter oder abgeleiteter Form –
|
||||||
|
an anderer Stelle als den offiziellen Kanälen des Autors
|
||||||
|
- Der Verkauf sowie jede sonstige kommerzielle Nutzung
|
||||||
|
|
||||||
|
3. Keine Gewährleistung
|
||||||
|
Die Software wird ohne jegliche Gewährleistung bereitgestellt, weder
|
||||||
|
ausdrücklich noch stillschweigend, einschließlich, aber nicht beschränkt auf
|
||||||
|
die Gewährleistung der Marktgängigkeit, der Eignung für einen bestimmten
|
||||||
|
Zweck und der Nichtverletzung von Rechten Dritter. Der Autor haftet nicht für
|
||||||
|
Schäden, die aus der Nutzung oder Unmöglichkeit der Nutzung der Software
|
||||||
|
entstehen.
|
||||||
|
|
||||||
|
4. Rechtsfolgen bei Verstoß
|
||||||
|
Wer gegen diese Bedingungen verstößt, macht sich strafbar und kann rechtlich
|
||||||
|
belangt werden. Alle Rechte, die hier nicht ausdrücklich eingeräumt werden,
|
||||||
|
bleiben beim Autor vorbehalten.
|
||||||
|
|
||||||
|
5. Markenhinweis
|
||||||
|
AeroMC ist kein offizielles Produkt von Mojang oder Microsoft. Minecraft ist
|
||||||
|
eine Marke von Mojang AB.
|
||||||
|
|
||||||
|
Kontakt: https://m-viper.de
|
||||||
@@ -1,86 +1,391 @@
|
|||||||
<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.0 Beta
|
Aktueller Stand: Version 1.1.6
|
||||||
|
|
||||||
</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
|
|
||||||
- Modrinth-Integration mit einfacher Mod-Installation inklusive Pflicht-Abhängigkeiten
|
**Instanzen**
|
||||||
- Microsoft-Login mit mehreren Konten
|
- Beliebig viele getrennte Minecraft-Instanzen
|
||||||
- Import von Modrinth-Modpacks im .mrpack-Format
|
- Unterstützung für Vanilla, Fabric, Quilt, Forge, NeoForge und OptiFine
|
||||||
- Import vorhandener MultiMC-Instanzen
|
- Versionsliste mit Filter: Releases, Snapshots oder alle
|
||||||
- Export, Kopieren, Gruppieren und Verwalten von Instanzen
|
- Performance-Paket beim Anlegen: Sodium, Lithium, FerriteCore, ImmediatelyFast, Entity Culling und ModernFix per Häkchen
|
||||||
- Verwaltung von Welten, Screenshots und Spielordnern direkt aus dem Launcher
|
- Export, Kopieren, Gruppieren, Löschen und Desktop-Verknüpfung pro Instanz
|
||||||
- Bedrock-Bereich für Start und Verwaltung vorhandener Bedrock-Daten unter Windows
|
- Eigene Icons pro Instanz – vorgefertigte Symbole, aus Minecraft extrahierte Block-/Item-Icons oder ein eigenes Bild – plus Notizen
|
||||||
- Update-Funktion für neue Launcher-Versionen
|
- Mehrfachauswahl mit Strg-Klick, Tastenkürzel für die wichtigsten Aktionen
|
||||||
|
- Schnellstart-Tasten: Lieblings-Instanzen auf **Strg+1** bis **Strg+5** legen
|
||||||
## Installation
|
- Instanzen per Rechtsklick auf das Symbol neben der Uhr starten, ohne das Fenster zu öffnen
|
||||||
|
- **Instanz teilen per Code**: Freunde bekommen Version, Loader und alle Mods in denselben Versionen
|
||||||
Es gibt zwei Varianten:
|
- Spielzeit-Statistik: Gesamtspielzeit, Spielstarts und Spielzeit pro Instanz
|
||||||
|
|
||||||
- AeroMC-Setup.exe installiert den Launcher und sorgt im Alltag für den schnelleren Start
|
**Mods, Modpacks und Inhalte**
|
||||||
- AeroMC-Portable.exe startet ohne Installation direkt als portable Variante
|
- Modrinth- **und CurseForge**-Integration für Mods, Ressourcenpakete, Shader und Datenpakete (Installation inkl. rekursiver Pflicht-Abhängigkeiten)
|
||||||
|
- Update-Prüfung samt Sammel-Update, auf Wunsch automatisch vor jedem Spielstart
|
||||||
Für die meisten Nutzer ist die Setup-Version die bessere Wahl.
|
- Modpacks direkt von Modrinth, CurseForge oder der Technic-Plattform mit einem Klick installieren
|
||||||
|
- Mods als eigene Datei hinzufügen oder per Drag & Drop aufs Fenster ziehen
|
||||||
Die portable Datei ist praktisch zum Mitnehmen, braucht beim Öffnen aber in der Regel etwas länger.
|
- Warnung beim Entfernen eines Mods, der noch als Pflicht-Abhängigkeit gebraucht wird
|
||||||
|
|
||||||
## Voraussetzungen
|
**Import**
|
||||||
|
- Gemeinsamer Import für AeroMC-ZIP, Modrinth-Modpacks (`.mrpack`), Minecraft Launcher, CurseForge, **Modrinth App**, MultiMC/Prism sowie Technic
|
||||||
- Windows x64
|
- Schon bei der Installation: der Installer erkennt andere Launcher und AeroMC bietet deren Instanzen beim ersten Start zum Übernehmen an
|
||||||
- Eine legale Minecraft Java Edition mit Microsoft-Konto für Java-Start und Anmeldung
|
- Von Freunden geteilte Instanzen per Code einlösen
|
||||||
- Internetverbindung beim ersten Download von Minecraft-Dateien, Loadern oder Mods
|
|
||||||
- Für Bedrock-Funktionen: installierte Minecraft Bedrock Edition aus dem Microsoft Store
|
**Spielaufzeichnung**
|
||||||
|
- Nimmt nur das Minecraft-Fenster auf – ohne Desktop-Ränder und ohne Mauszeiger – und speichert als MP4
|
||||||
## Erste Schritte
|
- Auflösung, Qualität und Bildrate einstellbar, mehrere Tonspuren (Systemton, nur Minecraft, Mikrofone) mit eigener Lautstärke
|
||||||
|
- Clip-Modus: läuft im Hintergrund mit, **Alt+F10** speichert die letzten 30, 60 oder 90 Sekunden
|
||||||
1. AeroMC installieren oder die portable Datei starten.
|
- Servername im Aufnahmetitel, Aufzeichnungen-Galerie mit Vorschaubildern
|
||||||
2. Über Konten ein Microsoft-Konto hinzufügen.
|
|
||||||
3. Eine neue Instanz anlegen.
|
**Freunde**
|
||||||
4. Minecraft-Version und bei Bedarf einen Loader auswählen.
|
- Freunde per Minecraft-Name hinzufügen, mit 3D-Spielerkopf, Online-Status und Spitznamen
|
||||||
5. Optional Mods oder ein Modpack hinzufügen.
|
- Meldung im Spiel, sobald ein Freund online kommt – bei mehreren Konten im jeweils richtigen Fenster
|
||||||
6. Instanz starten.
|
- **Beitreten**: startet eine passende Instanz und verbindet direkt mit dem Server eines Freundes
|
||||||
|
- **Einladen**: schickt Freunden deinen aktuellen Server – als Hinweis im Launcher und als Meldung im Spiel
|
||||||
## Was AeroMC verwaltet
|
- **Kurznachrichten** mit Schnellantworten wie „Bin in 10 Min. online“, auch als Meldung im Spiel
|
||||||
|
- **Screenshots** direkt aus der Galerie an Freunde schicken
|
||||||
- Minecraft-Versionen und gemeinsame Spieldateien
|
- Anmeldung über Mojangs Sitzungsprüfung – niemand kann sich als ein anderer Spieler ausgeben
|
||||||
- Instanzbezogene Mods und Konfigurationen
|
- Eigener Server auf Wunsch verborgen, Freundesystem komplett abschaltbar
|
||||||
- Welten und Screenshots
|
|
||||||
- Mehrere Microsoft-Konten
|
**Konten, Welten und Backups**
|
||||||
- Java-Auswahl pro Instanz
|
- Microsoft-Login mit mehreren Konten
|
||||||
|
- Eigenen Skin im Launcher hochladen, mit Live-Vorschau anpassen (klassisch/schlank) und zurücksetzen
|
||||||
Alle Nutzerdaten werden lokal unter %APPDATA%\AeroMC gespeichert.
|
- **Skin-Editor mit zwei Schichten**: Skins selbst pixeln – auf der Vorlage oder direkt auf der 3D-Figur, mit Spiegeln, Füllen, Pipette und Rückgängig
|
||||||
|
- Skin-Hintergrund des aktiven Kontos (mehrere Posen, passende Armform, einstellbare Deckkraft, optional wechselnd)
|
||||||
## Bedrock
|
- Welten, Ressourcenpakete, Datenpakete, Screenshots, Serverliste und Logdateien verwalten
|
||||||
|
- Serverliste mit Live-Status und Desktop-Verknüpfung pro Server (startet die Instanz und verbindet direkt)
|
||||||
Der Bedrock-Bereich ist für bereits installierte Bedrock-Versionen unter Windows gedacht. AeroMC kann Bedrock nicht selbst installieren, aber vorhandene Welten, Pakete, Add-ons und Ordner leichter zugänglich machen.
|
- Welten mit einem Klick in eine andere Instanz kopieren
|
||||||
|
- Globales Backup inkl. automatischer Zeitplan-Sicherungen, dazu Schnellsicherung einzelner Welten
|
||||||
## Links
|
- Automatische Welt-Sicherung nach jeder Spielsitzung (die letzten 5 Stände je Welt)
|
||||||
|
|
||||||
- Webseite: https://M_Viper.de
|
**Technik und Komfort**
|
||||||
- Gitseite: https://git.viper.ipv64.net
|
- Java pro Instanz oder global, inkl. automatischem Download fehlender Versionen (Eclipse Temurin)
|
||||||
- Autor: M_Viper
|
- 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
|
||||||
## Rechtliches
|
- Eingebaute Konsole/Log-Ansicht inkl. Absturzberichte pro Instanz
|
||||||
|
- Bedrock-Bereich für eine vorhandene Bedrock-Installation, inkl. Add-on-Installation
|
||||||
AeroMC ist kein offizielles Produkt von Mojang oder Microsoft.
|
- Update-Suche sofort beim Start, Updates installieren sich ohne Rückfragen
|
||||||
Minecraft ist eine Marke von Mojang AB.
|
- Optionaler Autostart mit Windows (im Infobereich statt als Fenster)
|
||||||
|
- Rechtsklick-Menü in allen Textfeldern (Ausschneiden, Kopieren, Einfügen)
|
||||||
## Lizenz
|
- **Fünf Designs**: Klassisch hell oder dunkel, „AeroMC“ im Look der Webseite, „Nachtlila“ (dasselbe in dunklem Lila) und „Minecraft“ mit Block-Optik und Pixel-Schrift – samt passender Titelleiste
|
||||||
|
- Optionaler HTTP-Proxy und Wrapper-Befehl
|
||||||
MIT
|
|
||||||
|
## Designs
|
||||||
|
|
||||||
|
Unter **Optionen → Darstellung → Design** stehen fünf Designs zur Wahl. Ein Klick auf eine Vorschau-Kachel zeigt es sofort an, mit **Speichern** gilt es in allen Fenstern – inklusive Titelleiste und Fensterknöpfen.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><img src="docs/design-aeromc.png" alt="AeroMC-Design: Nachtblau mit Cyan-Verläufen wie die Webseite" width="270" /><br /><b>AeroMC</b><br /><sub>Nachtblau, Cyan-Verläufe, runde Knöpfe – wie die Webseite</sub></td>
|
||||||
|
<td align="center"><img src="docs/design-nachtlila.png" alt="Nachtlila-Design: wie AeroMC in dunklem Lila" width="270" /><br /><b>Nachtlila</b><br /><sub>wie AeroMC, nur in dunklem Lila</sub></td>
|
||||||
|
<td align="center"><img src="docs/design-minecraft.png" alt="Minecraft-Design: Block-Optik mit Grasnarbe, Erde und Stein-Knöpfen" width="270" /><br /><b>Minecraft</b><br /><sub>Grasnarbe, Erde, Stein-Knöpfe und Pixel-Schrift</sub></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
Dazu **Klassisch Dunkel** (Bild ganz oben) und **Klassisch Hell** im bewährten Launcher-Look.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Es gibt zwei Varianten:
|
||||||
|
|
||||||
|
- **AeroMC-Setup.exe** – installiert den Launcher, schneller Start im Alltag
|
||||||
|
- **AeroMC-Portable.exe** – startet ohne Installation, ideal zum Mitnehmen (etwas längerer Start)
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Deinstallieren über **Windows-Einstellungen → Apps → AeroMC**. Instanzen und Welten bleiben dabei standardmäßig erhalten und lassen sich auf Wunsch mit entfernen (Papierkorb).
|
||||||
|
|
||||||
|
Automatische Updates bleiben getrennt: Setup erhält nur Setup-Updates, Portable nur Portable-Updates. Ein automatischer Wechsel zwischen beiden Varianten findet nicht statt.
|
||||||
|
|
||||||
|
## Voraussetzungen
|
||||||
|
|
||||||
|
- Windows 10 oder 11 (x64)
|
||||||
|
- Legale Minecraft Java Edition mit Microsoft-Konto
|
||||||
|
- Internetverbindung beim ersten Download von Minecraft-Dateien, Loadern oder Inhalten
|
||||||
|
- Für Bedrock: installierte Minecraft Bedrock Edition aus dem Microsoft Store
|
||||||
|
|
||||||
|
## Erste Schritte
|
||||||
|
|
||||||
|
1. AeroMC installieren oder die portable Datei starten.
|
||||||
|
2. Über den Konto-Knopf oben rechts (oder **Optionen → 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.
|
||||||
|
4. Optional Mods/Ressourcenpakete/Shader hinzufügen oder bestehende Setups über **Importieren** übernehmen.
|
||||||
|
5. Instanz starten (online oder offline).
|
||||||
|
|
||||||
|
## Instanzen
|
||||||
|
|
||||||
|
Jede Instanz hat:
|
||||||
|
|
||||||
|
- Namen und optionale Gruppe
|
||||||
|
- Minecraft-Version und optionalen Mod-Loader
|
||||||
|
- optionales Icon (vorgegeben, aus Minecraft extrahiert oder eigenes Bild) und Notizen
|
||||||
|
- eigene Mods, Ressourcenpakete, Shader, Welten, Datenpakete, Screenshots, Aufzeichnungen und Serverliste
|
||||||
|
- eigene Java-, RAM-, JVM-, Pre-Launch- und Post-Exit-Einstellungen (leer = globale Vorgabe)
|
||||||
|
|
||||||
|
Über die Seitenleiste einer ausgewählten Instanz erreichst du u. a.:
|
||||||
|
|
||||||
|
- Starten / Offline starten
|
||||||
|
- Instanz und Notizen bearbeiten
|
||||||
|
- Mods, Ressourcenpakete und Shader
|
||||||
|
- Welten (inkl. Schnellsicherung und „Kopieren nach …“ in eine andere Instanz), Datenpakete, Bildschirmfotos und Aufzeichnungen
|
||||||
|
- Spiel aufzeichnen
|
||||||
|
- Server verwalten (Live-Status, Übernahme eines Eintrags in andere Instanzen, Desktop-Verknüpfung pro Server)
|
||||||
|
- Logdateien und Absturzberichte ansehen
|
||||||
|
- Minecraft-, Konfigurations- und Instanz-Ordner öffnen
|
||||||
|
- Desktop-Verknüpfung erstellen
|
||||||
|
- Exportieren, **Instanz teilen** (Code für Freunde), Kopieren, Gruppe ändern, Löschen
|
||||||
|
- Bei importierten Modpacks: Modpack aktualisieren
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
### Tastenkürzel
|
||||||
|
|
||||||
|
Im Launcher:
|
||||||
|
|
||||||
|
| Taste | Wirkung |
|
||||||
|
|---|---|
|
||||||
|
| Strg+F | Suche fokussieren |
|
||||||
|
| Strg+N | Neue Instanz anlegen |
|
||||||
|
| Strg+1 … Strg+5 | Schnellstart-Instanz starten |
|
||||||
|
| Entf | Markierte Instanz(en) löschen |
|
||||||
|
| Esc | Aktuellen Dialog schließen |
|
||||||
|
|
||||||
|
Greifen nicht, während ein Textfeld fokussiert ist oder ein Dialog mit eigener Suche offen ist.
|
||||||
|
|
||||||
|
Im Spiel (solange ein über AeroMC gestartetes Minecraft läuft):
|
||||||
|
|
||||||
|
| Taste | Wirkung |
|
||||||
|
|---|---|
|
||||||
|
| Strg+Alt+F | Zeigt, welche Freunde gerade online sind |
|
||||||
|
| Strg+Alt+J | Kopiert die Server-Adresse der letzten Einladung |
|
||||||
|
| Alt+F10 | Speichert im Clip-Modus die letzten Sekunden (ersatzweise Strg+Alt+C) |
|
||||||
|
|
||||||
|
## Import
|
||||||
|
|
||||||
|
Über **Importieren**:
|
||||||
|
|
||||||
|
| Quelle | Typische Nutzung |
|
||||||
|
|---|---|
|
||||||
|
| ZIP-Archiv | AeroMC-Export (`instance.json`) sowie einzelne MultiMC/Prism-Instanzen (auch gezippt) |
|
||||||
|
| Modpack | Modrinth- oder CurseForge-Katalog direkt durchsuchen und installieren, oder Datei importieren: `.mrpack` sowie CurseForge-ZIP (`manifest.json`) |
|
||||||
|
| Minecraft Launcher | Profile aus dem offiziellen Launcher |
|
||||||
|
| CurseForge | Instanzen aus dem CurseForge-Ordner |
|
||||||
|
| Modrinth App | Instanzen aus der Modrinth App (`%APPDATA%\ModrinthApp`) inkl. Einstellungen, Spielzeit und Modpack-Verknüpfung |
|
||||||
|
| MultiMC / Prism | Vorhandene MultiMC- oder Prism-Instanzen |
|
||||||
|
| Technic | Technic-Katalog direkt durchsuchen und installieren, oder eine vorhandene Technic-Installation importieren (`%APPDATA%\.technic\modpacks`) |
|
||||||
|
| Per Code | Von einem Freund geteilte Instanz einlösen (siehe **Instanzen → Instanz teilen**) |
|
||||||
|
|
||||||
|
Existiert bereits eine Instanz mit demselben Namen, wird sie ersetzt.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
**Modrinth App:** Übernommen werden Name, Gruppe, Symbol, Minecraft-Version, Loader, Arbeitsspeicher, Java-Einstellungen, Spielzeit sowie Mods, Welten und Einstellungen. Modrinth-Modpacks bleiben mit ihrem Paket verknüpft und lassen sich weiter aktualisieren. Ein in der App verlegter Datenordner wird automatisch gefunden, und die Modrinth App darf dabei geöffnet bleiben – AeroMC liest auch ihr Schreibprotokoll mit. An den Daten der Modrinth App wird nichts verändert.
|
||||||
|
|
||||||
|
**Schon bei der Installation:** Der Installer sucht nach Minecraft Launcher, CurseForge, Modrinth App, 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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Mods, Ressourcenpakete und Shader
|
||||||
|
|
||||||
|
Im Inhalts-Dialog einer Instanz:
|
||||||
|
|
||||||
|
- Katalog durchsuchen (Quelle Modrinth oder CurseForge, Sortierung, Kategorien)
|
||||||
|
- 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
|
||||||
|
- Installierte Einträge aktivieren, deaktivieren, entfernen – mit Warnung, falls ein anderer Mod sie noch braucht
|
||||||
|
- Nach Updates suchen und alle aktualisieren
|
||||||
|
- Ordner der jeweiligen Inhaltsart öffnen
|
||||||
|
|
||||||
|
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**).
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
## Spielaufzeichnung
|
||||||
|
|
||||||
|
Über **Spiel aufzeichnen** in der Seitenleiste:
|
||||||
|
|
||||||
|
- Aufgenommen wird nur das Minecraft-Fenster – ohne Desktop-Ränder und ohne Mauszeiger
|
||||||
|
- 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
|
||||||
|
- 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**
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
## Freunde
|
||||||
|
|
||||||
|
Über **Freunde** in der oberen Leiste:
|
||||||
|
|
||||||
|
- 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
|
||||||
|
- 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
|
||||||
|
- **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“
|
||||||
|
- **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.
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Das Freundesystem funktioniert nur zwischen Spielern, die AeroMC nutzen und mindestens einmal mit eingeschaltetem Freundesystem darüber gespielt haben.
|
||||||
|
|
||||||
|
## Skin ändern
|
||||||
|
|
||||||
|
Über den **eigenen Kontonamen oben rechts → Skin ändern…** lässt sich der Skin des aktiven Microsoft-Kontos direkt im Launcher ändern (der Knopf **Skin** in der Werkzeugleiste schaltet dagegen nur den Skin-Hintergrund ein/aus):
|
||||||
|
|
||||||
|
- PNG-Datei auswählen, sofortige Prüfung auf gültige Maße
|
||||||
|
- Live-Vorschau am 3D-Modell vor dem Anwenden
|
||||||
|
- Modell wählen: klassisch oder schlank (slim)
|
||||||
|
- Auf den Standard-Skin (Steve/Alex) zurücksetzen – einen früheren eigenen Skin holt das nicht zurück; dafür vorher im Skin-Editor „Als PNG speichern…“ nutzen
|
||||||
|
|
||||||
|
Die Änderung wird direkt bei Mojang für das angemeldete Konto hinterlegt und gilt somit auch außerhalb von AeroMC.
|
||||||
|
|
||||||
|
### Skin-Editor
|
||||||
|
|
||||||
|
**Im Editor bearbeiten…** öffnet einen eigenen Pixel-Editor – mit der gewählten PNG-Datei oder dem aktuellen Skin des Kontos:
|
||||||
|
|
||||||
|
- **Zwei Schichten:** Grundschicht und 2. Schicht (die Hülle über der Figur für Haare, Jacken, Ärmel …) getrennt bemalen; 2. Schicht ein-/ausblenden, Grundschicht darunter durchscheinen lassen, 2. Schicht leeren
|
||||||
|
- **2D oder 3D:** auf der aufgeklappten Vorlage oder direkt auf der drehbaren Figur malen – die andere Ansicht läuft als Live-Vorschau mit und ist ebenfalls bemalbar
|
||||||
|
- **Werkzeuge:** Stift, Radierer (nur 2. Schicht), Pipette (auch Alt+Klick), Füllen, Farbwähler mit zuletzt benutzten Farben
|
||||||
|
- **Spiegeln:** malt gleichzeitig auf der anderen Körperseite
|
||||||
|
- Rückgängig/Wiederholen, klassische oder schmale Arme, alte 64×32-Skins werden automatisch umgewandelt
|
||||||
|
- Neu beginnen, aktuellen Skin laden oder eine PNG-Datei öffnen
|
||||||
|
- **Als Skin übernehmen** (direkt hochladen) oder **Als PNG speichern…**
|
||||||
|
|
||||||
|
| Taste | Wirkung |
|
||||||
|
|---|---|
|
||||||
|
| B / E / I / G | Stift / Radierer / Pipette / Füllen |
|
||||||
|
| M | Spiegeln ein/aus |
|
||||||
|
| L | Schicht wechseln |
|
||||||
|
| Strg+Z / Strg+Y | Rückgängig / Wiederholen |
|
||||||
|
|
||||||
|
## 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
|
||||||
|
- 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
|
||||||
|
- Design: Klassisch Dunkel, Klassisch Hell, AeroMC, Nachtlila oder Minecraft – mit Vorschau-Kacheln, gilt nach dem Speichern in allen Fenstern
|
||||||
|
- Block-Symbole räumlich (3D)
|
||||||
|
- Skin-Hintergrund: Quelle, Pose, Armform (automatisch vom Konto, normal oder schmal), 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.
|
||||||
|
|||||||
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 169 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,194 @@
|
|||||||
|
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, mr, 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(DetectModrinthApp);
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Modrinth App ----------
|
||||||
|
|
||||||
|
private static FoundLauncher? DetectModrinthApp()
|
||||||
|
{
|
||||||
|
// neuere Fassungen: ModrinthApp, sehr alte: com.modrinth.theseus (siehe src/mrimport.js)
|
||||||
|
var root = new[]
|
||||||
|
{
|
||||||
|
System.IO.Path.Combine(AppData, "ModrinthApp"),
|
||||||
|
System.IO.Path.Combine(AppData, "com.modrinth.theseus"),
|
||||||
|
}.FirstOrDefault(d => Directory.Exists(System.IO.Path.Combine(d, "profiles")));
|
||||||
|
if (root is null) return null;
|
||||||
|
|
||||||
|
string[] markers = ["profile.json", "mods", "config", "resourcepacks", "shaderpacks", "saves", "options.txt"];
|
||||||
|
int count = Directory.GetDirectories(System.IO.Path.Combine(root, "profiles")).Count(d => markers.Any(m => Exists(d, m)));
|
||||||
|
return new FoundLauncher("mr", "Modrinth App", root, 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.6",
|
||||||
|
"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")]
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<startup>
|
||||||
|
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
|
||||||
|
</startup>
|
||||||
|
</configuration>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
AeroMC-Setup
|
||||||
|
0.0.0.0
|
||||||
|
|
||||||
|
winexe
|
||||||
|
C#
|
||||||
|
.cs
|
||||||
|
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\obj\Debug\net48\
|
||||||
|
AeroMCSetup
|
||||||
|
none
|
||||||
|
false
|
||||||
|
TRACE;DEBUG;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
|
||||||
|
|
||||||
|
919373854
|
||||||
|
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("Debug")]
|
||||||
|
[assembly: System.Reflection.AssemblyCopyrightAttribute("Copyright (c) M_Viper. Alle Rechte vorbehalten.")]
|
||||||
|
[assembly: System.Reflection.AssemblyDescriptionAttribute("Installiert AeroMC")]
|
||||||
|
[assembly: System.Reflection.AssemblyFileVersionAttribute("0.0.0.0")]
|
||||||
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("0.0.0")]
|
||||||
|
[assembly: System.Reflection.AssemblyProductAttribute("AeroMC Setup")]
|
||||||
|
[assembly: System.Reflection.AssemblyTitleAttribute("AeroMC Setup")]
|
||||||
|
[assembly: System.Reflection.AssemblyVersionAttribute("0.0.0.0")]
|
||||||
|
[assembly: System.Resources.NeutralResourcesLanguageAttribute("de-DE")]
|
||||||
|
|
||||||
|
// Von der MSBuild WriteCodeFragment-Klasse generiert.
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
f169547649332c2f1c7da272087eb95fe510c6a52616eac752d180c8c6fe1d73
|
||||||
@@ -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;
|
||||||
@@ -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;V0.0.0.0;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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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;V0.0.0.0;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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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")]
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<startup>
|
||||||
|
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
|
||||||
|
</startup>
|
||||||
|
</configuration>
|
||||||
@@ -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.6.0")]
|
||||||
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.1.6")]
|
||||||
|
[assembly: System.Reflection.AssemblyProductAttribute("AeroMC Setup")]
|
||||||
|
[assembly: System.Reflection.AssemblyTitleAttribute("AeroMC Setup")]
|
||||||
|
[assembly: System.Reflection.AssemblyVersionAttribute("1.1.6.0")]
|
||||||
|
[assembly: System.Resources.NeutralResourcesLanguageAttribute("de-DE")]
|
||||||
|
|
||||||
|
// Von der MSBuild WriteCodeFragment-Klasse generiert.
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
5b6f28fb41c3f0e5fe29f9489cfc618a7d6593efb71d420030e4a1d4fdb4066e
|
||||||
@@ -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;
|
||||||
@@ -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
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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.6",
|
||||||
|
"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": "lJiGEWCFg6M=",
|
||||||
|
"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": []
|
||||||
|
}
|
||||||
@@ -1,19 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "aeromc",
|
"name": "aeromc",
|
||||||
"productName": "AeroMC",
|
"productName": "AeroMC",
|
||||||
"version": "1.0.0-beta",
|
"version": "1.1.6",
|
||||||
"description": "AeroMC – Instanz-basierter Minecraft-Launcher (à la MultiMC/Prism)",
|
"description": "AeroMC – Instanz-basierter Minecraft-Launcher (à la MultiMC/Prism)",
|
||||||
"homepage": "https://M_Viper.de",
|
"homepage": "https://m-viper.de",
|
||||||
|
"launcherWebsite": "https://aeromc.viper.ipv64.net",
|
||||||
"main": "src/main.js",
|
"main": "src/main.js",
|
||||||
"author": "M_Viper",
|
"author": "M_Viper",
|
||||||
"repository": {
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
"type": "git",
|
"private": true,
|
||||||
"url": "https://git.viper.ipv64.net"
|
|
||||||
},
|
|
||||||
"license": "MIT",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron .",
|
"start": "electron .",
|
||||||
"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})}}}\"",
|
"prebuild": "node scripts/write-build-config.js",
|
||||||
|
"prebuild:portable": "node scripts/write-build-config.js",
|
||||||
|
"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": {
|
||||||
@@ -23,15 +23,17 @@
|
|||||||
"compression": "store",
|
"compression": "store",
|
||||||
"files": [
|
"files": [
|
||||||
"src/**/*",
|
"src/**/*",
|
||||||
"package.json"
|
"package.json",
|
||||||
|
"LICENSE"
|
||||||
|
],
|
||||||
|
"asarUnpack": [
|
||||||
|
"node_modules/loopback-capture/**/*",
|
||||||
|
"node_modules/node-window-manager/**/*",
|
||||||
|
"node_modules/ffmpeg-static/**/*"
|
||||||
],
|
],
|
||||||
"win": {
|
"win": {
|
||||||
"target": "nsis",
|
"target": "dir",
|
||||||
"icon": "src/assets/icon.ico"
|
"icon": "src/assets/icon.ico"
|
||||||
},
|
|
||||||
"nsis": {
|
|
||||||
"oneClick": false,
|
|
||||||
"allowToChangeInstallationDirectory": true
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -39,7 +41,14 @@
|
|||||||
"electron-builder": "^24.0.0"
|
"electron-builder": "^24.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@xhayper/discord-rpc": "^1.3.4",
|
||||||
|
"archiver": "^7.0.1",
|
||||||
|
"ffmpeg-static": "^5.3.0",
|
||||||
|
"loopback-capture": "^3.0.2",
|
||||||
|
"node-window-manager": "^2.2.4",
|
||||||
"skinview3d": "^3.4.2",
|
"skinview3d": "^3.4.2",
|
||||||
"undici": "^6.28.0"
|
"sql.js": "^1.14.2",
|
||||||
|
"undici": "^6.28.0",
|
||||||
|
"unzipper": "^0.12.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,86 @@
|
|||||||
|
"""
|
||||||
|
make-mc-textures.py – erzeugt die 16×16-Kacheln für das Minecraft-Design
|
||||||
|
(src/assets/design/*.png). Eigene, zufällig erzeugte Muster im Block-Stil –
|
||||||
|
keine Original-Texturen aus dem Spiel.
|
||||||
|
|
||||||
|
Aufruf: python scripts/make-mc-textures.py
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
ZIEL = os.path.join(os.path.dirname(__file__), '..', 'src', 'assets', 'design')
|
||||||
|
os.makedirs(ZIEL, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def klemme(v):
|
||||||
|
return max(0, min(255, int(v)))
|
||||||
|
|
||||||
|
|
||||||
|
def rauschen(basis, streuung, seed, flecken=None):
|
||||||
|
"""Grundfarbe mit Pixelrauschen; flecken = (anteil, farbe, streuung)."""
|
||||||
|
rnd = random.Random(seed)
|
||||||
|
img = Image.new('RGB', (16, 16))
|
||||||
|
px = img.load()
|
||||||
|
for y in range(16):
|
||||||
|
for x in range(16):
|
||||||
|
d = rnd.randint(-streuung, streuung)
|
||||||
|
farbe = basis
|
||||||
|
if flecken and rnd.random() < flecken[0]:
|
||||||
|
farbe = flecken[1]
|
||||||
|
d = rnd.randint(-flecken[2], flecken[2])
|
||||||
|
px[x, y] = tuple(klemme(c + d) for c in farbe)
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
def speichere(img, name):
|
||||||
|
img.save(os.path.join(ZIEL, name))
|
||||||
|
print(name)
|
||||||
|
|
||||||
|
|
||||||
|
# Hintergrund der Instanzliste: dunkle Erde (wie hinter Menüs im Spiel)
|
||||||
|
speichere(rauschen((44, 32, 23), 6, 11, (0.16, (31, 22, 15), 4)), 'erde-dunkel.png')
|
||||||
|
|
||||||
|
# Leisten (Werkzeugleiste, Seitenleiste, Statusleiste): dunkler Schiefer
|
||||||
|
schiefer = rauschen((46, 46, 50), 6, 23, (0.12, (34, 34, 38), 3))
|
||||||
|
px = schiefer.load()
|
||||||
|
rnd = random.Random(5)
|
||||||
|
for _ in range(5): # ein paar waagerechte Bruchkanten wie bei Tiefenschiefer
|
||||||
|
y = rnd.randrange(16)
|
||||||
|
x0 = rnd.randrange(16)
|
||||||
|
for x in range(x0, min(16, x0 + rnd.randint(3, 7))):
|
||||||
|
px[x, y] = (30, 30, 33)
|
||||||
|
speichere(schiefer, 'schiefer.png')
|
||||||
|
|
||||||
|
# Dialoge: Stein
|
||||||
|
stein = rauschen((78, 78, 80), 8, 37, (0.10, (60, 60, 62), 4))
|
||||||
|
speichere(stein, 'stein.png')
|
||||||
|
|
||||||
|
# Holzbretter (z. B. Kopf der Dialoge): vier Bretter zu je 4 Pixeln
|
||||||
|
bretter = Image.new('RGB', (16, 16))
|
||||||
|
px = bretter.load()
|
||||||
|
rnd = random.Random(41)
|
||||||
|
farben = [(122, 92, 56), (114, 86, 52), (128, 97, 60), (118, 89, 54)]
|
||||||
|
fugen = [3, 11, 7, 14]
|
||||||
|
for brett in range(4):
|
||||||
|
for y in range(brett * 4, brett * 4 + 4):
|
||||||
|
for x in range(16):
|
||||||
|
c = farben[brett]
|
||||||
|
d = rnd.randint(-5, 5)
|
||||||
|
if y == brett * 4 + 3: # dunkle Fuge unter jedem Brett
|
||||||
|
c, d = (74, 55, 33), rnd.randint(-2, 2)
|
||||||
|
elif x == fugen[brett]: # Stoßfuge
|
||||||
|
c, d = (88, 66, 40), 0
|
||||||
|
px[x, y] = tuple(klemme(v + d) for v in c)
|
||||||
|
speichere(bretter, 'bretter.png')
|
||||||
|
|
||||||
|
# Grasnarbe: oberer Rand der Werkzeugleiste (16×4, grün mit "Tropfen")
|
||||||
|
gras = Image.new('RGBA', (16, 4), (0, 0, 0, 0))
|
||||||
|
px = gras.load()
|
||||||
|
rnd = random.Random(53)
|
||||||
|
tiefe = [2, 3, 2, 2, 4, 3, 2, 3, 2, 2, 3, 4, 2, 3, 2, 3]
|
||||||
|
for x in range(16):
|
||||||
|
for y in range(tiefe[x]):
|
||||||
|
g = rnd.randint(-10, 10)
|
||||||
|
px[x, y] = (klemme(84 + g), klemme(140 + g), klemme(52 + g // 2), 255)
|
||||||
|
speichere(gras, 'gras.png')
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* make-pixelfont.js – erzeugt src/assets/aeropixel.ttf
|
||||||
|
* -----------------------------------------------------
|
||||||
|
* Eigene Pixel-Schrift für das Minecraft-Design (keine fremde Schrift, also
|
||||||
|
* keine Lizenzfragen). Jede Glyphe ist ein Raster aus '#' (Pixel) und ' ':
|
||||||
|
* Zeile 0 ist die Oberkante der Großbuchstaben, Zeile 6 sitzt auf der
|
||||||
|
* Grundlinie, Zeilen 7–8 sind Unterlängen (g, j, p, q, y, Komma).
|
||||||
|
* Fehlende Zeichen fallen im Launcher automatisch auf Segoe UI zurück.
|
||||||
|
*
|
||||||
|
* Aufruf: node scripts/make-pixelfont.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const P = 125; // ein Pixel in Font-Einheiten (8 Pixel = 1 em)
|
||||||
|
const EM = 1000;
|
||||||
|
const ASC = 1000; // 7 Zeilen Großbuchstaben + 1 Zeile Luft
|
||||||
|
const DESC = 250; // 2 Zeilen Unterlänge
|
||||||
|
|
||||||
|
const G = {
|
||||||
|
A: [' ### ', '# #', '# #', '#####', '# #', '# #', '# #'],
|
||||||
|
B: ['#### ', '# #', '# #', '#### ', '# #', '# #', '#### '],
|
||||||
|
C: [' ### ', '# #', '# ', '# ', '# ', '# #', ' ### '],
|
||||||
|
D: ['#### ', '# #', '# #', '# #', '# #', '# #', '#### '],
|
||||||
|
E: ['#####', '# ', '# ', '#### ', '# ', '# ', '#####'],
|
||||||
|
F: ['#####', '# ', '# ', '#### ', '# ', '# ', '# '],
|
||||||
|
G: [' ####', '# ', '# ', '# ##', '# #', '# #', ' ####'],
|
||||||
|
H: ['# #', '# #', '# #', '#####', '# #', '# #', '# #'],
|
||||||
|
I: ['###', ' # ', ' # ', ' # ', ' # ', ' # ', '###'],
|
||||||
|
J: [' #', ' #', ' #', ' #', ' #', '# #', ' ### '],
|
||||||
|
K: ['# #', '# # ', '# # ', '## ', '# # ', '# # ', '# #'],
|
||||||
|
L: ['# ', '# ', '# ', '# ', '# ', '# ', '#####'],
|
||||||
|
M: ['# #', '## ##', '# # #', '# #', '# #', '# #', '# #'],
|
||||||
|
N: ['# #', '## #', '# # #', '# ##', '# #', '# #', '# #'],
|
||||||
|
O: [' ### ', '# #', '# #', '# #', '# #', '# #', ' ### '],
|
||||||
|
P: ['#### ', '# #', '# #', '#### ', '# ', '# ', '# '],
|
||||||
|
Q: [' ### ', '# #', '# #', '# #', '# #', '# # ', ' ## #'],
|
||||||
|
R: ['#### ', '# #', '# #', '#### ', '# # ', '# #', '# #'],
|
||||||
|
S: [' ####', '# ', '# ', ' ### ', ' #', ' #', '#### '],
|
||||||
|
T: ['#####', ' # ', ' # ', ' # ', ' # ', ' # ', ' # '],
|
||||||
|
U: ['# #', '# #', '# #', '# #', '# #', '# #', ' ### '],
|
||||||
|
V: ['# #', '# #', '# #', '# #', '# #', ' # # ', ' # '],
|
||||||
|
W: ['# #', '# #', '# #', '# #', '# # #', '## ##', '# #'],
|
||||||
|
X: ['# #', '# #', ' # # ', ' # ', ' # # ', '# #', '# #'],
|
||||||
|
Y: ['# #', '# #', ' # # ', ' # ', ' # ', ' # ', ' # '],
|
||||||
|
Z: ['#####', ' #', ' # ', ' # ', ' # ', '# ', '#####'],
|
||||||
|
|
||||||
|
a: [' ', ' ', ' ### ', ' #', ' ####', '# #', ' ####'],
|
||||||
|
b: ['# ', '# ', '# ## ', '## #', '# #', '# #', '#### '],
|
||||||
|
c: [' ', ' ', ' ### ', '# #', '# ', '# #', ' ### '],
|
||||||
|
d: [' #', ' #', ' ## #', '# ##', '# #', '# #', ' ####'],
|
||||||
|
e: [' ', ' ', ' ### ', '# #', '#####', '# ', ' ####'],
|
||||||
|
f: [' ##', ' # ', '####', ' # ', ' # ', ' # ', ' # '],
|
||||||
|
g: [' ', ' ', ' ####', '# #', '# #', '# #', ' ####', ' #', ' ### '],
|
||||||
|
h: ['# ', '# ', '# ## ', '## #', '# #', '# #', '# #'],
|
||||||
|
i: ['#', ' ', '#', '#', '#', '#', '#'],
|
||||||
|
j: [' #', ' ', ' #', ' #', ' #', ' #', ' #', '# #', ' ## '],
|
||||||
|
k: ['# ', '# ', '# #', '# # ', '## ', '# # ', '# #'],
|
||||||
|
l: ['# ', '# ', '# ', '# ', '# ', '# ', ' #'],
|
||||||
|
m: [' ', ' ', '## # ', '# # #', '# # #', '# #', '# #'],
|
||||||
|
n: [' ', ' ', '#### ', '# #', '# #', '# #', '# #'],
|
||||||
|
o: [' ', ' ', ' ### ', '# #', '# #', '# #', ' ### '],
|
||||||
|
p: [' ', ' ', '# ## ', '## #', '# #', '# #', '#### ', '# ', '# '],
|
||||||
|
q: [' ', ' ', ' ## #', '# ##', '# #', '# #', ' ####', ' #', ' #'],
|
||||||
|
r: [' ', ' ', '# ## ', '## #', '# ', '# ', '# '],
|
||||||
|
s: [' ', ' ', ' ####', '# ', ' ### ', ' #', '#### '],
|
||||||
|
t: [' # ', ' # ', '###', ' # ', ' # ', ' # ', ' #'],
|
||||||
|
u: [' ', ' ', '# #', '# #', '# #', '# #', ' ####'],
|
||||||
|
v: [' ', ' ', '# #', '# #', '# #', ' # # ', ' # '],
|
||||||
|
w: [' ', ' ', '# #', '# #', '# # #', '# # #', ' ####'],
|
||||||
|
x: [' ', ' ', '# #', ' # # ', ' # ', ' # # ', '# #'],
|
||||||
|
y: [' ', ' ', '# #', '# #', '# #', '# #', ' ####', ' #', '#### '],
|
||||||
|
z: [' ', ' ', '#####', ' # ', ' # ', ' # ', '#####'],
|
||||||
|
|
||||||
|
0: [' ### ', '# #', '# ##', '# # #', '## #', '# #', ' ### '],
|
||||||
|
1: [' # ', ' ## ', ' # ', ' # ', ' # ', ' # ', '#####'],
|
||||||
|
2: [' ### ', '# #', ' #', ' ## ', ' # ', '# ', '#####'],
|
||||||
|
3: [' ### ', '# #', ' #', ' ## ', ' #', '# #', ' ### '],
|
||||||
|
4: [' ##', ' # #', ' # #', '# #', '#####', ' #', ' #'],
|
||||||
|
5: ['#####', '# ', '#### ', ' #', ' #', '# #', ' ### '],
|
||||||
|
6: [' ## ', ' # ', '# ', '#### ', '# #', '# #', ' ### '],
|
||||||
|
7: ['#####', '# #', ' #', ' # ', ' # ', ' # ', ' # '],
|
||||||
|
8: [' ### ', '# #', '# #', ' ### ', '# #', '# #', ' ### '],
|
||||||
|
9: [' ### ', '# #', '# #', ' ####', ' #', ' # ', ' ## '],
|
||||||
|
|
||||||
|
'Ä': ['# #', ' ### ', '# #', '# #', '#####', '# #', '# #'],
|
||||||
|
'Ö': ['# #', ' ### ', '# #', '# #', '# #', '# #', ' ### '],
|
||||||
|
'Ü': ['# #', ' ', '# #', '# #', '# #', '# #', ' ### '],
|
||||||
|
'ä': [' # # ', ' ', ' ### ', ' #', ' ####', '# #', ' ####'],
|
||||||
|
'ö': [' # # ', ' ', ' ### ', '# #', '# #', '# #', ' ### '],
|
||||||
|
'ü': [' # # ', ' ', '# #', '# #', '# #', '# #', ' ####'],
|
||||||
|
'ß': [' ### ', '# #', '# # ', '# # ', '# # ', '# #', '# ## '],
|
||||||
|
'é': [' # ', ' # ', ' ### ', '# #', '#####', '# ', ' ####'],
|
||||||
|
|
||||||
|
'.': [' ', ' ', ' ', ' ', ' ', ' ', '#'],
|
||||||
|
',': [' ', ' ', ' ', ' ', ' ', ' ', '#', '#'],
|
||||||
|
':': [' ', ' ', '#', ' ', ' ', ' ', '#'],
|
||||||
|
';': [' ', ' ', '#', ' ', ' ', ' ', '#', '#'],
|
||||||
|
'!': ['#', '#', '#', '#', '#', ' ', '#'],
|
||||||
|
'?': [' ### ', '# #', ' #', ' # ', ' # ', ' ', ' # '],
|
||||||
|
'-': [' ', ' ', ' ', '####', ' ', ' ', ' '],
|
||||||
|
'–': [' ', ' ', ' ', '#####', ' ', ' ', ' '],
|
||||||
|
'_': [' ', ' ', ' ', ' ', ' ', ' ', ' ', '#####'],
|
||||||
|
'(': [' #', ' # ', '# ', '# ', '# ', ' # ', ' #'],
|
||||||
|
')': ['# ', ' # ', ' #', ' #', ' #', ' # ', '# '],
|
||||||
|
'[': ['##', '# ', '# ', '# ', '# ', '# ', '##'],
|
||||||
|
']': ['##', ' #', ' #', ' #', ' #', ' #', '##'],
|
||||||
|
'/': [' #', ' # ', ' # ', ' # ', ' # ', ' # ', '# '],
|
||||||
|
'\\': ['# ', ' # ', ' # ', ' # ', ' # ', ' # ', ' #'],
|
||||||
|
'+': [' ', ' # ', ' # ', '#####', ' # ', ' # ', ' '],
|
||||||
|
'=': [' ', ' ', '#####', ' ', '#####', ' ', ' '],
|
||||||
|
'*': [' ', ' ', '# # #', ' ### ', '# # #', ' ', ' '],
|
||||||
|
'%': ['# #', ' #', ' # ', ' # ', ' # ', '# ', '# #'],
|
||||||
|
'&': [' ## ', '# # ', ' ## ', ' ## #', '# # ', '# # ', ' ## #'],
|
||||||
|
'#': [' # # ', '#####', ' # # ', ' # # ', ' # # ', '#####', ' # # '],
|
||||||
|
'<': [' #', ' # ', ' # ', '# ', ' # ', ' # ', ' #'],
|
||||||
|
'>': ['# ', ' # ', ' # ', ' #', ' # ', ' # ', '# '],
|
||||||
|
"'": ['#', '#'],
|
||||||
|
'"': ['# #', '# #'],
|
||||||
|
'|': ['#', '#', '#', '#', '#', '#', '#', '#', '#'],
|
||||||
|
'·': [' ', ' ', ' ', '#', ' ', ' ', ' '],
|
||||||
|
'…': [' ', ' ', ' ', ' ', ' ', ' ', '# # #'],
|
||||||
|
'@': [' ### ', '# #', '# ###', '# # #', '# ###', '# ', ' ####'],
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------- Glyphen in Umrisse umwandeln (waagerechte Pixel-Läufe als Rechtecke) ----------
|
||||||
|
function umrisse(zeilen) {
|
||||||
|
const konturen = [];
|
||||||
|
zeilen.forEach((zeile, r) => {
|
||||||
|
const yOben = (7 - r) * P;
|
||||||
|
const yUnten = yOben - P;
|
||||||
|
let x = 0;
|
||||||
|
while (x < zeile.length) {
|
||||||
|
if (zeile[x] !== '#') { x++; continue; }
|
||||||
|
let ende = x;
|
||||||
|
while (ende < zeile.length && zeile[ende] === '#') ende++;
|
||||||
|
// im Uhrzeigersinn (TrueType-Außenkontur)
|
||||||
|
konturen.push([[x * P, yUnten], [x * P, yOben], [ende * P, yOben], [ende * P, yUnten]]);
|
||||||
|
x = ende;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return konturen;
|
||||||
|
}
|
||||||
|
|
||||||
|
const glyphen = [{ name: '.notdef', code: null, breite: 4, konturen: [[[0, 0], [0, 875], [500, 875], [500, 0]], [[125, 125], [375, 125], [375, 750], [125, 750]]] },
|
||||||
|
{ name: 'space', code: 32, breite: 3, konturen: [] }];
|
||||||
|
for (const [zeichen, zeilen] of Object.entries(G)) {
|
||||||
|
const breite = Math.max(...zeilen.map((z) => z.length));
|
||||||
|
glyphen.push({ code: zeichen.codePointAt(0), breite, konturen: umrisse(zeilen) });
|
||||||
|
}
|
||||||
|
const sortiert = [glyphen[0], glyphen[1], ...glyphen.slice(2).sort((a, b) => a.code - b.code)];
|
||||||
|
|
||||||
|
// ---------- Hilfen zum Schreiben ----------
|
||||||
|
class Puffer {
|
||||||
|
constructor() { this.teile = []; this.laenge = 0; }
|
||||||
|
u8(v) { const b = Buffer.alloc(1); b.writeUInt8(v); return this.roh(b); }
|
||||||
|
u16(v) { const b = Buffer.alloc(2); b.writeUInt16BE(v & 0xffff); return this.roh(b); }
|
||||||
|
i16(v) { const b = Buffer.alloc(2); b.writeInt16BE(v); return this.roh(b); }
|
||||||
|
u32(v) { const b = Buffer.alloc(4); b.writeUInt32BE(v >>> 0); return this.roh(b); }
|
||||||
|
i64(v) { const b = Buffer.alloc(8); b.writeBigInt64BE(BigInt(v)); return this.roh(b); }
|
||||||
|
roh(b) { this.teile.push(b); this.laenge += b.length; return this; }
|
||||||
|
fertig() { return Buffer.concat(this.teile); }
|
||||||
|
}
|
||||||
|
const auf4 = (b) => (b.length % 4 ? Buffer.concat([b, Buffer.alloc(4 - (b.length % 4))]) : b);
|
||||||
|
function pruefsumme(b) {
|
||||||
|
const p = auf4(b);
|
||||||
|
let s = 0;
|
||||||
|
for (let i = 0; i < p.length; i += 4) s = (s + p.readUInt32BE(i)) >>> 0;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- glyf + loca + hmtx ----------
|
||||||
|
const glyf = new Puffer();
|
||||||
|
const loca = [];
|
||||||
|
const hmtx = new Puffer();
|
||||||
|
let maxPunkte = 0, maxKonturen = 0;
|
||||||
|
let gxMin = 0, gyMin = 0, gxMax = 0, gyMax = 0;
|
||||||
|
let maxVorschub = 0, minLsb = 1e9, minRsb = 1e9, maxXAusdehnung = 0;
|
||||||
|
for (const g of sortiert) {
|
||||||
|
loca.push(glyf.laenge);
|
||||||
|
const vorschub = (g.breite + 1) * P;
|
||||||
|
maxVorschub = Math.max(maxVorschub, vorschub);
|
||||||
|
if (!g.konturen.length) { hmtx.u16(vorschub).i16(0); continue; }
|
||||||
|
const punkte = g.konturen.flat();
|
||||||
|
const xs = punkte.map((p) => p[0]), ys = punkte.map((p) => p[1]);
|
||||||
|
const xMin = Math.min(...xs), xMax = Math.max(...xs), yMin = Math.min(...ys), yMax = Math.max(...ys);
|
||||||
|
gxMin = Math.min(gxMin, xMin); gyMin = Math.min(gyMin, yMin); gxMax = Math.max(gxMax, xMax); gyMax = Math.max(gyMax, yMax);
|
||||||
|
minLsb = Math.min(minLsb, xMin); minRsb = Math.min(minRsb, vorschub - xMax); maxXAusdehnung = Math.max(maxXAusdehnung, xMax);
|
||||||
|
maxPunkte = Math.max(maxPunkte, punkte.length); maxKonturen = Math.max(maxKonturen, g.konturen.length);
|
||||||
|
const b = new Puffer();
|
||||||
|
b.i16(g.konturen.length).i16(xMin).i16(yMin).i16(xMax).i16(yMax);
|
||||||
|
let ende = -1;
|
||||||
|
for (const k of g.konturen) { ende += k.length; b.u16(ende); }
|
||||||
|
b.u16(0); // keine Hinting-Anweisungen
|
||||||
|
for (let i = 0; i < punkte.length; i++) b.u8(0x01); // alle Punkte auf der Kurve, Koordinaten als int16
|
||||||
|
let px = 0, py = 0;
|
||||||
|
for (const [x] of punkte) { b.i16(x - px); px = x; }
|
||||||
|
for (const [, y] of punkte) { b.i16(y - py); py = y; }
|
||||||
|
glyf.roh(auf4(b.fertig()));
|
||||||
|
hmtx.u16(vorschub).i16(xMin);
|
||||||
|
}
|
||||||
|
loca.push(glyf.laenge);
|
||||||
|
const anzahl = sortiert.length;
|
||||||
|
|
||||||
|
const locaB = new Puffer();
|
||||||
|
for (const o of loca) locaB.u32(o);
|
||||||
|
|
||||||
|
// ---------- cmap (Format 4, ein Segment je Zeichen) ----------
|
||||||
|
const abbildung = sortiert.map((g, i) => [g.code, i]).filter(([c]) => c !== null).sort((a, b) => a[0] - b[0]);
|
||||||
|
const segmente = abbildung.map(([c, i]) => ({ start: c, ende: c, delta: (i - c) & 0xffff }));
|
||||||
|
segmente.push({ start: 0xffff, ende: 0xffff, delta: 1 });
|
||||||
|
const segX2 = segmente.length * 2;
|
||||||
|
const suchbereich = 2 * 2 ** Math.floor(Math.log2(segmente.length));
|
||||||
|
const sub = new Puffer();
|
||||||
|
sub.u16(4).u16(16 + segmente.length * 8).u16(0).u16(segX2).u16(suchbereich)
|
||||||
|
.u16(Math.log2(suchbereich / 2)).u16(segX2 - suchbereich);
|
||||||
|
for (const s of segmente) sub.u16(s.ende);
|
||||||
|
sub.u16(0);
|
||||||
|
for (const s of segmente) sub.u16(s.start);
|
||||||
|
for (const s of segmente) sub.u16(s.delta);
|
||||||
|
for (const s of segmente) sub.u16(0);
|
||||||
|
const cmap = new Puffer().u16(0).u16(1).u16(3).u16(1).u32(12).roh(sub.fertig());
|
||||||
|
|
||||||
|
// ---------- head / hhea / maxp / OS/2 / name / post ----------
|
||||||
|
const zeit = Math.floor(Date.UTC(2026, 9, 3) / 1000) + 2082844800; // Sekunden seit 1904
|
||||||
|
const head = new Puffer().u32(0x00010000).u32(0x00010000).u32(0).u32(0x5f0f3cf5).u16(0x000b).u16(EM)
|
||||||
|
.i64(zeit).i64(zeit).i16(gxMin).i16(gyMin).i16(gxMax).i16(gyMax).u16(0).u16(8).i16(2).i16(1).i16(0);
|
||||||
|
const hhea = new Puffer().u32(0x00010000).i16(ASC).i16(-DESC).i16(0).u16(maxVorschub).i16(minLsb).i16(minRsb)
|
||||||
|
.i16(maxXAusdehnung).i16(1).i16(0).i16(0).i16(0).i16(0).i16(0).i16(0).i16(0).u16(anzahl);
|
||||||
|
const maxp = new Puffer().u32(0x00010000).u16(anzahl).u16(maxPunkte).u16(maxKonturen).u16(0).u16(0)
|
||||||
|
.u16(2).u16(0).u16(0).u16(0).u16(0).u16(0).u16(0).u16(0).u16(0);
|
||||||
|
const codes = abbildung.map(([c]) => c);
|
||||||
|
const os2 = new Puffer().u16(4).i16(5 * P).u16(400).u16(5).u16(0)
|
||||||
|
.i16(650).i16(600).i16(0).i16(75).i16(650).i16(600).i16(0).i16(350).i16(P).i16(3 * P)
|
||||||
|
.i16(0).roh(Buffer.alloc(10)).u32(3).u32(0).u32(0).u32(0).roh(Buffer.from('AERO'))
|
||||||
|
.u16(0x40).u16(Math.min(...codes)).u16(Math.min(0xffff, Math.max(...codes)))
|
||||||
|
.i16(ASC).i16(-DESC).i16(0).u16(ASC).u16(DESC).u32(1).u32(0).i16(5 * P).i16(7 * P).u16(0).u16(32).u16(1);
|
||||||
|
const namen = { 1: 'AeroPixel', 2: 'Regular', 3: 'AeroPixel Regular', 4: 'AeroPixel', 5: 'Version 1.0', 6: 'AeroPixel-Regular' };
|
||||||
|
const nameDaten = Object.entries(namen).map(([id, text]) => [Number(id), Buffer.from(text, 'utf16le').swap16()]);
|
||||||
|
const name = new Puffer().u16(0).u16(nameDaten.length).u16(6 + nameDaten.length * 12);
|
||||||
|
let versatz = 0;
|
||||||
|
for (const [id, b] of nameDaten) { name.u16(3).u16(1).u16(0x409).u16(id).u16(b.length).u16(versatz); versatz += b.length; }
|
||||||
|
for (const [, b] of nameDaten) name.roh(b);
|
||||||
|
const post = new Puffer().u32(0x00030000).u32(0).i16(-P).i16(P).u32(0).u32(0).u32(0).u32(0).u32(0);
|
||||||
|
|
||||||
|
// ---------- zusammensetzen ----------
|
||||||
|
const tabellen = [
|
||||||
|
['OS/2', os2], ['cmap', cmap], ['glyf', glyf], ['head', head], ['hhea', hhea],
|
||||||
|
['hmtx', hmtx], ['loca', locaB], ['maxp', maxp], ['name', name], ['post', post],
|
||||||
|
].map(([tag, p]) => [tag, p.fertig()]);
|
||||||
|
const n = tabellen.length;
|
||||||
|
const sb = 16 * 2 ** Math.floor(Math.log2(n));
|
||||||
|
const kopf = new Puffer().u32(0x00010000).u16(n).u16(sb).u16(Math.log2(sb / 16)).u16(n * 16 - sb);
|
||||||
|
let offset = 12 + n * 16;
|
||||||
|
const verzeichnis = new Puffer();
|
||||||
|
for (const [tag, daten] of tabellen) {
|
||||||
|
verzeichnis.roh(Buffer.from(tag, 'latin1')).u32(pruefsumme(daten)).u32(offset).u32(daten.length);
|
||||||
|
offset += auf4(daten).length;
|
||||||
|
}
|
||||||
|
let schrift = Buffer.concat([kopf.fertig(), verzeichnis.fertig(), ...tabellen.map(([, d]) => auf4(d))]);
|
||||||
|
// checkSumAdjustment im head-Eintrag setzen
|
||||||
|
const headOffset = 12 + n * 16 + tabellen.slice(0, 3).reduce((s, [, d]) => s + auf4(d).length, 0);
|
||||||
|
schrift.writeUInt32BE((0xb1b0afba - pruefsumme(schrift)) >>> 0, headOffset + 8);
|
||||||
|
|
||||||
|
const ziel = path.join(__dirname, '..', 'src', 'assets', 'aeropixel.ttf');
|
||||||
|
fs.writeFileSync(ziel, schrift);
|
||||||
|
console.log(`${ziel}: ${anzahl} Glyphen, ${schrift.length} Bytes`);
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* write-build-config.js – Vorgaben aus der Umgebung ins Paket schreiben
|
||||||
|
* ---------------------------------------------------------------------
|
||||||
|
* Läuft automatisch vor jedem Build und erzeugt src/build-config.json.
|
||||||
|
*
|
||||||
|
* Damit landen Adresse und Token der CurseForge-Weiterleitung im fertigen
|
||||||
|
* Paket, ohne je im Quelltext oder in einem Repository zu stehen. Gesetzt
|
||||||
|
* werden sie beim Bauen als Umgebungsvariablen:
|
||||||
|
*
|
||||||
|
* PowerShell:
|
||||||
|
* $env:AEROMC_CF_PROXY_URL = "https://cf.example.de"
|
||||||
|
* $env:AEROMC_CF_PROXY_TOKEN = "..."
|
||||||
|
* npm run build
|
||||||
|
*
|
||||||
|
* bash:
|
||||||
|
* AEROMC_CF_PROXY_URL=https://cf.example.de AEROMC_CF_PROXY_TOKEN=... npm run build
|
||||||
|
*
|
||||||
|
* Ohne gesetzte Variablen wird eine leere Vorgabe geschrieben – der Launcher
|
||||||
|
* verhält sich dann wie bisher und jeder Nutzer trägt selbst etwas ein.
|
||||||
|
*
|
||||||
|
* Zur Ehrlichkeit: Ein Token im ausgelieferten Paket ist auslesbar. Es hält
|
||||||
|
* Gelegenheitsnutzer ab, keine entschlossenen. Der API-Schlüssel selbst
|
||||||
|
* bleibt davon unberührt – der liegt ausschließlich auf dem Server.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const ziel = path.join(__dirname, '..', 'src', 'build-config.json');
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
curseforgeProxyUrl: String(process.env.AEROMC_CF_PROXY_URL || '').trim().replace(/\/+$/, ''),
|
||||||
|
curseforgeProxyToken: String(process.env.AEROMC_CF_PROXY_TOKEN || '').trim(),
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.writeFileSync(ziel, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
||||||
|
|
||||||
|
// Den Token selbst nie ausgeben – Build-Protokolle landen schnell woanders.
|
||||||
|
const wo = config.curseforgeProxyUrl || '(keine)';
|
||||||
|
const tok = config.curseforgeProxyToken ? 'gesetzt (' + config.curseforgeProxyToken.length + ' Zeichen)' : 'keins';
|
||||||
|
console.log(`build-config.json geschrieben – Weiterleitung: ${wo}, Token: ${tok}`);
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Vorlage. Kopieren nach .env und ausfüllen.
|
||||||
|
# .env gehört NICHT in ein Repository (steht bereits in der .gitignore).
|
||||||
|
|
||||||
|
# Pflicht: dein CurseForge-API-Key von console.curseforge.com
|
||||||
|
CF_API_KEY=hier-deinen-schluessel-eintragen
|
||||||
|
|
||||||
|
# Port und Adresse, auf denen der Dienst lauscht.
|
||||||
|
# Steht ein Reverse-Proxy davor, reicht 127.0.0.1 als HOST.
|
||||||
|
PORT=8420
|
||||||
|
HOST=0.0.0.0
|
||||||
|
|
||||||
|
# Gemeinsames Geheimnis zwischen Launcher und Server. Hält Scanner und
|
||||||
|
# Zufallsfunde zuverlässig ab – deine Domain steht durch das TLS-Zertifikat
|
||||||
|
# öffentlich in den Certificate-Transparency-Listen und wird gefunden.
|
||||||
|
# Gegen jemanden, der gezielt die .exe auspackt, hilft es nicht.
|
||||||
|
#
|
||||||
|
# Mehrere Token durch Komma getrennt. Das brauchst du beim Wechseln:
|
||||||
|
# altes und neues eine Weile parallel eintragen, damit ältere Installationen
|
||||||
|
# weiterlaufen, bis alle das Update haben – danach das alte streichen.
|
||||||
|
# CF_PROXY_TOKEN=neues-token,altes-token
|
||||||
|
#
|
||||||
|
# Leer lassen = keine Tokenprüfung.
|
||||||
|
CF_PROXY_TOKEN=
|
||||||
|
|
||||||
|
# Anfragen je Minute und Absender. 0 = keine Begrenzung.
|
||||||
|
RATE_LIMIT_PER_MIN=60
|
||||||
|
|
||||||
|
# Obergrenze für durchgereichte Dateien in MB. 0 = keine Grenze.
|
||||||
|
MAX_DOWNLOAD_MB=512
|
||||||
|
|
||||||
|
# Anzahl der Reverse-Proxy-Stufen davor:
|
||||||
|
# 0 = keine, der Dienst ist direkt erreichbar
|
||||||
|
# 1 = ein Proxy, z. B. Nginx Proxy Manager oder Caddy
|
||||||
|
# 2 = zwei, z. B. Cloudflare vor dem Nginx Proxy Manager
|
||||||
|
#
|
||||||
|
# Die Zahl zählt, nicht nur "an oder aus": nginx hängt die echte Adresse an
|
||||||
|
# X-Forwarded-For an, statt sie zu ersetzen. Gezählt wird deshalb von rechts.
|
||||||
|
# Zu hoch angesetzt, könnte ein Aufrufer eine Adresse erfinden und die
|
||||||
|
# Ratenbegrenzung umgehen – im Zweifel lieber 0.
|
||||||
|
TRUST_PROXY=0
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
FROM node:22-alpine
|
||||||
|
|
||||||
|
# keine Abhängigkeiten – das Programm nutzt nur Node-Bordmittel
|
||||||
|
WORKDIR /app
|
||||||
|
COPY index.js ./
|
||||||
|
|
||||||
|
ENV PORT=8420 \
|
||||||
|
HOST=0.0.0.0 \
|
||||||
|
NODE_ENV=production
|
||||||
|
|
||||||
|
# nicht als root laufen
|
||||||
|
USER node
|
||||||
|
|
||||||
|
EXPOSE 8420
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
|
||||||
|
CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||8420)+'/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
|
||||||
|
|
||||||
|
CMD ["node", "index.js"]
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
# AeroMC – CurseForge-Weiterleitung
|
||||||
|
|
||||||
|
Kleiner Dienst, der den CurseForge-API-Schlüssel hält und die Anfragen
|
||||||
|
stellvertretend für den Launcher stellt.
|
||||||
|
|
||||||
|
## Wozu das Ganze
|
||||||
|
|
||||||
|
CurseForge erlaubt es nicht, den API-Schlüssel weiterzugeben. In einer
|
||||||
|
verteilten `.exe` wäre er für jeden auslesbar – das Entpacken einer
|
||||||
|
Electron-Anwendung dauert keine Minute. Deshalb bleibt der Schlüssel hier
|
||||||
|
auf dem Server, und der Launcher kennt nur die Adresse dieses Dienstes.
|
||||||
|
|
||||||
|
## Wege
|
||||||
|
|
||||||
|
| Weg | Zweck |
|
||||||
|
|---|---|
|
||||||
|
| `GET /health` | Lebenszeichen, verrät nichts über den Schlüssel |
|
||||||
|
| `POST /v1/mods/files` | Dateiliste auflösen, Rumpf `{ "fileIds": [...] }`, höchstens 100 je Anfrage |
|
||||||
|
| `GET /v1/download?url=…` | Mod-Datei durchreichen; das CurseForge-CDN verlangt seit Juli 2026 ebenfalls einen Schlüssel |
|
||||||
|
|
||||||
|
## Einrichten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# CF_API_KEY eintragen
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ohne Docker
|
||||||
|
|
||||||
|
Braucht nur Node 18 oder neuer, keine Pakete.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node index.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Dauerhaft über systemd: `aeromc-cf-proxy.service` nach
|
||||||
|
`/etc/systemd/system/` kopieren, Pfade und Benutzer darin anpassen, dann
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now aeromc-cf-proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mit pm2
|
||||||
|
|
||||||
|
`ecosystem.config.js` liegt bei und liest die `.env` selbst ein – pm2 tut das
|
||||||
|
von sich aus nämlich nicht.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pm2 start ecosystem.config.js
|
||||||
|
pm2 save
|
||||||
|
pm2 startup # einmalig, für den Start nach einem Neustart
|
||||||
|
```
|
||||||
|
|
||||||
|
Nach jeder Änderung an der `.env`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pm2 restart aeromc-cf-proxy --update-env
|
||||||
|
```
|
||||||
|
|
||||||
|
Ohne `--update-env` behält pm2 die alten Werte, und du suchst lange nach dem
|
||||||
|
Grund, warum der neue Schlüssel nicht greift.
|
||||||
|
|
||||||
|
Nachsehen, ob er läuft:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pm2 logs aeromc-cf-proxy --lines 20
|
||||||
|
```
|
||||||
|
|
||||||
|
**Nicht auf Cluster-Betrieb umstellen.** Die Konfiguration steht bewusst auf
|
||||||
|
`exec_mode: 'fork'` mit einem Prozess. Die Ratenbegrenzung zählt im
|
||||||
|
Arbeitsspeicher – bei vier Arbeitern hätte jeder seine eigene Zählung, und
|
||||||
|
die Grenze wäre in Wahrheit viermal so hoch wie eingestellt.
|
||||||
|
|
||||||
|
### Mit Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Launcher einstellen
|
||||||
|
|
||||||
|
In den Launcher-Einstellungen unter **Netzwerk → CurseForge** die Adresse
|
||||||
|
eintragen, zum Beispiel `https://cf.example.de`. Das Feld „Eigener API-Key"
|
||||||
|
bleibt leer – wer dort einen Schlüssel einträgt, geht direkt zu CurseForge
|
||||||
|
und nutzt diesen Dienst nicht.
|
||||||
|
|
||||||
|
Soll die Adresse für alle Nutzer voreingestellt sein, trage sie in
|
||||||
|
`src/services.js` bei `STANDARD_WEITERLEITUNG` ein. Die Adresse ist kein
|
||||||
|
Geheimnis, nur der Schlüssel ist eines.
|
||||||
|
|
||||||
|
## Absicherung
|
||||||
|
|
||||||
|
Der Dienst steht offen im Netz. Eingebaut ist:
|
||||||
|
|
||||||
|
- **Host-Liste** – durchgereicht wird nur an `edge.forgecdn.net`,
|
||||||
|
`mediafilez.forgecdn.net` und `media.forgecdn.net`. Ohne diese Prüfung
|
||||||
|
könnte jemand den Dienst Adressen in deinem Heimnetz abrufen lassen und
|
||||||
|
die Antwort mitlesen.
|
||||||
|
- **Umleitungen** werden selbst verfolgt und bei jedem Sprung erneut gegen
|
||||||
|
die Host-Liste geprüft, höchstens vier Sprünge.
|
||||||
|
- **Ratenbegrenzung** je Absender, einstellbar über `RATE_LIMIT_PER_MIN`.
|
||||||
|
- **Größengrenze** für durchgereichte Dateien über `MAX_DOWNLOAD_MB`.
|
||||||
|
- **Optionales Token** über `CF_PROXY_TOKEN`.
|
||||||
|
|
||||||
|
Zum Token eine ehrliche Einordnung: Es steckt dann auch im Launcher und ist
|
||||||
|
dort auslesbar. Es hält Gelegenheitsnutzer ab, keine entschlossenen. Die
|
||||||
|
Ratenbegrenzung ist der wirksamere Schutz für dein Kontingent.
|
||||||
|
|
||||||
|
### Reverse-Proxy: `TRUST_PROXY` richtig setzen
|
||||||
|
|
||||||
|
`TRUST_PROXY` ist keine Ja/Nein-Angabe, sondern die **Anzahl der Stufen** vor
|
||||||
|
dem Dienst: `0` ohne Proxy, `1` mit Nginx Proxy Manager, `2` mit Cloudflare
|
||||||
|
davor.
|
||||||
|
|
||||||
|
Der Grund: nginx **ergänzt** `X-Forwarded-For`, statt es zu ersetzen. Schickt
|
||||||
|
ein Aufrufer selbst `X-Forwarded-For: 1.2.3.4`, steht hinterher
|
||||||
|
`1.2.3.4, <echte-IP>` darin. Deshalb wird von rechts gezählt – die eigenen
|
||||||
|
Proxy-Stufen sind die einzigen Einträge, die niemand von außen setzen kann.
|
||||||
|
|
||||||
|
Zu hoch angesetzt, greift der Dienst auf einen Eintrag zu, den der Aufrufer
|
||||||
|
selbst erfunden hat: Dann kann sich jeder mit jeder Anfrage eine neue Adresse
|
||||||
|
ausdenken und die Ratenbegrenzung ins Leere laufen lassen. Im Zweifel `0`.
|
||||||
|
|
||||||
|
### Nginx Proxy Manager
|
||||||
|
|
||||||
|
Im Proxy Host eintragen:
|
||||||
|
|
||||||
|
| Feld | Wert |
|
||||||
|
|---|---|
|
||||||
|
| Scheme | `http` |
|
||||||
|
| Forward Hostname / IP | Containername oder LAN-Adresse des Dienstes |
|
||||||
|
| Forward Port | `8420` |
|
||||||
|
| Block Common Exploits | an |
|
||||||
|
| Websockets Support | nicht nötig |
|
||||||
|
|
||||||
|
Im Reiter **SSL**: Let's-Encrypt-Zertifikat anfordern, „Force SSL" und
|
||||||
|
HTTP/2 aktivieren. Ohne HTTPS ginge das Token im Klartext über die Leitung.
|
||||||
|
|
||||||
|
Im Reiter **Advanced** empfiehlt sich für die durchgereichten Mod-Dateien:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
```
|
||||||
|
|
||||||
|
Ohne `proxy_buffering off` schreibt nginx große Dateien erst vollständig in
|
||||||
|
eine temporäre Datei, bevor der Launcher das erste Byte sieht – bei
|
||||||
|
mehreren hundert MB fühlt sich das wie ein Hänger an.
|
||||||
|
|
||||||
|
Läuft der Nginx Proxy Manager selbst in Docker, gehören beide ins gleiche
|
||||||
|
Docker-Netz und der Port muss gar nicht nach außen gebunden werden: In der
|
||||||
|
`docker-compose.yml` dann den `ports`-Block weglassen. Sonst wenigstens auf
|
||||||
|
`127.0.0.1:8420:8420` einschränken, damit der Dienst nicht an der Absicherung
|
||||||
|
des Proxys vorbei erreichbar ist.
|
||||||
|
|
||||||
|
## Was der Dienst nicht tut
|
||||||
|
|
||||||
|
Er speichert nichts, protokolliert keine Adressen der Nutzer und gibt
|
||||||
|
Schlüsselfehler von CurseForge nicht im Wortlaut nach außen weiter.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=AeroMC CurseForge-Weiterleitung
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
# Pfad und Benutzer anpassen
|
||||||
|
WorkingDirectory=/opt/aeromc-cf-proxy
|
||||||
|
EnvironmentFile=/opt/aeromc-cf-proxy/.env
|
||||||
|
ExecStart=/usr/bin/node /opt/aeromc-cf-proxy/index.js
|
||||||
|
User=aeromc
|
||||||
|
Group=aeromc
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
# Absicherung: der Dienst braucht keinen Schreibzugriff und keine neuen Rechte
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
ReadWritePaths=
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
services:
|
||||||
|
aeromc-cf-proxy:
|
||||||
|
build: .
|
||||||
|
container_name: aeromc-cf-proxy
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file: .env
|
||||||
|
ports:
|
||||||
|
# nach außen nur binden, wenn kein Reverse-Proxy davorsteht.
|
||||||
|
# Mit Reverse-Proxy besser: "127.0.0.1:8420:8420"
|
||||||
|
- "8420:8420"
|
||||||
|
# der Dienst braucht nichts zu schreiben
|
||||||
|
read_only: true
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* pm2-Konfiguration für die CurseForge-Weiterleitung.
|
||||||
|
*
|
||||||
|
* pm2 start ecosystem.config.js
|
||||||
|
* pm2 save
|
||||||
|
*
|
||||||
|
* pm2 liest .env-Dateien nicht von selbst, deshalb tut es diese Datei.
|
||||||
|
* So bleibt der Schlüssel in der .env und steht nicht in der Konfiguration,
|
||||||
|
* die man eher mal weitergibt oder eincheckt.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// kleiner .env-Leser – Kommentare, Anführungszeichen und Werte mit "=" inklusive
|
||||||
|
function leseEnv(datei) {
|
||||||
|
const werte = {};
|
||||||
|
if (!fs.existsSync(datei)) return werte;
|
||||||
|
for (const zeile of fs.readFileSync(datei, 'utf8').split(/\r?\n/)) {
|
||||||
|
const t = zeile.trim();
|
||||||
|
if (!t || t.startsWith('#')) continue;
|
||||||
|
const i = t.indexOf('=');
|
||||||
|
if (i < 1) continue;
|
||||||
|
const name = t.slice(0, i).trim();
|
||||||
|
let wert = t.slice(i + 1).trim();
|
||||||
|
const q = wert[0];
|
||||||
|
if ((q === '"' || q === "'") && wert.endsWith(q) && wert.length > 1) wert = wert.slice(1, -1);
|
||||||
|
werte[name] = wert;
|
||||||
|
}
|
||||||
|
return werte;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
apps: [{
|
||||||
|
name: 'aeromc-cf-proxy',
|
||||||
|
script: 'index.js',
|
||||||
|
cwd: __dirname,
|
||||||
|
|
||||||
|
// Fork-Betrieb mit genau einem Prozess. Im Cluster-Betrieb hätte jeder
|
||||||
|
// Arbeiter seine eigene Zählung – die Ratenbegrenzung wäre dann bei
|
||||||
|
// vier Prozessen viermal so hoch wie eingestellt.
|
||||||
|
exec_mode: 'fork',
|
||||||
|
instances: 1,
|
||||||
|
|
||||||
|
autorestart: true,
|
||||||
|
max_restarts: 10,
|
||||||
|
restart_delay: 3000,
|
||||||
|
max_memory_restart: '256M',
|
||||||
|
|
||||||
|
env: leseEnv(path.join(__dirname, '.env')),
|
||||||
|
}],
|
||||||
|
};
|
||||||
@@ -0,0 +1,344 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* AeroMC – CurseForge-Weiterleitung
|
||||||
|
* ---------------------------------
|
||||||
|
* Hält den CurseForge-API-Schlüssel und stellt die Anfragen stellvertretend
|
||||||
|
* für den Launcher. So bleibt der Schlüssel auf dem Server: in der verteilten
|
||||||
|
* .exe wäre er für jeden auslesbar, und CurseForge untersagt die Weitergabe
|
||||||
|
* an Dritte ausdrücklich.
|
||||||
|
*
|
||||||
|
* Wege:
|
||||||
|
* GET /health – Lebenszeichen, ohne Schlüsselangaben
|
||||||
|
* POST /v1/mods/files – Dateiliste auflösen (Rumpf: { fileIds: [...] })
|
||||||
|
* GET /v1/download?url=... – Mod-Datei durchreichen (CDN verlangt seit
|
||||||
|
* Juli 2026 ebenfalls einen Schlüssel)
|
||||||
|
*
|
||||||
|
* Einstellungen über Umgebungsvariablen, siehe .env.example.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const http = require('http');
|
||||||
|
const { Readable } = require('stream');
|
||||||
|
|
||||||
|
const KEY = (process.env.CF_API_KEY || '').trim();
|
||||||
|
const PORT = Number(process.env.PORT || 8420);
|
||||||
|
const HOST = process.env.HOST || '0.0.0.0';
|
||||||
|
/*
|
||||||
|
* Ein oder mehrere gültige Token, durch Komma getrennt.
|
||||||
|
*
|
||||||
|
* Mehrere deshalb, weil das Token in ausgelieferten Paketen steckt: Beim
|
||||||
|
* Wechsel trägst du eine Zeit lang altes und neues ein, damit ältere
|
||||||
|
* Installationen weiterlaufen, bis alle das Update haben. Danach das alte
|
||||||
|
* streichen.
|
||||||
|
*/
|
||||||
|
const TOKENS = new Set(
|
||||||
|
String(process.env.CF_PROXY_TOKEN || '')
|
||||||
|
.split(',').map((t) => t.trim()).filter(Boolean),
|
||||||
|
);
|
||||||
|
const LIMIT_PRO_MIN = Number(process.env.RATE_LIMIT_PER_MIN || 60);
|
||||||
|
const MAX_MB = Number(process.env.MAX_DOWNLOAD_MB || 512);
|
||||||
|
// Anzahl der Reverse-Proxies davor. 0 = keiner, 1 = z. B. Nginx Proxy Manager,
|
||||||
|
// 2 = z. B. Cloudflare vor dem Proxy Manager.
|
||||||
|
const PROXY_STUFEN = Math.max(0, Number(process.env.TRUST_PROXY || 0) || 0);
|
||||||
|
|
||||||
|
const CF_API = 'https://api.curseforge.com/v1';
|
||||||
|
|
||||||
|
// Ohne User-Agent weisen Dienste hinter Cloudflare Anfragen gerne mit 403 ab.
|
||||||
|
const UA = 'AeroMC-Launcher-Proxy/1.0 (+https://git.viper.ipv64.net)';
|
||||||
|
|
||||||
|
function cfKopfzeilen(weitere) {
|
||||||
|
return Object.assign({ 'x-api-key': KEY, Accept: 'application/json', 'User-Agent': UA }, weitere || {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nur diese Hosts dürfen durchgereicht werden. Ohne diese Liste wäre der
|
||||||
|
// Server ein offenes Tor in dein Netz – jemand könnte ihn Adressen im
|
||||||
|
// Heimnetz abrufen lassen und die Antwort mitlesen.
|
||||||
|
const ERLAUBTE_HOSTS = new Set([
|
||||||
|
'edge.forgecdn.net',
|
||||||
|
'mediafilez.forgecdn.net',
|
||||||
|
'media.forgecdn.net',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!KEY) {
|
||||||
|
console.error('CF_API_KEY fehlt. Bitte setzen (siehe .env.example) und neu starten.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Ratenbegrenzung: einfaches Zeitfenster je Absender
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const zaehler = new Map(); // ip -> { anzahl, bis }
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Ermittelt den Absender – die Grundlage der Ratenbegrenzung.
|
||||||
|
*
|
||||||
|
* nginx & Co. hängen die echte Adresse hinten an X-Forwarded-For an, statt
|
||||||
|
* den Kopfeintrag zu ersetzen. Alles davor kann der Aufrufer frei erfinden.
|
||||||
|
* Vertrauenswürdig ist deshalb nur der Eintrag, den die letzte eigene
|
||||||
|
* Proxy-Stufe geschrieben hat: von rechts gezählt.
|
||||||
|
*
|
||||||
|
* Aufrufer schickt "1.2.3.4", ein Proxy davor
|
||||||
|
* -> "1.2.3.4, <echte-IP>" -> mit TRUST_PROXY=1 zählt <echte-IP>
|
||||||
|
*/
|
||||||
|
function absender(req) {
|
||||||
|
if (PROXY_STUFEN > 0) {
|
||||||
|
const kette = String(req.headers['x-forwarded-for'] || '')
|
||||||
|
.split(',').map((s) => s.trim()).filter(Boolean);
|
||||||
|
const i = kette.length - PROXY_STUFEN;
|
||||||
|
if (i >= 0 && kette[i]) return kette[i];
|
||||||
|
// Kette kürzer als erwartet -> nicht dem Aufrufer glauben
|
||||||
|
}
|
||||||
|
return req.socket.remoteAddress || 'unbekannt';
|
||||||
|
}
|
||||||
|
|
||||||
|
function darfNoch(ip) {
|
||||||
|
if (LIMIT_PRO_MIN <= 0) return true;
|
||||||
|
const jetzt = Date.now();
|
||||||
|
const e = zaehler.get(ip);
|
||||||
|
if (!e || jetzt > e.bis) { zaehler.set(ip, { anzahl: 1, bis: jetzt + 60000 }); return true; }
|
||||||
|
e.anzahl++;
|
||||||
|
return e.anzahl <= LIMIT_PRO_MIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
// alte Einträge regelmäßig wegräumen, sonst wächst die Liste unbegrenzt
|
||||||
|
setInterval(() => {
|
||||||
|
const jetzt = Date.now();
|
||||||
|
for (const [ip, e] of zaehler) if (jetzt > e.bis) zaehler.delete(ip);
|
||||||
|
}, 60000).unref();
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Hilfen
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function sendeJson(res, code, daten) {
|
||||||
|
const koerper = JSON.stringify(daten);
|
||||||
|
res.writeHead(code, { 'Content-Type': 'application/json; charset=utf-8', 'Content-Length': Buffer.byteLength(koerper) });
|
||||||
|
res.end(koerper);
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokenOk(req) {
|
||||||
|
if (!TOKENS.size) return true;
|
||||||
|
return TOKENS.has(String(req.headers['x-aeromc-token'] || '').trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
function leseKoerper(req, maxBytes = 1_000_000) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let daten = '';
|
||||||
|
let laenge = 0;
|
||||||
|
req.on('data', (d) => {
|
||||||
|
laenge += d.length;
|
||||||
|
if (laenge > maxBytes) { reject(new Error('Anfrage zu groß')); req.destroy(); return; }
|
||||||
|
daten += d;
|
||||||
|
});
|
||||||
|
req.on('end', () => resolve(daten));
|
||||||
|
req.on('error', reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// prüft die Zieladresse einer Download-Anfrage
|
||||||
|
function pruefeDownloadZiel(roh) {
|
||||||
|
let u;
|
||||||
|
try { u = new URL(roh); } catch { return null; }
|
||||||
|
if (u.protocol !== 'https:') return null;
|
||||||
|
if (!ERLAUBTE_HOSTS.has(u.hostname)) return null;
|
||||||
|
return u;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Wege
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async function dateilisteAufloesen(req, res) {
|
||||||
|
let rumpf;
|
||||||
|
try { rumpf = JSON.parse(await leseKoerper(req)); }
|
||||||
|
catch { return sendeJson(res, 400, { error: 'Ungültiger Rumpf.' }); }
|
||||||
|
|
||||||
|
const ids = Array.isArray(rumpf && rumpf.fileIds) ? rumpf.fileIds.map(Number).filter(Boolean) : [];
|
||||||
|
if (!ids.length) return sendeJson(res, 400, { error: 'fileIds fehlen.' });
|
||||||
|
if (ids.length > 100) return sendeJson(res, 400, { error: 'Höchstens 100 fileIds je Anfrage.' });
|
||||||
|
|
||||||
|
const antwort = await fetch(CF_API + '/mods/files', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: cfKopfzeilen({ 'Content-Type': 'application/json' }),
|
||||||
|
body: JSON.stringify({ fileIds: ids }),
|
||||||
|
});
|
||||||
|
if (!antwort.ok) {
|
||||||
|
// Schlüsselprobleme nicht im Wortlaut weiterreichen
|
||||||
|
const code = antwort.status === 401 || antwort.status === 403 ? 502 : antwort.status;
|
||||||
|
return sendeJson(res, code, { error: 'CurseForge antwortet mit HTTP ' + antwort.status });
|
||||||
|
}
|
||||||
|
sendeJson(res, 200, await antwort.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dateiDurchreichen(req, res, url) {
|
||||||
|
const ziel = pruefeDownloadZiel(url.searchParams.get('url') || '');
|
||||||
|
if (!ziel) return sendeJson(res, 400, { error: 'Adresse fehlt oder zeigt nicht auf das CurseForge-CDN.' });
|
||||||
|
|
||||||
|
// Weiterleitungen selbst verfolgen und jedes Mal den Host neu prüfen
|
||||||
|
let aktuell = ziel;
|
||||||
|
let antwort = null;
|
||||||
|
for (let sprung = 0; sprung < 4; sprung++) {
|
||||||
|
antwort = await fetch(aktuell, { headers: cfKopfzeilen(), redirect: 'manual' });
|
||||||
|
const weiter = antwort.headers.get('location');
|
||||||
|
if (antwort.status >= 300 && antwort.status < 400 && weiter) {
|
||||||
|
const naechste = pruefeDownloadZiel(new URL(weiter, aktuell).toString());
|
||||||
|
if (!naechste) return sendeJson(res, 502, { error: 'Weiterleitung führt aus dem CurseForge-CDN heraus.' });
|
||||||
|
aktuell = naechste;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!antwort || !antwort.ok) {
|
||||||
|
return sendeJson(res, antwort ? antwort.status : 502, { error: 'Download fehlgeschlagen.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const laenge = Number(antwort.headers.get('content-length')) || 0;
|
||||||
|
if (MAX_MB > 0 && laenge > MAX_MB * 1024 * 1024) {
|
||||||
|
return sendeJson(res, 413, { error: `Datei größer als erlaubt (${MAX_MB} MB).` });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': antwort.headers.get('content-type') || 'application/octet-stream',
|
||||||
|
...(laenge ? { 'Content-Length': String(laenge) } : {}),
|
||||||
|
});
|
||||||
|
Readable.fromWeb(antwort.body).pipe(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const server = http.createServer(async (req, res) => {
|
||||||
|
let url;
|
||||||
|
try { url = new URL(req.url, 'http://localhost'); }
|
||||||
|
catch { return sendeJson(res, 400, { error: 'Ungültige Anfrage.' }); }
|
||||||
|
|
||||||
|
if (url.pathname === '/health') {
|
||||||
|
return sendeJson(res, 200, { ok: true, dienst: 'aeromc-cf-proxy' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tokenOk(req)) return sendeJson(res, 401, { error: 'Nicht berechtigt.' });
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Hilfe beim Einstellen von TRUST_PROXY: zeigt, was der Dienst von DIESER
|
||||||
|
* Anfrage sieht. Steht bewusst hinter der Token-Prüfung, damit Scanner
|
||||||
|
* nichts über den Aufbau erfahren. Aufruf von außen:
|
||||||
|
*
|
||||||
|
* curl -s -H 'x-aeromc-token: DEIN-TOKEN' https://deine-adresse/whoami
|
||||||
|
*
|
||||||
|
* erkannt == deine echte öffentliche IP -> TRUST_PROXY stimmt
|
||||||
|
* erkannt == eine interne Adresse -> zu niedrig
|
||||||
|
* erkannt == eine erfundene Adresse -> zu hoch
|
||||||
|
*
|
||||||
|
* Vor der Ratenbegrenzung, damit die Diagnose auch dann noch antwortet,
|
||||||
|
* wenn man sich gerade selbst ausgesperrt hat.
|
||||||
|
*/
|
||||||
|
if (url.pathname === '/whoami') {
|
||||||
|
const kette = String(req.headers['x-forwarded-for'] || '')
|
||||||
|
.split(',').map((s) => s.trim()).filter(Boolean);
|
||||||
|
return sendeJson(res, 200, {
|
||||||
|
erkannt: absender(req),
|
||||||
|
proxyStufen: PROXY_STUFEN,
|
||||||
|
kette,
|
||||||
|
verbindungVon: req.socket.remoteAddress || null,
|
||||||
|
hinweis: 'Stimmt "erkannt" mit deiner echten öffentlichen IP überein, ist TRUST_PROXY richtig gesetzt.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Selbsttest: prüft den API-Schlüssel, den DIESER Prozess wirklich geladen
|
||||||
|
* hat, direkt gegen CurseForge. Beantwortet die zwei Fragen, die man von
|
||||||
|
* außen sonst nicht auseinanderhalten kann:
|
||||||
|
* - hat der Prozess überhaupt den neuen Schlüssel?
|
||||||
|
* - akzeptiert CurseForge ihn?
|
||||||
|
* Gibt nur Länge und die ersten Zeichen aus (bei CurseForge immer "$2a$10$"),
|
||||||
|
* niemals den Schlüssel selbst.
|
||||||
|
*/
|
||||||
|
if (url.pathname === '/selftest') {
|
||||||
|
const roh = process.env.CF_API_KEY || '';
|
||||||
|
const befund = {
|
||||||
|
schluessel: {
|
||||||
|
laenge: KEY.length,
|
||||||
|
beginntMit: KEY.slice(0, 7),
|
||||||
|
randzeichenEntfernt: roh !== KEY,
|
||||||
|
sahAusWieCurseForge: /^\$2[aby]\$\d\d\$/.test(KEY),
|
||||||
|
},
|
||||||
|
curseforge: null,
|
||||||
|
urteil: '',
|
||||||
|
};
|
||||||
|
/*
|
||||||
|
* Mehrere Zugänge nacheinander prüfen. Erst der Vergleich zeigt, ob der
|
||||||
|
* Schlüssel grundsätzlich abgelehnt wird oder nur für bestimmte Bereiche
|
||||||
|
* fehlt – ein einzelner Aufruf kann das nicht unterscheiden.
|
||||||
|
*/
|
||||||
|
const PROBEN = [
|
||||||
|
{ name: 'games – Liste aller Spiele', method: 'GET', pfad: '/games' },
|
||||||
|
{ name: 'games/432 – Minecraft', method: 'GET', pfad: '/games/432' },
|
||||||
|
{ name: 'categories – Minecraft-Kategorien', method: 'GET', pfad: '/categories?gameId=432' },
|
||||||
|
{ name: 'mods/search – Modsuche', method: 'GET', pfad: '/mods/search?gameId=432&searchFilter=jei' },
|
||||||
|
{ name: 'mods/files – was der Import braucht', method: 'POST', pfad: '/mods/files', body: { fileIds: [4959679] } },
|
||||||
|
];
|
||||||
|
|
||||||
|
befund.proben = [];
|
||||||
|
for (const p of PROBEN) {
|
||||||
|
try {
|
||||||
|
const a = await fetch(CF_API + p.pfad, {
|
||||||
|
method: p.method,
|
||||||
|
headers: cfKopfzeilen(p.body ? { 'Content-Type': 'application/json' } : undefined),
|
||||||
|
body: p.body ? JSON.stringify(p.body) : undefined,
|
||||||
|
});
|
||||||
|
let text = '';
|
||||||
|
try { text = (await a.text()).replace(/\s+/g, ' ').trim().slice(0, 160); } catch { /* egal */ }
|
||||||
|
befund.proben.push({
|
||||||
|
name: p.name,
|
||||||
|
status: a.status,
|
||||||
|
server: a.headers.get('server') || null,
|
||||||
|
antwortAnfang: a.ok ? '(ok)' : text,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
befund.proben.push({ name: p.name, fehler: err.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stati = befund.proben.map((p) => p.status);
|
||||||
|
const alleAbgelehnt = stati.every((s) => s === 401 || s === 403);
|
||||||
|
const alleGut = stati.every((s) => s >= 200 && s < 300);
|
||||||
|
const nurDateien = befund.proben.find((p) => p.name.startsWith('mods/files'));
|
||||||
|
|
||||||
|
if (alleGut) befund.urteil = 'Alle Zugänge offen – der Schlüssel ist in Ordnung.';
|
||||||
|
else if (alleAbgelehnt) {
|
||||||
|
befund.urteil = 'Kein einziger Zugang offen: Dieser Schlüssel gilt bei CurseForge nicht. '
|
||||||
|
+ 'Entweder wurde er nach dem Kopieren erneut erzeugt („Regenerate" macht den alten sofort ungültig), '
|
||||||
|
+ 'oder das Organisationskonto ist nicht freigeschaltet. Kein Fehler im Launcher oder auf diesem Server.';
|
||||||
|
} else if (nurDateien && (nurDateien.status === 401 || nurDateien.status === 403)) {
|
||||||
|
befund.urteil = 'Der Schlüssel gilt grundsätzlich, aber nicht für die Dateiabfrage. '
|
||||||
|
+ 'Das deutet auf einen Schlüssel, der nur für ein einzelnes Spiel gilt – für Minecraft-Modpacks '
|
||||||
|
+ 'wird der Organisationsschlüssel gebraucht.';
|
||||||
|
} else befund.urteil = 'Gemischtes Bild – siehe die einzelnen Proben.';
|
||||||
|
|
||||||
|
// Kurzfassung für den schnellen Blick
|
||||||
|
befund.kurz = befund.proben.map((p) => `${p.name}: ${p.status ?? p.fehler}`);
|
||||||
|
return sendeJson(res, 200, befund);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!darfNoch(absender(req))) return sendeJson(res, 429, { error: 'Zu viele Anfragen, bitte kurz warten.' });
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (req.method === 'POST' && url.pathname === '/v1/mods/files') return await dateilisteAufloesen(req, res);
|
||||||
|
if (req.method === 'GET' && url.pathname === '/v1/download') return await dateiDurchreichen(req, res, url);
|
||||||
|
sendeJson(res, 404, { error: 'Unbekannter Weg.' });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Fehler]', err.message);
|
||||||
|
if (!res.headersSent) sendeJson(res, 500, { error: 'Interner Fehler.' });
|
||||||
|
else res.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(PORT, HOST, () => {
|
||||||
|
console.log(`AeroMC CurseForge-Weiterleitung läuft auf ${HOST}:${PORT}`);
|
||||||
|
console.log(` Ratenbegrenzung: ${LIMIT_PRO_MIN > 0 ? LIMIT_PRO_MIN + '/Minute je Absender' : 'aus'}`);
|
||||||
|
// niemals die Token selbst ausgeben – Protokolle wandern schnell woanders hin
|
||||||
|
console.log(` Token verlangt : ${TOKENS.size ? 'ja (' + TOKENS.size + ' gültige)' : 'nein'}`);
|
||||||
|
console.log(` Proxy-Stufen : ${PROXY_STUFEN} (${PROXY_STUFEN ? 'Absender aus X-Forwarded-For' : 'Absender aus der Verbindung'})`);
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = { server, pruefeDownloadZiel, darfNoch, absender, ERLAUBTE_HOSTS };
|
||||||
|
After Width: | Height: | Size: 484 B |
|
After Width: | Height: | Size: 542 B |
|
After Width: | Height: | Size: 211 B |
|
After Width: | Height: | Size: 536 B |
|
After Width: | Height: | Size: 572 B |
@@ -0,0 +1,452 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const archiver = require('archiver');
|
||||||
|
const fs = require('fs');
|
||||||
|
const os = require('os');
|
||||||
|
const path = require('path');
|
||||||
|
const { pipeline } = require('stream/promises');
|
||||||
|
const unzipper = require('unzipper');
|
||||||
|
|
||||||
|
function pad2(value) {
|
||||||
|
return String(value).padStart(2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultBackupFileName(date = new Date()) {
|
||||||
|
return 'AeroMC-Backup-' +
|
||||||
|
date.getFullYear() + '-' +
|
||||||
|
pad2(date.getMonth() + 1) + '-' +
|
||||||
|
pad2(date.getDate()) + '_' +
|
||||||
|
pad2(date.getHours()) + '-' +
|
||||||
|
pad2(date.getMinutes()) + '-' +
|
||||||
|
pad2(date.getSeconds()) +
|
||||||
|
'.zip';
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultBackupDir(documentsDir) {
|
||||||
|
return path.join(documentsDir, 'AeroMC Launcher', 'Backups');
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAutoBackupPath(documentsDir, date = new Date()) {
|
||||||
|
const dir = defaultBackupDir(documentsDir);
|
||||||
|
ensureDir(dir);
|
||||||
|
|
||||||
|
const baseName = defaultBackupFileName(date);
|
||||||
|
const ext = path.extname(baseName);
|
||||||
|
const stem = baseName.slice(0, -ext.length);
|
||||||
|
let attempt = 0;
|
||||||
|
let candidate = path.join(dir, baseName);
|
||||||
|
|
||||||
|
while (fs.existsSync(candidate)) {
|
||||||
|
attempt += 1;
|
||||||
|
candidate = path.join(dir, `${stem}-${attempt}${ext}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
function listGlobalBackups(documentsDir) {
|
||||||
|
const dir = defaultBackupDir(documentsDir);
|
||||||
|
ensureDir(dir);
|
||||||
|
|
||||||
|
return fs.readdirSync(dir, { withFileTypes: true })
|
||||||
|
.filter((entry) => entry.isFile() && /\.zip$/i.test(entry.name) && !/\.partial\.zip$/i.test(entry.name))
|
||||||
|
.map((entry) => {
|
||||||
|
const filePath = path.join(dir, entry.name);
|
||||||
|
const stat = fs.statSync(filePath);
|
||||||
|
return {
|
||||||
|
name: entry.name,
|
||||||
|
path: filePath,
|
||||||
|
size: stat.size,
|
||||||
|
modifiedAt: stat.mtime.toISOString(),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
const timeDiff = new Date(b.modifiedAt).getTime() - new Date(a.modifiedAt).getTime();
|
||||||
|
return timeDiff || a.name.localeCompare(b.name, 'de');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureDir(dir) {
|
||||||
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function countInstanceDirs(instancesDir) {
|
||||||
|
if (!fs.existsSync(instancesDir)) return 0;
|
||||||
|
return fs.readdirSync(instancesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readJson(file, fallback) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
|
||||||
|
} catch {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeJson(file, data) {
|
||||||
|
ensureDir(path.dirname(file));
|
||||||
|
fs.writeFileSync(file, JSON.stringify(data, null, 2), 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitProgress(cb, mode, percent, detail) {
|
||||||
|
if (!cb) return;
|
||||||
|
cb({ mode, percent: Math.max(0, Math.min(100, Math.round(percent))), detail: detail || '' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeZipEntryPath(entryPath) {
|
||||||
|
const parts = String(entryPath || '')
|
||||||
|
.replace(/\\/g, '/')
|
||||||
|
.split('/')
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
if (!parts.length) return '';
|
||||||
|
if (parts.some((part) => part === '.' || part === '..')) {
|
||||||
|
throw new Error('Das Backup-Archiv enthält ungültige Pfade.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.join(...parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractBackupArchive(sourcePath, destinationDir, options) {
|
||||||
|
const opts = options || {};
|
||||||
|
const onProgress = opts.onProgress;
|
||||||
|
const mode = opts.mode || 'restore';
|
||||||
|
const startPercent = opts.startPercent ?? 5;
|
||||||
|
const endPercent = opts.endPercent ?? 18;
|
||||||
|
const detail = opts.detail || 'Entpacke Backup';
|
||||||
|
const directory = await unzipper.Open.file(sourcePath);
|
||||||
|
const files = directory.files || [];
|
||||||
|
const fileEntries = files.filter((entry) => entry.type !== 'Directory');
|
||||||
|
const totalBytes = fileEntries.reduce((sum, entry) => sum + Number(entry.uncompressedSize || 0), 0);
|
||||||
|
let processedBytes = 0;
|
||||||
|
let processedEntries = 0;
|
||||||
|
|
||||||
|
ensureDir(destinationDir);
|
||||||
|
if (!files.length) {
|
||||||
|
emitProgress(onProgress, mode, endPercent, detail + ' abgeschlossen');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of files) {
|
||||||
|
const relativePath = normalizeZipEntryPath(entry.path);
|
||||||
|
if (!relativePath) continue;
|
||||||
|
|
||||||
|
const targetPath = path.join(destinationDir, relativePath);
|
||||||
|
if (entry.type === 'Directory') {
|
||||||
|
ensureDir(targetPath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureDir(path.dirname(targetPath));
|
||||||
|
await pipeline(entry.stream(), fs.createWriteStream(targetPath));
|
||||||
|
|
||||||
|
processedBytes += Number(entry.uncompressedSize || 0);
|
||||||
|
processedEntries += 1;
|
||||||
|
const ratio = totalBytes > 0
|
||||||
|
? Math.max(0, Math.min(1, processedBytes / totalBytes))
|
||||||
|
: Math.max(0, Math.min(1, processedEntries / Math.max(1, fileEntries.length)));
|
||||||
|
const percent = startPercent + ((endPercent - startPercent) * ratio);
|
||||||
|
emitProgress(onProgress, mode, percent, detail + ' ...');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBackupArchive(options) {
|
||||||
|
const opts = options || {};
|
||||||
|
const instancesDir = opts.instancesDir;
|
||||||
|
const settingsFile = opts.settingsFile;
|
||||||
|
const includeSettings = opts.includeSettings !== false;
|
||||||
|
const backupInfo = opts.backupInfo || {};
|
||||||
|
const destinationPath = opts.destinationPath;
|
||||||
|
const onProgress = opts.onProgress;
|
||||||
|
const mode = opts.mode || 'create';
|
||||||
|
const startPercent = opts.startPercent ?? 15;
|
||||||
|
const endPercent = opts.endPercent ?? 98;
|
||||||
|
const detail = opts.detail || 'Packe ZIP-Archiv';
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
ensureDir(path.dirname(destinationPath));
|
||||||
|
|
||||||
|
const output = fs.createWriteStream(destinationPath);
|
||||||
|
const archive = archiver('zip', { store: true });
|
||||||
|
let done = false;
|
||||||
|
|
||||||
|
const fail = (err) => {
|
||||||
|
if (done) return;
|
||||||
|
done = true;
|
||||||
|
try { archive.destroy(); } catch { /* ignore */ }
|
||||||
|
try { output.destroy(); } catch { /* ignore */ }
|
||||||
|
reject(err);
|
||||||
|
};
|
||||||
|
|
||||||
|
output.on('close', () => {
|
||||||
|
if (done) return;
|
||||||
|
done = true;
|
||||||
|
resolve({ bytes: archive.pointer() });
|
||||||
|
});
|
||||||
|
output.on('error', fail);
|
||||||
|
archive.on('error', fail);
|
||||||
|
archive.on('warning', (err) => {
|
||||||
|
if (err && err.code === 'ENOENT') return;
|
||||||
|
fail(err);
|
||||||
|
});
|
||||||
|
archive.on('progress', (progress) => {
|
||||||
|
const totalBytes = progress && progress.fs ? progress.fs.totalBytes : 0;
|
||||||
|
const processedBytes = progress && progress.fs ? progress.fs.processedBytes : 0;
|
||||||
|
if (!totalBytes) return;
|
||||||
|
|
||||||
|
const ratio = Math.max(0, Math.min(1, processedBytes / totalBytes));
|
||||||
|
const percent = startPercent + ((endPercent - startPercent) * ratio);
|
||||||
|
emitProgress(onProgress, mode, percent, detail + ' ...');
|
||||||
|
});
|
||||||
|
|
||||||
|
archive.pipe(output);
|
||||||
|
if (fs.existsSync(instancesDir)) archive.directory(instancesDir, 'instances');
|
||||||
|
else archive.append('', { name: 'instances/.keep' });
|
||||||
|
|
||||||
|
if (includeSettings && settingsFile && fs.existsSync(settingsFile)) {
|
||||||
|
archive.file(settingsFile, { name: 'settings.json' });
|
||||||
|
}
|
||||||
|
|
||||||
|
archive.append(`${JSON.stringify(backupInfo, null, 2)}\n`, { name: 'backup-info.json' });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const finalizeResult = archive.finalize();
|
||||||
|
if (finalizeResult && typeof finalizeResult.catch === 'function') finalizeResult.catch(fail);
|
||||||
|
} catch (err) {
|
||||||
|
fail(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyDirContentsTracked(sourceDir, targetDir, options) {
|
||||||
|
const opts = options || {};
|
||||||
|
const mode = opts.mode || 'create';
|
||||||
|
const onProgress = opts.onProgress;
|
||||||
|
const startPercent = opts.startPercent ?? 0;
|
||||||
|
const endPercent = opts.endPercent ?? 100;
|
||||||
|
const detailPrefix = opts.detailPrefix || 'Kopiere';
|
||||||
|
|
||||||
|
ensureDir(targetDir);
|
||||||
|
if (!fs.existsSync(sourceDir)) {
|
||||||
|
emitProgress(onProgress, mode, endPercent, detailPrefix + ' abgeschlossen');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
|
||||||
|
if (!entries.length) {
|
||||||
|
emitProgress(onProgress, mode, endPercent, detailPrefix + ' abgeschlossen');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.forEach((entry, index) => {
|
||||||
|
fs.cpSync(path.join(sourceDir, entry.name), path.join(targetDir, entry.name), { recursive: true });
|
||||||
|
const ratio = (index + 1) / entries.length;
|
||||||
|
const percent = startPercent + ((endPercent - startPercent) * ratio);
|
||||||
|
emitProgress(onProgress, mode, percent, `${detailPrefix}: ${entry.name}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveDirTracked(sourceDir, targetDir, options) {
|
||||||
|
const opts = options || {};
|
||||||
|
const mode = opts.mode || 'restore';
|
||||||
|
const onProgress = opts.onProgress;
|
||||||
|
const startPercent = opts.startPercent ?? 0;
|
||||||
|
const endPercent = opts.endPercent ?? 100;
|
||||||
|
const detailPrefix = opts.detailPrefix || 'Verschiebe';
|
||||||
|
|
||||||
|
if (!fs.existsSync(sourceDir)) {
|
||||||
|
emitProgress(onProgress, mode, endPercent, detailPrefix + ' abgeschlossen');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureDir(path.dirname(targetDir));
|
||||||
|
try {
|
||||||
|
fs.renameSync(sourceDir, targetDir);
|
||||||
|
emitProgress(onProgress, mode, endPercent, detailPrefix + ' abgeschlossen');
|
||||||
|
return;
|
||||||
|
} catch (err) {
|
||||||
|
if (!err || !['EXDEV', 'EPERM', 'EACCES'].includes(err.code)) throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
copyDirContentsTracked(sourceDir, targetDir, opts);
|
||||||
|
try { fs.rmSync(sourceDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveExtractedBackupRoot(extractDir) {
|
||||||
|
if (fs.existsSync(path.join(extractDir, 'instances'))) return extractDir;
|
||||||
|
const entries = fs.readdirSync(extractDir, { withFileTypes: true }).filter((entry) => entry.isDirectory());
|
||||||
|
if (entries.length === 1) {
|
||||||
|
const nested = path.join(extractDir, entries[0].name);
|
||||||
|
if (fs.existsSync(path.join(nested, 'instances'))) return nested;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createGlobalBackup(options) {
|
||||||
|
const instancesDir = options && options.instancesDir;
|
||||||
|
const userDataDir = options && options.userDataDir;
|
||||||
|
const destinationPath = options && options.destinationPath;
|
||||||
|
const includeSettings = !options || options.includeSettings !== false;
|
||||||
|
const onProgress = options && options.onProgress;
|
||||||
|
if (!instancesDir || !userDataDir || !destinationPath) throw new Error('Backup-Parameter unvollständig.');
|
||||||
|
|
||||||
|
const settingsFile = path.join(userDataDir, 'settings.json');
|
||||||
|
const workingArchivePath = destinationPath.replace(/\.zip$/i, '') + '.partial.zip';
|
||||||
|
const backupInfo = {
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
instanceCount: countInstanceDirs(instancesDir),
|
||||||
|
includeSettings,
|
||||||
|
app: 'AeroMC',
|
||||||
|
version: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
emitProgress(onProgress, 'create', 5, 'Bereite Backup vor');
|
||||||
|
emitProgress(onProgress, 'create', 10, 'Ermittle Backup-Inhalt');
|
||||||
|
|
||||||
|
if (fs.existsSync(destinationPath)) fs.unlinkSync(destinationPath);
|
||||||
|
if (fs.existsSync(workingArchivePath)) fs.unlinkSync(workingArchivePath);
|
||||||
|
emitProgress(onProgress, 'create', 90, 'Packe ZIP-Archiv');
|
||||||
|
await createBackupArchive({
|
||||||
|
instancesDir,
|
||||||
|
settingsFile,
|
||||||
|
includeSettings,
|
||||||
|
backupInfo,
|
||||||
|
destinationPath: workingArchivePath,
|
||||||
|
mode: 'create',
|
||||||
|
onProgress,
|
||||||
|
startPercent: 15,
|
||||||
|
endPercent: 98,
|
||||||
|
detail: 'Packe ZIP-Archiv',
|
||||||
|
});
|
||||||
|
emitProgress(onProgress, 'create', 99, 'Finalisiere Backup');
|
||||||
|
fs.renameSync(workingArchivePath, destinationPath);
|
||||||
|
emitProgress(onProgress, 'create', 100, 'Backup abgeschlossen');
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
path: destinationPath,
|
||||||
|
instanceCount: countInstanceDirs(instancesDir),
|
||||||
|
includedSettings: includeSettings && fs.existsSync(settingsFile),
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(workingArchivePath)) fs.rmSync(workingArchivePath, { force: true });
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restoreGlobalBackup(options) {
|
||||||
|
const instancesDir = options && options.instancesDir;
|
||||||
|
const userDataDir = options && options.userDataDir;
|
||||||
|
const sourcePath = options && options.sourcePath;
|
||||||
|
const includeSettings = !!(options && options.includeSettings);
|
||||||
|
const onProgress = options && options.onProgress;
|
||||||
|
const instancesDirSetting = options && Object.prototype.hasOwnProperty.call(options, 'instancesDirSetting')
|
||||||
|
? options.instancesDirSetting
|
||||||
|
: null;
|
||||||
|
if (!instancesDir || !userDataDir || !sourcePath) throw new Error('Restore-Parameter unvollständig.');
|
||||||
|
|
||||||
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'aeromc-restore-'));
|
||||||
|
const extractDir = path.join(tmp, 'extract');
|
||||||
|
const rollbackDir = path.join(tmp, 'rollback');
|
||||||
|
const rollbackInstances = path.join(rollbackDir, 'instances');
|
||||||
|
const settingsFile = path.join(userDataDir, 'settings.json');
|
||||||
|
const rollbackSettings = path.join(rollbackDir, 'settings.json');
|
||||||
|
let settingsPreviouslyExisted = false;
|
||||||
|
// wird erst true, sobald der aktuelle Stand tatsächlich weggesichert wurde -
|
||||||
|
// vorher darf der Rollback nichts anfassen, sonst löscht ein früher Fehler
|
||||||
|
// (z. B. ungültiges Archiv) den unveränderten, intakten Originalbestand
|
||||||
|
let originalMovedAway = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
ensureDir(extractDir);
|
||||||
|
emitProgress(onProgress, 'restore', 5, 'Entpacke Backup');
|
||||||
|
await extractBackupArchive(sourcePath, extractDir, {
|
||||||
|
mode: 'restore',
|
||||||
|
onProgress,
|
||||||
|
startPercent: 5,
|
||||||
|
endPercent: 18,
|
||||||
|
detail: 'Entpacke Backup',
|
||||||
|
});
|
||||||
|
|
||||||
|
const backupRoot = resolveExtractedBackupRoot(extractDir);
|
||||||
|
if (!backupRoot) throw new Error('Das Archiv enthält kein gültiges AeroMC-Backup.');
|
||||||
|
emitProgress(onProgress, 'restore', 18, 'Backup geprüft');
|
||||||
|
|
||||||
|
const sourceInstances = path.join(backupRoot, 'instances');
|
||||||
|
const sourceSettings = path.join(backupRoot, 'settings.json');
|
||||||
|
ensureDir(rollbackDir);
|
||||||
|
|
||||||
|
emitProgress(onProgress, 'restore', 25, 'Sichere aktuellen Stand');
|
||||||
|
moveDirTracked(instancesDir, rollbackInstances, {
|
||||||
|
mode: 'restore',
|
||||||
|
onProgress,
|
||||||
|
startPercent: 28,
|
||||||
|
endPercent: 40,
|
||||||
|
detailPrefix: 'Sichere aktuelle Instanzen',
|
||||||
|
});
|
||||||
|
originalMovedAway = true;
|
||||||
|
settingsPreviouslyExisted = fs.existsSync(settingsFile);
|
||||||
|
if (settingsPreviouslyExisted) fs.copyFileSync(settingsFile, rollbackSettings);
|
||||||
|
|
||||||
|
ensureDir(path.dirname(instancesDir));
|
||||||
|
moveDirTracked(sourceInstances, instancesDir, {
|
||||||
|
mode: 'restore',
|
||||||
|
onProgress,
|
||||||
|
startPercent: 45,
|
||||||
|
endPercent: 78,
|
||||||
|
detailPrefix: 'Stelle Instanzen wieder her',
|
||||||
|
});
|
||||||
|
|
||||||
|
let restoredSettings = false;
|
||||||
|
if (includeSettings && fs.existsSync(sourceSettings)) {
|
||||||
|
const restored = readJson(sourceSettings, {});
|
||||||
|
restored.instancesDir = instancesDirSetting;
|
||||||
|
writeJson(settingsFile, restored);
|
||||||
|
restoredSettings = true;
|
||||||
|
emitProgress(onProgress, 'restore', 90, 'Stelle Einstellungen wieder her');
|
||||||
|
} else {
|
||||||
|
emitProgress(onProgress, 'restore', 90, 'Einstellungen übersprungen');
|
||||||
|
}
|
||||||
|
|
||||||
|
emitProgress(onProgress, 'restore', 100, 'Wiederherstellen abgeschlossen');
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
path: sourcePath,
|
||||||
|
instanceCount: countInstanceDirs(instancesDir),
|
||||||
|
restoredSettings,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
// Nur zurückrollen, wenn wir den ursprünglichen Bestand überhaupt schon
|
||||||
|
// angefasst haben (siehe originalMovedAway oben). Schlägt die
|
||||||
|
// Wiederherstellung vorher fehl (ungültiges/beschädigtes Archiv o. Ä.),
|
||||||
|
// ist instancesDir noch der unveränderte Originalbestand - den lassen wir
|
||||||
|
// dann bewusst in Ruhe, statt ihn zu löschen.
|
||||||
|
if (originalMovedAway) {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(instancesDir)) fs.rmSync(instancesDir, { recursive: true, force: true });
|
||||||
|
if (fs.existsSync(rollbackInstances)) fs.cpSync(rollbackInstances, instancesDir, { recursive: true });
|
||||||
|
else ensureDir(instancesDir);
|
||||||
|
|
||||||
|
if (includeSettings) {
|
||||||
|
if (fs.existsSync(rollbackSettings)) fs.copyFileSync(rollbackSettings, settingsFile);
|
||||||
|
else if (!settingsPreviouslyExisted && fs.existsSync(settingsFile)) fs.rmSync(settingsFile, { force: true });
|
||||||
|
}
|
||||||
|
} catch { /* ignore rollback errors */ }
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createGlobalBackup,
|
||||||
|
restoreGlobalBackup,
|
||||||
|
defaultBackupFileName,
|
||||||
|
defaultBackupDir,
|
||||||
|
listGlobalBackups,
|
||||||
|
resolveAutoBackupPath,
|
||||||
|
};
|
||||||
@@ -14,7 +14,12 @@ const path = require('path');
|
|||||||
const { execFile, spawn } = require('child_process');
|
const { execFile, spawn } = require('child_process');
|
||||||
|
|
||||||
const PKG_FAMILY = 'Microsoft.MinecraftUWP_8wekyb3d8bbwe';
|
const PKG_FAMILY = 'Microsoft.MinecraftUWP_8wekyb3d8bbwe';
|
||||||
const APP_ID = PKG_FAMILY + '!App';
|
// Der Teil nach "!" (Application Id) ist NICHT stabil – Microsoft hat ihn
|
||||||
|
// schon mindestens einmal geändert (früher "App", inzwischen "Game").
|
||||||
|
// Deshalb zur Laufzeit über Get-StartApps auflösen statt fest zu verdrahten;
|
||||||
|
// dieser Wert dient nur als letzter Rückfallwert, falls die Auflösung
|
||||||
|
// fehlschlägt (z. B. PowerShell blockiert).
|
||||||
|
const FALLBACK_APP_ID = PKG_FAMILY + '!Game';
|
||||||
|
|
||||||
function comMojangDir() {
|
function comMojangDir() {
|
||||||
return path.join(process.env.LOCALAPPDATA || '', 'Packages', PKG_FAMILY,
|
return path.join(process.env.LOCALAPPDATA || '', 'Packages', PKG_FAMILY,
|
||||||
@@ -49,30 +54,87 @@ async function detect() {
|
|||||||
// Start
|
// Start
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
let cachedAppId = null;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Ermittelt die aktuell gültige AUMID über Get-StartApps – das ist derselbe
|
||||||
|
* Weg, den auch das Startmenü nutzt, und bildet daher immer die Application
|
||||||
|
* Id ab, die Windows gerade tatsächlich zum Starten akzeptiert.
|
||||||
|
*/
|
||||||
|
async function resolveAppId() {
|
||||||
|
if (cachedAppId) return cachedAppId;
|
||||||
|
const out = await runPwsh(
|
||||||
|
`(Get-StartApps | Where-Object { $_.AppID -like '${PKG_FAMILY}*' } `
|
||||||
|
+ `| Select-Object -First 1 -ExpandProperty AppID)`);
|
||||||
|
cachedAppId = out || FALLBACK_APP_ID;
|
||||||
|
return cachedAppId;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Startet Bedrock direkt.
|
* Startet Bedrock direkt.
|
||||||
* Wichtig: Der AppsFolder-Pfad darf NICHT in Anführungszeichen stehen – sonst
|
* Wichtig: Der AppsFolder-Pfad darf NICHT in Anführungszeichen stehen – sonst
|
||||||
* öffnet der Explorer nur ein Ordnerfenster. Daher windowsVerbatimArguments.
|
* öffnet der Explorer nur ein Ordnerfenster. Daher windowsVerbatimArguments.
|
||||||
*/
|
*/
|
||||||
function launch() {
|
async function launch() {
|
||||||
const target = 'shell:appsFolder\\' + APP_ID;
|
const appId = await resolveAppId();
|
||||||
|
const target = 'shell:appsFolder\\' + appId;
|
||||||
|
// Wichtig: Es darf pro Klick nur EINE Aktivierung tatsächlich lostreten.
|
||||||
|
// Die App-Aktivierung lässt sich nicht abbrechen – wenn wir bei einer
|
||||||
|
// Verzögerung (Xbox-Dienste, Kaltstart) automatisch einen zweiten Weg
|
||||||
|
// probieren, laufen im Zweifel beide an und Bedrock öffnet sich doppelt.
|
||||||
|
// Die anderen Wege dienen daher nur als Rückfall, falls das Spawnen
|
||||||
|
// selbst scheitert (Programm nicht gefunden o. Ä.), nicht als
|
||||||
|
// Reaktion auf einen bloß langsamen Start.
|
||||||
const versuche = [
|
const versuche = [
|
||||||
// 1) genau der Aufruf, der sich unter Windows als zuverlässig erwiesen hat
|
// 1) explorer direkt, ohne zusätzliche Anführungszeichen – entspricht
|
||||||
|
// exakt dem manuellen "explorer.exe shell:appsFolder\...", der
|
||||||
|
// nachweislich zuverlässig funktioniert
|
||||||
|
['explorer.exe', [target], { windowsVerbatimArguments: true }],
|
||||||
|
// 2) über die Eingabeaufforderung
|
||||||
|
['cmd', ['/c', 'start', '', target], { windowsVerbatimArguments: true }],
|
||||||
|
// 3) Rückfallweg über eine versteckte PowerShell – aus einem
|
||||||
|
// windowless/detached Kindprozess heraus offenbar nicht überall
|
||||||
|
// zuverlässig, daher nur letzter Versuch
|
||||||
['powershell', ['-NoProfile', '-WindowStyle', 'Hidden', '-Command',
|
['powershell', ['-NoProfile', '-WindowStyle', 'Hidden', '-Command',
|
||||||
`Start-Process explorer.exe -ArgumentList '${target}'`], {}],
|
`Start-Process explorer.exe -ArgumentList '${target}'`], {}],
|
||||||
// 2) explorer direkt, ohne zusätzliche Anführungszeichen
|
|
||||||
['explorer.exe', [target], { windowsVerbatimArguments: true }],
|
|
||||||
// 3) über die Eingabeaufforderung
|
|
||||||
['cmd', ['/c', 'start', '', target], { windowsVerbatimArguments: true }],
|
|
||||||
];
|
];
|
||||||
|
let methode = null;
|
||||||
for (const [cmd, args, extra] of versuche) {
|
for (const [cmd, args, extra] of versuche) {
|
||||||
try {
|
try {
|
||||||
const p = spawn(cmd, args, Object.assign({ detached: true, stdio: 'ignore', windowsHide: true }, extra));
|
const p = spawn(cmd, args, Object.assign({ detached: true, stdio: 'ignore', windowsHide: true }, extra));
|
||||||
p.unref();
|
p.unref();
|
||||||
return { ok: true, method: cmd };
|
methode = cmd;
|
||||||
} catch { /* nächsten Weg versuchen */ }
|
break; // nur diesen einen Weg tatsächlich auslösen
|
||||||
|
} catch { /* Spawnen selbst fehlgeschlagen – nächsten Weg versuchen */ }
|
||||||
|
}
|
||||||
|
if (!methode) return { ok: false, message: 'Bedrock konnte nicht gestartet werden.' };
|
||||||
|
// Aktivierung kann dauern (v. a. beim ersten Start nach längerer Pause) –
|
||||||
|
// hier nur zur ehrlichen Rückmeldung abwarten, OHNE bei Zeitüberschreitung
|
||||||
|
// einen weiteren Versuch zu starten (siehe Hinweis oben).
|
||||||
|
const bestaetigt = await waitForBedrockProcess(15000);
|
||||||
|
return { ok: true, method: methode, confirmed: bestaetigt };
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBedrockRunning() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
execFile('tasklist', ['/FI', 'IMAGENAME eq Minecraft.Windows.exe', '/FO', 'CSV', '/NH'],
|
||||||
|
{ windowsHide: true, timeout: 5000 },
|
||||||
|
(err, stdout) => resolve(!err && /Minecraft\.Windows\.exe/i.test(String(stdout || ''))));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function warten(ms) {
|
||||||
|
return new Promise((resolve) => { setTimeout(resolve, ms); });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForBedrockProcess(timeoutMs) {
|
||||||
|
const start = Date.now();
|
||||||
|
// sofort prüfen (falls schon ein Fenster lief) und danach im Kurztakt
|
||||||
|
for (;;) {
|
||||||
|
if (await isBedrockRunning()) return true;
|
||||||
|
if (Date.now() - start >= timeoutMs) return false;
|
||||||
|
await warten(500);
|
||||||
}
|
}
|
||||||
return { ok: false, message: 'Bedrock konnte nicht gestartet werden.' };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"curseforgeProxyUrl": "",
|
||||||
|
"curseforgeProxyToken": ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* cfimport.js – Instanzen aus dem CurseForge-Launcher übernehmen
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
* Unterstützt sowohl den Launcher-Wurzelordner als auch direkt den
|
||||||
|
* Instances-Ordner. Gelesen werden nach Möglichkeit minecraftinstance.json
|
||||||
|
* und die vorhandenen Spieldaten im Profilordner.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const os = require('os');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const META_FILES = ['minecraftinstance.json', 'instance.json'];
|
||||||
|
|
||||||
|
function readJson(file, fallback) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
|
||||||
|
} catch {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function existingMetaFile(dir) {
|
||||||
|
return META_FILES.map((name) => path.join(dir, name)).find((file) => fs.existsSync(file)) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function metaVersion(meta) {
|
||||||
|
return String(
|
||||||
|
(meta && (
|
||||||
|
meta.gameVersion || meta.minecraftVersion || meta.mcVersion ||
|
||||||
|
(meta.installedModpack && meta.installedModpack.gameVersion)
|
||||||
|
)) || ''
|
||||||
|
).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveInstancesDir(inputPath) {
|
||||||
|
if (!inputPath || !fs.existsSync(inputPath)) return null;
|
||||||
|
const directNames = new Set(['instances', 'minecraftinstances']);
|
||||||
|
if (directNames.has(path.basename(inputPath).toLowerCase())) return inputPath;
|
||||||
|
|
||||||
|
const candidates = [
|
||||||
|
path.join(inputPath, 'Instances'),
|
||||||
|
path.join(inputPath, 'instances'),
|
||||||
|
path.join(inputPath, 'minecraftInstances'),
|
||||||
|
path.join(inputPath, 'Minecraft', 'Instances'),
|
||||||
|
path.join(inputPath, 'minecraft', 'Instances'),
|
||||||
|
];
|
||||||
|
|
||||||
|
const looksLikeWindowsInstall =
|
||||||
|
/curseforge windows/i.test(inputPath) ||
|
||||||
|
(fs.existsSync(path.join(inputPath, 'CurseForge.exe')) && fs.existsSync(path.join(inputPath, 'resources')));
|
||||||
|
if (looksLikeWindowsInstall) {
|
||||||
|
candidates.push(
|
||||||
|
path.join(os.homedir(), 'curseforge', 'minecraft', 'Instances'),
|
||||||
|
path.join(os.homedir(), 'CurseForge', 'minecraft', 'Instances'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return candidates.find((candidate) => fs.existsSync(candidate)) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeLoaderVersion(loader, rawValue, mcVersion) {
|
||||||
|
let value = String(rawValue || '').trim();
|
||||||
|
if (!value) return '';
|
||||||
|
|
||||||
|
if (loader === 'forge') {
|
||||||
|
value = value.replace(/^forge-/i, '');
|
||||||
|
if (mcVersion && value.startsWith(mcVersion + '-')) value = value.slice(mcVersion.length + 1);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (loader === 'neoforge') {
|
||||||
|
return value.replace(/^neoforge-/i, '');
|
||||||
|
}
|
||||||
|
if (loader === 'fabric') {
|
||||||
|
value = value.replace(/^fabric(?:-loader)?-/i, '');
|
||||||
|
if (mcVersion && value.endsWith('-' + mcVersion)) value = value.slice(0, -1 * (mcVersion.length + 1));
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (loader === 'quilt') {
|
||||||
|
value = value.replace(/^quilt(?:-loader)?-/i, '');
|
||||||
|
if (mcVersion && value.endsWith('-' + mcVersion)) value = value.slice(0, -1 * (mcVersion.length + 1));
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectLoader(meta, mcVersion) {
|
||||||
|
const raw = meta && (
|
||||||
|
meta.baseModLoader || meta.modLoader || meta.modloader || meta.loader || meta.modLoaderId
|
||||||
|
);
|
||||||
|
const value = typeof raw === 'object'
|
||||||
|
? (raw.name || raw.id || raw.value || raw.version || '')
|
||||||
|
: (raw || '');
|
||||||
|
const lower = String(value).toLowerCase();
|
||||||
|
const objectVersion = raw && typeof raw === 'object'
|
||||||
|
? (raw.version || raw.name || raw.id || '')
|
||||||
|
: '';
|
||||||
|
|
||||||
|
if (!lower) return { loader: 'vanilla', loaderVersion: '' };
|
||||||
|
if (lower.includes('neoforge')) return { loader: 'neoforge', loaderVersion: normalizeLoaderVersion('neoforge', objectVersion || value, mcVersion) };
|
||||||
|
if (lower.includes('fabric')) return { loader: 'fabric', loaderVersion: normalizeLoaderVersion('fabric', objectVersion || value, mcVersion) };
|
||||||
|
if (lower.includes('quilt')) return { loader: 'quilt', loaderVersion: normalizeLoaderVersion('quilt', objectVersion || value, mcVersion) };
|
||||||
|
if (lower.includes('forge')) return { loader: 'forge', loaderVersion: normalizeLoaderVersion('forge', objectVersion || value, mcVersion) };
|
||||||
|
return { loader: 'vanilla', loaderVersion: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectVersion(meta, loaderVersion) {
|
||||||
|
return metaVersion(meta) || (loaderVersion && loaderVersion.includes('-') ? loaderVersion.split('-')[0] : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function findGameDir(dir) {
|
||||||
|
const nested = ['minecraft', '.minecraft']
|
||||||
|
.map((name) => path.join(dir, name))
|
||||||
|
.find((candidate) => fs.existsSync(candidate));
|
||||||
|
if (nested) return nested;
|
||||||
|
|
||||||
|
const markers = ['mods', 'config', 'resourcepacks', 'shaderpacks', 'saves', 'options.txt'];
|
||||||
|
return markers.some((name) => fs.existsSync(path.join(dir, name))) ? dir : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scan(inputPath) {
|
||||||
|
const instancesDir = resolveInstancesDir(inputPath);
|
||||||
|
if (!instancesDir) return { ok: false, reason: 'not-found' };
|
||||||
|
|
||||||
|
const list = [];
|
||||||
|
for (const entry of fs.readdirSync(instancesDir, { withFileTypes: true })) {
|
||||||
|
if (!entry.isDirectory()) continue;
|
||||||
|
const dir = path.join(instancesDir, entry.name);
|
||||||
|
const metaFile = existingMetaFile(dir);
|
||||||
|
const meta = metaFile ? readJson(metaFile, {}) : {};
|
||||||
|
const gameDir = findGameDir(dir);
|
||||||
|
if (!metaFile && !gameDir) continue;
|
||||||
|
|
||||||
|
const versionHint = metaVersion(meta);
|
||||||
|
const loaderInfo = detectLoader(meta, versionHint);
|
||||||
|
list.push({
|
||||||
|
folder: entry.name,
|
||||||
|
dir,
|
||||||
|
name: meta.name || meta.displayName || entry.name,
|
||||||
|
group: '',
|
||||||
|
notes: meta.notes || meta.summary || '',
|
||||||
|
version: versionHint || detectVersion(meta, loaderInfo.loaderVersion),
|
||||||
|
loader: loaderInfo.loader,
|
||||||
|
loaderVersion: loaderInfo.loaderVersion,
|
||||||
|
javaPath: String(meta.javaPath || meta.javaExecutable || '').trim(),
|
||||||
|
minMemMb: Number(meta.minimumMemory || meta.minMemory || meta.minMemAlloc) || null,
|
||||||
|
maxMemMb: Number(meta.maximumMemory || meta.maxMemory || meta.maxMemAlloc || meta.allocatedMemory) || null,
|
||||||
|
gameDir: gameDir || dir,
|
||||||
|
hasGameData: !!gameDir,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
list.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
return { ok: true, instancesDir, count: list.length, instances: list };
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyInto(sourceDir, targetDir) {
|
||||||
|
fs.mkdirSync(targetDir, { recursive: true });
|
||||||
|
for (const name of fs.readdirSync(sourceDir)) {
|
||||||
|
if (META_FILES.includes(name)) continue;
|
||||||
|
fs.cpSync(path.join(sourceDir, name), path.join(targetDir, name), { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function importOne(store, entry, copyData = true) {
|
||||||
|
const created = store.createOrReplaceImportedInstance({
|
||||||
|
name: entry.name,
|
||||||
|
group: entry.group,
|
||||||
|
notes: entry.notes,
|
||||||
|
minecraft: {
|
||||||
|
version: entry.version,
|
||||||
|
loader: entry.loader,
|
||||||
|
loaderVersion: entry.loaderVersion,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const patch = {};
|
||||||
|
if (entry.javaPath || entry.minMemMb || entry.maxMemMb) {
|
||||||
|
patch.java = {
|
||||||
|
path: entry.javaPath || '',
|
||||||
|
minMemMb: entry.minMemMb || null,
|
||||||
|
maxMemMb: entry.maxMemMb || null,
|
||||||
|
extraArgs: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (Object.keys(patch).length) store.updateInstance(created.id, patch);
|
||||||
|
|
||||||
|
let copied = false;
|
||||||
|
if (copyData && entry.gameDir && fs.existsSync(entry.gameDir)) {
|
||||||
|
copyInto(entry.gameDir, store.gameDir(created.id));
|
||||||
|
copied = true;
|
||||||
|
}
|
||||||
|
return { id: created.id, name: created.name, copied };
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { scan, importOne, resolveInstancesDir };
|
||||||
@@ -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 };
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* crashreports.js – Absturzberichte von Minecraft auswerten
|
||||||
|
* ----------------------------------------------------------
|
||||||
|
* Minecraft legt bei einem Absturz eine Datei unter
|
||||||
|
* <spielordner>/crash-reports/crash-<datum>-client.txt an. Die enthält
|
||||||
|
* hunderte Zeilen Systemangaben, aber nur wenige, die weiterhelfen.
|
||||||
|
*
|
||||||
|
* Dieses Modul sucht den neuesten Bericht und zieht daraus heraus:
|
||||||
|
* Beschreibung, Fehlerart, verdächtige Mods und die ersten Stellen im
|
||||||
|
* Aufrufverlauf.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const zlib = require('zlib');
|
||||||
|
|
||||||
|
// größere Berichte gibt es kaum; die Grenze schützt vor Ausreißern
|
||||||
|
const MAX_BYTES = 2 * 1024 * 1024;
|
||||||
|
|
||||||
|
function berichtsOrdner(gameDir) {
|
||||||
|
return path.join(gameDir, 'crash-reports');
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Neuester Bericht im Ordner. seit: nur Berichte ab diesem Zeitpunkt
|
||||||
|
* (Millisekunden) – sonst zeigt man nach einem sauberen Start den Absturz
|
||||||
|
* von vorgestern an.
|
||||||
|
*/
|
||||||
|
function neuesterBericht(gameDir, seit) {
|
||||||
|
const dir = berichtsOrdner(gameDir);
|
||||||
|
let eintraege = [];
|
||||||
|
try { eintraege = fs.readdirSync(dir, { withFileTypes: true }); } catch { return null; }
|
||||||
|
|
||||||
|
let bester = null;
|
||||||
|
for (const e of eintraege) {
|
||||||
|
if (!e.isFile() || !/^crash-.*\.txt$/i.test(e.name)) continue;
|
||||||
|
const voll = path.join(dir, e.name);
|
||||||
|
let st;
|
||||||
|
try { st = fs.statSync(voll); } catch { continue; }
|
||||||
|
if (seit && st.mtimeMs < seit) continue;
|
||||||
|
if (!bester || st.mtimeMs > bester.mtimeMs) bester = { pfad: voll, name: e.name, mtimeMs: st.mtimeMs, groesse: st.size };
|
||||||
|
}
|
||||||
|
return bester;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Zieht die aussagekräftigen Zeilen aus einem Bericht.
|
||||||
|
* -> { beschreibung, fehler, verdaechtigeMods, diagnose, empfehlung, verlauf: [] }
|
||||||
|
*/
|
||||||
|
function auswerten(text) {
|
||||||
|
const zeilen = String(text || '').split(/\r?\n/);
|
||||||
|
const befund = {
|
||||||
|
beschreibung: '',
|
||||||
|
fehler: '',
|
||||||
|
verdaechtigeMods: '',
|
||||||
|
diagnose: '',
|
||||||
|
empfehlung: '',
|
||||||
|
verlauf: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < zeilen.length; i++) {
|
||||||
|
const z = zeilen[i];
|
||||||
|
const roh = z.trim();
|
||||||
|
|
||||||
|
if (!befund.beschreibung && /^Description:/i.test(roh)) {
|
||||||
|
befund.beschreibung = roh.replace(/^Description:\s*/i, '').trim();
|
||||||
|
// Die Fehlerzeile steht direkt darunter, nach einer Leerzeile.
|
||||||
|
for (let j = i + 1; j < Math.min(i + 6, zeilen.length); j++) {
|
||||||
|
const k = zeilen[j].trim();
|
||||||
|
if (!k) continue;
|
||||||
|
befund.fehler = k;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forge/NeoForge nennen Verdächtige ausdrücklich
|
||||||
|
if (!befund.verdaechtigeMods && /^Suspected Mods?:/i.test(roh)) {
|
||||||
|
befund.verdaechtigeMods = roh.replace(/^Suspected Mods?:\s*/i, '').trim();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// die obersten Stellen im Aufrufverlauf – dort steht meist der Verursacher
|
||||||
|
if (befund.verlauf.length < 8 && /^at\s+\S/.test(roh)) {
|
||||||
|
befund.verlauf.push(roh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manche Berichte nennen keine Description, dafür eine Ausnahme weiter oben
|
||||||
|
if (!befund.fehler) {
|
||||||
|
const treffer = zeilen.find((z) => /^[\w.$]+(Exception|Error)(:|\s|$)/.test(z.trim()));
|
||||||
|
if (treffer) befund.fehler = treffer.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const volltext = zeilen.join('\n');
|
||||||
|
const shutdownWatchdog = /Description:\s*Client shutdown from post-main/i.test(volltext)
|
||||||
|
&& /ClientShutdownWatchdog/i.test(volltext);
|
||||||
|
const webMapJetty = /journeymap_webmap:\s*JourneyMap WebMap\b/i.test(volltext)
|
||||||
|
&& /JettyServerThreadPool/i.test(volltext)
|
||||||
|
&& /ServerConnector.*\{0\.0\.0\.0:\d+\}/i.test(volltext);
|
||||||
|
if (shutdownWatchdog && webMapJetty) {
|
||||||
|
const version = volltext.match(/journeymap_webmap:\s*JourneyMap WebMap\s+([^\s\r\n]+)/i);
|
||||||
|
const name = `JourneyMap WebMap${version ? ` ${version[1]}` : ''}`;
|
||||||
|
befund.verdaechtigeMods = name;
|
||||||
|
befund.diagnose = `${name} hat seinen eingebetteten Jetty-Webserver beim Beenden nicht gestoppt. `
|
||||||
|
+ 'Dessen nicht beendete Server-Threads haben den Minecraft-Shutdown-Watchdog ausgelöst.';
|
||||||
|
befund.empfehlung = 'AeroMC beendet einen nach dem regulären Client-Shutdown verbliebenen Java-Prozess nun '
|
||||||
|
+ 'nach einer kurzen Schonfrist automatisch. JourneyMap WebMap kann installiert und aktiviert bleiben.';
|
||||||
|
}
|
||||||
|
return befund;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Sucht und liest den neuesten Bericht.
|
||||||
|
* -> { ok: false, grund } oder { ok: true, datei, zeitpunkt, ...auswertung, text }
|
||||||
|
*/
|
||||||
|
function lies(gameDir, seit) {
|
||||||
|
const bericht = neuesterBericht(gameDir, seit);
|
||||||
|
if (!bericht) return { ok: false, grund: 'kein-bericht' };
|
||||||
|
if (bericht.groesse > MAX_BYTES) return { ok: false, grund: 'zu-gross', datei: bericht.name };
|
||||||
|
|
||||||
|
let text = '';
|
||||||
|
try { text = fs.readFileSync(bericht.pfad, 'utf8'); }
|
||||||
|
catch (err) { return { ok: false, grund: 'nicht-lesbar', message: err.message }; }
|
||||||
|
|
||||||
|
return Object.assign({
|
||||||
|
ok: true,
|
||||||
|
datei: bericht.name,
|
||||||
|
pfad: bericht.pfad,
|
||||||
|
zeitpunkt: new Date(bericht.mtimeMs).toISOString(),
|
||||||
|
text,
|
||||||
|
}, auswerten(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Logdateien der Instanz auflisten. Minecraft schreibt latest.log und legt
|
||||||
|
* ältere Läufe gepackt als <datum>-<nr>.log.gz daneben.
|
||||||
|
* -> [{ name, groesse, geaendert, gepackt }] – neueste zuerst
|
||||||
|
*/
|
||||||
|
function listeLogs(gameDir) {
|
||||||
|
const dir = path.join(gameDir, 'logs');
|
||||||
|
let eintraege = [];
|
||||||
|
try { eintraege = fs.readdirSync(dir, { withFileTypes: true }); } catch { return []; }
|
||||||
|
|
||||||
|
const logs = [];
|
||||||
|
for (const e of eintraege) {
|
||||||
|
if (!e.isFile()) continue;
|
||||||
|
const gepackt = /\.log\.gz$/i.test(e.name);
|
||||||
|
if (!gepackt && !/\.log$/i.test(e.name)) continue;
|
||||||
|
try {
|
||||||
|
const st = fs.statSync(path.join(dir, e.name));
|
||||||
|
logs.push({ name: e.name, groesse: st.size, geaendert: st.mtimeMs, gepackt });
|
||||||
|
} catch { /* überspringen */ }
|
||||||
|
}
|
||||||
|
// latest.log immer zuoberst, danach nach Datum
|
||||||
|
logs.sort((a, b) => {
|
||||||
|
if (a.name.toLowerCase() === 'latest.log') return -1;
|
||||||
|
if (b.name.toLowerCase() === 'latest.log') return 1;
|
||||||
|
return b.geaendert - a.geaendert;
|
||||||
|
});
|
||||||
|
return logs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Inhalt einer Logdatei. Gepackte werden entpackt.
|
||||||
|
* maxZeilen begrenzt die Rückgabe – ein Log kann zehntausende Zeilen haben,
|
||||||
|
* und die Oberfläche soll dabei nicht einfrieren.
|
||||||
|
*/
|
||||||
|
function liesLog(gameDir, name, maxZeilen = 5000) {
|
||||||
|
const dir = path.join(gameDir, 'logs');
|
||||||
|
const voll = path.join(dir, path.basename(String(name || '')));
|
||||||
|
if (path.dirname(voll) !== dir) return { ok: false, grund: 'bad-name' };
|
||||||
|
if (!fs.existsSync(voll)) return { ok: false, grund: 'nicht-gefunden' };
|
||||||
|
|
||||||
|
let text = '';
|
||||||
|
try {
|
||||||
|
if (/\.gz$/i.test(voll)) text = zlib.gunzipSync(fs.readFileSync(voll)).toString('utf8');
|
||||||
|
else text = fs.readFileSync(voll, 'utf8');
|
||||||
|
} catch (err) {
|
||||||
|
return { ok: false, grund: 'nicht-lesbar', message: err.message };
|
||||||
|
}
|
||||||
|
|
||||||
|
const zeilen = text.split(/\r?\n/);
|
||||||
|
const gekuerzt = zeilen.length > maxZeilen;
|
||||||
|
// bei langen Logs das Ende zeigen – dort steht, was zuletzt passierte
|
||||||
|
const sichtbar = gekuerzt ? zeilen.slice(zeilen.length - maxZeilen) : zeilen;
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
name: path.basename(voll),
|
||||||
|
zeilenGesamt: zeilen.length,
|
||||||
|
gekuerzt,
|
||||||
|
text: sichtbar.join('\n'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function logOrdner(gameDir) {
|
||||||
|
return path.join(gameDir, 'logs');
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { lies, auswerten, neuesterBericht, berichtsOrdner, listeLogs, liesLog, logOrdner };
|
||||||
@@ -0,0 +1,424 @@
|
|||||||
|
/*
|
||||||
|
* designs.css – wählbare Designs (Optionen → Darstellung → Design)
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
* Klassisch (hell/dunkel) steht in styles.css. Die Designs hier bauen
|
||||||
|
* auf den dunklen Regeln auf (data-theme="dark") und überschreiben sie über
|
||||||
|
* data-design – gleiche Spezifität, deshalb muss diese Datei NACH styles.css
|
||||||
|
* geladen werden.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ===================== Design-Auswahl in den Optionen ===================== */
|
||||||
|
.design-raster{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}
|
||||||
|
.settings-card-wide .design-raster{grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||||
|
@media (max-width:900px){.settings-card-wide .design-raster{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||||
|
.design-wahl{display:flex;flex-direction:column;gap:5px;padding:6px;border:1px solid var(--border);border-radius:8px;background:none;color:var(--text);font-family:inherit;font-size:12px;cursor:pointer;text-align:left}
|
||||||
|
.design-wahl:hover{border-color:var(--sel-br)}
|
||||||
|
.design-wahl.on{border-color:var(--sel-br);box-shadow:0 0 0 1px var(--sel-br);background:var(--sel)}
|
||||||
|
.design-wahl small{font-size:12px;font-weight:600;padding:0 2px}
|
||||||
|
.dv{position:relative;display:block;width:100%;align-self:stretch;height:58px;border-radius:5px;overflow:hidden;border:1px solid rgba(0,0,0,.25)}
|
||||||
|
.dv i,.dv b,.dv em{position:absolute;display:block}
|
||||||
|
.dv i{left:0;right:0;top:0;height:11px}
|
||||||
|
.dv em{top:11px;right:0;bottom:0;width:24%}
|
||||||
|
.dv b{left:10px;top:20px;width:22px;height:22px;border-radius:3px}
|
||||||
|
.dv-dark{background:#242426}.dv-dark i{background:#333337}.dv-dark em{background:#2d2d30;border-left:1px solid #3f3f46}.dv-dark b{background:#0e639c;border:1px solid #1177bb}
|
||||||
|
.dv-light{background:#e2e2e2}.dv-light i{background:#f0f0f0;border-bottom:1px solid #c4c7cc}.dv-light em{background:#f0f0f0;border-left:1px solid #c4c7cc}.dv-light b{background:#dce8f6;border:1px solid #3a6ea5}
|
||||||
|
.dv-aero{background:radial-gradient(60px 30px at 35% 10px,rgba(64,126,232,.55),transparent 70%),linear-gradient(to right,rgba(148,175,255,.09) 1px,transparent 1px) 0 0/9px 9px,linear-gradient(to bottom,rgba(148,175,255,.09) 1px,transparent 1px) 0 0/9px 9px,#05070d}
|
||||||
|
.dv-aero i{background:rgba(10,14,26,.95);border-bottom:1px solid rgba(148,175,255,.18)}
|
||||||
|
.dv-aero em{background:#0a0e1a;border-left:1px solid rgba(148,175,255,.18)}
|
||||||
|
.dv-aero b{border-radius:7px;background:linear-gradient(135deg,#8fd8ff,#407ee8 60%,#2658be);box-shadow:0 4px 12px -3px rgba(64,126,232,.9)}
|
||||||
|
.dv-lila{background:radial-gradient(60px 30px at 35% 10px,rgba(139,92,246,.6),transparent 70%),linear-gradient(to right,rgba(196,165,255,.09) 1px,transparent 1px) 0 0/9px 9px,linear-gradient(to bottom,rgba(196,165,255,.09) 1px,transparent 1px) 0 0/9px 9px,#08050f}
|
||||||
|
.dv-lila i{background:rgba(15,10,26,.95);border-bottom:1px solid rgba(196,165,255,.18)}
|
||||||
|
.dv-lila em{background:#0f0a1a;border-left:1px solid rgba(196,165,255,.18)}
|
||||||
|
.dv-lila b{border-radius:7px;background:linear-gradient(135deg,#e8beff,#8b5cf6 60%,#6d28d9);box-shadow:0 4px 12px -3px rgba(139,92,246,.9)}
|
||||||
|
.dv-mc{background:url(assets/design/erde-dunkel.png) 0 0/24px 24px;image-rendering:pixelated}
|
||||||
|
.dv-mc i{height:12px;background:url(assets/design/gras.png) 0 0/16px 4px repeat-x,url(assets/design/schiefer.png) 0 0/24px 24px;border-bottom:1px solid #000}
|
||||||
|
.dv-mc em{background:url(assets/design/schiefer.png) 0 0/24px 24px;border-left:1px solid #000}
|
||||||
|
.dv-mc b{border-radius:0;background:#3c8527;border:1px solid #000;box-shadow:inset 1px 1px 0 #6cc349,inset -1px -1px 0 #1d4d13}
|
||||||
|
|
||||||
|
/* ===================================================================== */
|
||||||
|
/* ============ Designs: AeroMC (Blau, wie die Webseite) & Nachtlila ===== */
|
||||||
|
/* ===================================================================== */
|
||||||
|
/* Beide Designs teilen sich alle Regeln unten; sie unterscheiden sich nur in
|
||||||
|
der Palette. --p-* ohne "#" sind RGB-Tripel, damit rgba(var(--p-…),.5) geht. */
|
||||||
|
:root[data-design="aero"]{
|
||||||
|
--p-akzent:64,126,232; --p-tief:38,88,190; --p-akzent2:104,196,250; --p-hell:143,216,255;
|
||||||
|
--p-linie:148,175,255; --p-win:10,14,26; --p-surface:13,20,40; --p-surface2:16,26,53; --p-schatten:2,4,10;
|
||||||
|
--p-akzent-hover:#4f8df0; --p-tief-hover:#2c63cc; --p-aktiv:#0b1226; --p-konsole:#03050a;
|
||||||
|
--win:#0a0e1a; --area:#05070d; --bar:#0d1428; --surface:#0d1428; --surface2:#101a35; --input:#070b16;
|
||||||
|
--text:#e8ecfb; --dim:#93a0c4; --link:#8fd8ff;
|
||||||
|
}
|
||||||
|
:root[data-design="lila"]{
|
||||||
|
--p-akzent:139,92,246; --p-tief:109,40,217; --p-akzent2:192,132,252; --p-hell:232,190,255;
|
||||||
|
--p-linie:196,165,255; --p-win:15,10,26; --p-surface:24,15,41; --p-surface2:32,20,56; --p-schatten:6,3,12;
|
||||||
|
--p-akzent-hover:#9d72ff; --p-tief-hover:#7c3aed; --p-aktiv:#140c24; --p-konsole:#050309;
|
||||||
|
--win:#0f0a1a; --area:#08050f; --bar:#180f29; --surface:#180f29; --surface2:#201438; --input:#0b0715;
|
||||||
|
--text:#f0eafb; --dim:#a99cc6; --link:#e8beff;
|
||||||
|
}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]){
|
||||||
|
--border:rgba(var(--p-linie),.14); --border2:rgba(var(--p-linie),.09);
|
||||||
|
--sel:rgba(var(--p-akzent),.20); --sel-br:var(--p-akzent-hover); --accent:rgb(var(--p-akzent));
|
||||||
|
--danger:#ff7a8a; --play:#4ade80;
|
||||||
|
--cyan:rgb(var(--p-hell)); --cyan-soft:rgb(var(--p-akzent2));
|
||||||
|
--stat-bar:rgb(var(--p-akzent2));
|
||||||
|
--titel-bg:var(--win); --titel-fg:var(--text);
|
||||||
|
}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) ::selection{background:rgb(var(--p-akzent));color:#fff}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .titel-text{font-weight:600;background:linear-gradient(120deg,rgb(var(--p-hell)),rgb(var(--p-akzent)) 70%);-webkit-background-clip:text;background-clip:text;color:transparent}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) body{background:var(--area)}
|
||||||
|
|
||||||
|
/* Werkzeugleiste */
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .toolbar{background:rgba(var(--p-win),.94);border-bottom:1px solid var(--border);padding:7px 8px;gap:2px}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .tbtn{border-radius:999px;padding:6px 10px;color:var(--text)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .tbtn:hover{background:var(--surface);border-color:var(--border)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .tbtn:active{background:var(--surface2)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .tbtn svg,
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) #btn-new svg,:root:is([data-design="aero"],[data-design="lila"]) #btn-import svg,:root:is([data-design="aero"],[data-design="lila"]) #btn-open-dir svg,
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) #btn-console svg,:root:is([data-design="aero"],[data-design="lila"]) #btn-account svg,:root:is([data-design="aero"],[data-design="lila"]) #btn-settings svg,
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .acct-av svg{color:var(--cyan-soft)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .tbsep{background:var(--border)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .search-wrap input{border-radius:999px;padding:5px 12px;width:150px}
|
||||||
|
|
||||||
|
/* Instanzliste: Raster und blaues Leuchten wie im Kopf der Webseite */
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .workspace{background:var(--area)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .list-area{background:
|
||||||
|
radial-gradient(520px 260px at 32% -60px,rgba(var(--p-akzent),.30),transparent 70%),
|
||||||
|
linear-gradient(to right,rgba(var(--p-linie),.05) 1px,transparent 1px) 0 0/44px 44px,
|
||||||
|
linear-gradient(to bottom,rgba(var(--p-linie),.05) 1px,transparent 1px) 0 0/44px 44px,
|
||||||
|
var(--area)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .group-header{color:var(--text);padding:8px 4px 4px}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .group-header .chev{color:var(--cyan-soft)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .group-header::after{background:linear-gradient(to right,rgba(var(--p-linie),.22),rgba(var(--p-linie),.04))}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .group-grid{gap:6px}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .card{border-radius:14px;padding:8px 4px 6px;transition:background .15s,border-color .15s,box-shadow .15s}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .card:hover{background:rgba(var(--p-surface),.85);border-color:var(--border)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .card.sel{background:linear-gradient(180deg,rgba(var(--p-akzent),.24),rgba(var(--p-tief),.14));border-color:rgba(var(--p-akzent2),.55);box-shadow:0 10px 28px -14px rgba(var(--p-akzent),.95),0 0 0 1px rgba(var(--p-hell),.08) inset}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .card.sel .card-name{background:linear-gradient(120deg,rgb(var(--p-akzent2)),rgb(var(--p-akzent)) 60%,rgb(var(--p-tief)));color:#fff;border-radius:999px}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .card-icon-img{border-radius:10px}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .card-taste{border-radius:999px;background:linear-gradient(120deg,rgb(var(--p-akzent2)),rgb(var(--p-akzent)))}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .empty h2{background:linear-gradient(120deg,rgb(var(--p-hell)),rgb(var(--p-akzent)) 60%,rgb(var(--p-tief)));-webkit-background-clip:text;background-clip:text;color:transparent;font-size:26px}
|
||||||
|
|
||||||
|
/* Seitenleiste */
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .side{background:var(--win);border-left:1px solid var(--border);padding:10px 6px}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .side-ico img{border-radius:12px}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .side-grp{color:var(--cyan-soft)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .hr{background:var(--border);margin:8px 4px}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .sbtn{border-radius:999px;margin-bottom:1px;padding:5px 2px}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .sbtn:hover{background:var(--surface);border-color:var(--border)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .sbtn.play{color:#fff;background:linear-gradient(180deg,rgb(var(--p-akzent)),rgb(var(--p-tief)));border-color:transparent;box-shadow:0 0 0 1px rgba(255,255,255,.08) inset,0 10px 24px -12px rgba(var(--p-akzent),.95);padding:7px 4px}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .sbtn.play:hover{background:linear-gradient(180deg,var(--p-akzent-hover),var(--p-tief-hover))}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .sbtn.del:hover{color:var(--danger);border-color:rgba(255,122,138,.35)}
|
||||||
|
|
||||||
|
/* Statusleiste */
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .statusbar{background:var(--win);border-top:1px solid var(--border)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .st-row{color:var(--dim)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .st-row2{background:var(--input);border-top-color:var(--border2)}
|
||||||
|
|
||||||
|
/* Dialoge */
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .modal{background:rgba(var(--p-schatten),.68);backdrop-filter:blur(3px)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .modal-card{background:var(--win);border:1px solid var(--border);border-radius:16px;box-shadow:0 30px 80px -20px rgba(0,0,0,.85),0 0 0 1px rgba(var(--p-hell),.04)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .modal-head{background:linear-gradient(180deg,rgba(var(--p-surface2),.9),rgba(var(--p-win),.9));border-bottom:1px solid var(--border);padding:12px 16px}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .modal-body{background:var(--win)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .modal-foot{background:var(--win);border-top:1px solid var(--border);padding:12px 16px}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .modal-close{color:var(--dim)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .modal-close:hover{color:var(--cyan)}
|
||||||
|
|
||||||
|
/* Eingabefelder */
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .search-wrap input,
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .field input,
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .field select,
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .field textarea,
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .mods-search input,
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .mods-search select,
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .mods-filters select,
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .mmc-bar select,
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .clip-length select,
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .path-row input,
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .se-farbzeile input[type=text],
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .se-feld select{background:var(--input);color:var(--text);border-color:var(--border);border-radius:10px}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .search-wrap input{border-radius:999px}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .search-wrap input:focus,
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .field input:focus,
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .field select:focus,
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .field textarea:focus,
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .mods-search input:focus,
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .path-row input:focus{border-color:var(--cyan-soft);box-shadow:0 0 0 3px rgba(var(--p-akzent2),.15)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) input[type=checkbox],:root:is([data-design="aero"],[data-design="lila"]) input[type=radio],:root:is([data-design="aero"],[data-design="lila"]) input[type=range]{accent-color:rgb(var(--p-akzent))}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .login-usercode{background:var(--input);color:var(--text);border-color:var(--border);border-radius:12px}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .upd-notes{background:var(--input);border-radius:10px}
|
||||||
|
|
||||||
|
/* Knöpfe – rund wie auf der Webseite */
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .btn{border-radius:999px;padding:7px 15px;background:var(--surface);border-color:var(--border);color:var(--text);transition:background .15s,border-color .15s,transform .15s}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .btn:hover{background:var(--surface2);border-color:rgba(var(--p-linie),.32)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .btn:active{background:var(--p-aktiv)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .btn-primary{background:linear-gradient(180deg,rgb(var(--p-akzent)),rgb(var(--p-tief)));border-color:transparent;color:#fff;box-shadow:0 0 0 1px rgba(255,255,255,.08) inset,0 10px 28px -12px rgba(var(--p-akzent),.9)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .btn-primary:hover{background:linear-gradient(180deg,var(--p-akzent-hover),var(--p-tief-hover));transform:translateY(-1px)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .btn-primary:disabled{transform:none}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .btn-danger{color:var(--danger);border-color:rgba(255,122,138,.35)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .btn-danger:hover{background:rgba(255,122,138,.10);border-color:rgba(255,122,138,.6)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .btn-mini{padding:4px 10px}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .badge{background:var(--surface2);color:var(--dim);border-color:var(--border);border-radius:999px}
|
||||||
|
|
||||||
|
/* Listen, Kacheln, Tabs */
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .mods-list,
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .account-item,
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .screen-thumb{background:var(--input);border-color:var(--border);border-radius:12px}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .mod-icon,
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .account-av,
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .icon-preview{background:var(--surface2);border-radius:8px}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .mod-item{border-radius:10px}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .mod-item+.mod-item{border-color:var(--border2)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .mod-item:hover{background:var(--surface)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .mtab{border-radius:10px 10px 0 0}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .mtab.on{background:var(--surface);border-color:var(--border);color:var(--cyan)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .advanced{background:var(--surface);border-color:var(--border);border-radius:12px}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .console-out{background:var(--p-konsole);border-color:var(--border);border-radius:12px}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .stat-tile,
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .crash-fund,
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .share-code-box,
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .share-preview,
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .clip-bar,
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .wahl-option{border-radius:12px;background:var(--surface)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .chat-verlauf{background:var(--input);border-radius:12px}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .chat-blase{background:var(--surface2);border-radius:14px}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .chat-blase.ich{background:linear-gradient(180deg,rgb(var(--p-akzent)),rgb(var(--p-tief)));border-color:transparent}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .stat-bar{border-radius:0 6px 6px 0;background:linear-gradient(90deg,rgb(var(--p-akzent)),rgb(var(--p-akzent2)))}
|
||||||
|
|
||||||
|
/* Einstellungen */
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .settings-body{background:var(--area)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .settings-nav{background:var(--win);border-right-color:var(--border)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .snav{border-radius:10px}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .snav:hover{background:var(--surface)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .snav.on{background:linear-gradient(120deg,rgba(var(--p-akzent),.30),rgba(var(--p-tief),.18));border-color:rgba(var(--p-akzent2),.40);box-shadow:none}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .settings-card{background:var(--surface);border-color:var(--border);border-radius:16px}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .settings-kv-row,
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .about-brand{border-bottom-color:var(--border2)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .appinfo-item{background:rgba(var(--p-linie),.04);border-color:var(--border2);border-radius:12px}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .info-meta-link{color:var(--cyan)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .settings-hero h4{background:linear-gradient(120deg,rgb(var(--p-hell)),rgb(var(--p-akzent)) 70%);-webkit-background-clip:text;background-clip:text;color:transparent}
|
||||||
|
|
||||||
|
/* Kontextmenü, Hinweis, Bildlaufleisten */
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .ctx{background:var(--surface);border-color:var(--border);border-radius:12px;box-shadow:0 18px 40px -12px rgba(0,0,0,.8)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .ctx-item{border-radius:8px}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .ctx-item:hover{background:rgba(var(--p-akzent),.22)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .toast{background:var(--surface2);border:1px solid var(--border);border-radius:999px;padding:10px 18px}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) ::-webkit-scrollbar-thumb{background:rgba(var(--p-linie),.18);border-color:var(--area)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) ::-webkit-scrollbar-thumb:hover{background:rgba(var(--p-linie),.3)}
|
||||||
|
|
||||||
|
/* Skin-Editor */
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .se-modus{border-radius:999px;background:var(--surface)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .se-modus-btn.on{background:linear-gradient(180deg,rgb(var(--p-akzent)),rgb(var(--p-tief)))}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .se-tool,:root:is([data-design="aero"],[data-design="lila"]) .se-ebene{background:var(--surface)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .se-tool{border-radius:10px}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .se-ebenen{border-radius:10px}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .se-buehne,:root:is([data-design="aero"],[data-design="lila"]) .se-mini{border-radius:14px;background:var(--input)}
|
||||||
|
:root:is([data-design="aero"],[data-design="lila"]) .design-wahl{border-radius:12px}
|
||||||
|
|
||||||
|
/* ===================================================================== */
|
||||||
|
/* ===================== Design: Minecraft ============================= */
|
||||||
|
/* ===================================================================== */
|
||||||
|
@font-face{font-family:"AeroPixel";src:url(assets/aeropixel.ttf) format("truetype");font-display:block}
|
||||||
|
:root[data-design="minecraft"]{
|
||||||
|
--win:#313233; --area:#1e1e1f; --bar:#48494a;
|
||||||
|
--border:#000; --border2:#1a1a1b;
|
||||||
|
--text:#ffffff; --dim:#b8b9bc;
|
||||||
|
--sel:#2f6b1f; --sel-br:#6cc349; --link:#86d75e; --accent:#3c8527;
|
||||||
|
--danger:#ff5555; --play:#6cc349; --input:#000;
|
||||||
|
--stat-bar:#6cc349;
|
||||||
|
--pixel:"AeroPixel","Segoe UI",sans-serif;
|
||||||
|
--mc-schatten:2px 2px 0 #3f3f3f;
|
||||||
|
}
|
||||||
|
:root[data-design="minecraft"] body{background:var(--area)}
|
||||||
|
:root[data-design="minecraft"] ::selection{background:#3c8527;color:#fff}
|
||||||
|
:root[data-design="minecraft"]{--titel-bg:#1e1e1f;--titel-fg:#ffffff}
|
||||||
|
:root[data-design="minecraft"] .titel-text{font-family:var(--pixel);font-size:16px;text-shadow:var(--mc-schatten);padding-bottom:2px}
|
||||||
|
|
||||||
|
/* Wiederkehrender Block-Knopf: grauer Stein mit hellem/dunklem Rand */
|
||||||
|
:root[data-design="minecraft"] .btn,
|
||||||
|
:root[data-design="minecraft"] .sbtn,
|
||||||
|
:root[data-design="minecraft"] .snav,
|
||||||
|
:root[data-design="minecraft"] .se-tool,
|
||||||
|
:root[data-design="minecraft"] .wahl-option{
|
||||||
|
background:#58595b;color:#fff;border:2px solid #000;border-radius:0;
|
||||||
|
box-shadow:inset 2px 2px 0 #8b8c8f,inset -2px -2px 0 #36373a;
|
||||||
|
text-shadow:1px 1px 0 #2a2a2a}
|
||||||
|
:root[data-design="minecraft"] .btn:hover,
|
||||||
|
:root[data-design="minecraft"] .sbtn:hover,
|
||||||
|
:root[data-design="minecraft"] .snav:hover,
|
||||||
|
:root[data-design="minecraft"] .se-tool:hover:not(:disabled),
|
||||||
|
:root[data-design="minecraft"] .wahl-option:hover{background:#6a6b6f;border-color:#fff;box-shadow:inset 2px 2px 0 #a3a4a8,inset -2px -2px 0 #3e3f42}
|
||||||
|
:root[data-design="minecraft"] .btn:active,
|
||||||
|
:root[data-design="minecraft"] .sbtn:active{background:#48494b;box-shadow:inset -2px -2px 0 #8b8c8f,inset 2px 2px 0 #36373a}
|
||||||
|
:root[data-design="minecraft"] .btn:disabled,
|
||||||
|
:root[data-design="minecraft"] .sbtn:disabled{background:#2b2b2c;color:#8b8b8b;box-shadow:none;border-color:#000;text-shadow:none;opacity:1}
|
||||||
|
:root[data-design="minecraft"] .sbtn:disabled:hover{background:#2b2b2c;border-color:#000}
|
||||||
|
/* grün wie der Spielen-Knopf */
|
||||||
|
:root[data-design="minecraft"] .btn-primary,
|
||||||
|
:root[data-design="minecraft"] .sbtn.play,
|
||||||
|
:root[data-design="minecraft"] .snav.on,
|
||||||
|
:root[data-design="minecraft"] .se-tool.on,
|
||||||
|
:root[data-design="minecraft"] .wahl-option.an{background:#3c8527;color:#fff;box-shadow:inset 2px 2px 0 #6cc349,inset -2px -2px 0 #1d4d13}
|
||||||
|
:root[data-design="minecraft"] .btn-primary:hover,
|
||||||
|
:root[data-design="minecraft"] .sbtn.play:hover{background:#4a9a33;box-shadow:inset 2px 2px 0 #82d65c,inset -2px -2px 0 #24601a;border-color:#fff}
|
||||||
|
:root[data-design="minecraft"] .btn-danger{background:#8c2b2b;box-shadow:inset 2px 2px 0 #c55050,inset -2px -2px 0 #561616}
|
||||||
|
:root[data-design="minecraft"] .btn-danger:hover{background:#a33434;border-color:#fff;box-shadow:inset 2px 2px 0 #d86262,inset -2px -2px 0 #651b1b}
|
||||||
|
:root[data-design="minecraft"] .sbtn.del:hover{background:#8c2b2b;color:#fff;box-shadow:inset 2px 2px 0 #c55050,inset -2px -2px 0 #561616}
|
||||||
|
|
||||||
|
/* Werkzeugleiste: Schiefer mit Grasnarbe */
|
||||||
|
:root[data-design="minecraft"] .toolbar{
|
||||||
|
background:url(assets/design/gras.png) 0 0/32px 8px repeat-x,url(assets/design/schiefer.png) 0 0/64px 64px;
|
||||||
|
image-rendering:pixelated;border-bottom:2px solid #000;padding:11px 8px 6px}
|
||||||
|
:root[data-design="minecraft"] .tbtn{color:#fff;border-radius:0;text-shadow:1px 1px 0 #1a1a1a;image-rendering:auto}
|
||||||
|
:root[data-design="minecraft"] .tbtn:hover{background:rgba(0,0,0,.35);border-color:#fff}
|
||||||
|
:root[data-design="minecraft"] .tbtn:active{background:rgba(0,0,0,.5)}
|
||||||
|
:root[data-design="minecraft"] .tbtn svg,:root[data-design="minecraft"] #btn-console svg,:root[data-design="minecraft"] .acct-av svg{color:#e6e6e6}
|
||||||
|
:root[data-design="minecraft"] #btn-new svg,:root[data-design="minecraft"] #btn-account svg{color:#7fd858}
|
||||||
|
:root[data-design="minecraft"] #btn-import svg{color:#6db8ff}
|
||||||
|
:root[data-design="minecraft"] #btn-open-dir svg{color:#f2c94c}
|
||||||
|
:root[data-design="minecraft"] #btn-settings svg{color:#d0d0d0}
|
||||||
|
:root[data-design="minecraft"] .tbsep{background:#000;width:2px}
|
||||||
|
:root[data-design="minecraft"] .tb-badge{border-radius:0;background:#ff5555;box-shadow:inset -1px -1px 0 #a32a2a}
|
||||||
|
|
||||||
|
/* Eingabefelder: schwarz mit grauem Rand wie im Spiel */
|
||||||
|
:root[data-design="minecraft"] .search-wrap input,
|
||||||
|
:root[data-design="minecraft"] .field input,
|
||||||
|
:root[data-design="minecraft"] .field select,
|
||||||
|
:root[data-design="minecraft"] .field textarea,
|
||||||
|
:root[data-design="minecraft"] .mods-search input,
|
||||||
|
:root[data-design="minecraft"] .mods-search select,
|
||||||
|
:root[data-design="minecraft"] .mods-filters select,
|
||||||
|
:root[data-design="minecraft"] .mmc-bar select,
|
||||||
|
:root[data-design="minecraft"] .clip-length select,
|
||||||
|
:root[data-design="minecraft"] .path-row input,
|
||||||
|
:root[data-design="minecraft"] .login-usercode,
|
||||||
|
:root[data-design="minecraft"] .upd-notes,
|
||||||
|
:root[data-design="minecraft"] .se-farbzeile input[type=text],
|
||||||
|
:root[data-design="minecraft"] .se-feld select{background:#000;color:#fff;border:1px solid #a0a0a0;border-radius:0}
|
||||||
|
:root[data-design="minecraft"] .search-wrap input:focus,
|
||||||
|
:root[data-design="minecraft"] .field input:focus,
|
||||||
|
:root[data-design="minecraft"] .field select:focus,
|
||||||
|
:root[data-design="minecraft"] .field textarea:focus,
|
||||||
|
:root[data-design="minecraft"] .mods-search input:focus,
|
||||||
|
:root[data-design="minecraft"] .path-row input:focus{border-color:#fff;box-shadow:none}
|
||||||
|
:root[data-design="minecraft"] input[type=checkbox],:root[data-design="minecraft"] input[type=radio],:root[data-design="minecraft"] input[type=range]{accent-color:#3c8527}
|
||||||
|
|
||||||
|
/* Instanzliste auf dunkler Erde */
|
||||||
|
:root[data-design="minecraft"] .workspace{background:var(--area)}
|
||||||
|
:root[data-design="minecraft"] .list-area{background:url(assets/design/erde-dunkel.png) 0 0/64px 64px;image-rendering:pixelated}
|
||||||
|
:root[data-design="minecraft"] .list-area::after,
|
||||||
|
:root[data-design="minecraft"] .list{image-rendering:auto}
|
||||||
|
:root[data-design="minecraft"] .group-header{font-family:var(--pixel);font-size:16px;font-weight:400;color:#fff;text-shadow:var(--mc-schatten);padding:8px 4px 4px}
|
||||||
|
:root[data-design="minecraft"] .group-count{color:#b8b9bc}
|
||||||
|
:root[data-design="minecraft"] .group-header .chev{color:#fff;font-family:"Segoe UI",sans-serif;font-size:10px}
|
||||||
|
:root[data-design="minecraft"] .group-header::after{height:2px;background:rgba(0,0,0,.55);box-shadow:0 1px 0 rgba(255,255,255,.08)}
|
||||||
|
:root[data-design="minecraft"] .card{border-radius:0}
|
||||||
|
:root[data-design="minecraft"] .card-name{color:#fff;text-shadow:1px 1px 0 #1a1a1a}
|
||||||
|
:root[data-design="minecraft"] .card:hover{background:rgba(0,0,0,.35);border-color:rgba(255,255,255,.35)}
|
||||||
|
:root[data-design="minecraft"] .card.sel{background:rgba(0,0,0,.6);border-color:#fff;outline:1px solid #fff;outline-offset:-2px}
|
||||||
|
:root[data-design="minecraft"] .card.sel .card-name{background:none;color:#ffff7f;border-radius:0}
|
||||||
|
:root[data-design="minecraft"] .card-taste{border-radius:0;background:#3c8527;box-shadow:inset -1px -1px 0 #1d4d13}
|
||||||
|
:root[data-design="minecraft"] .card-icon-img{border-radius:0}
|
||||||
|
:root[data-design="minecraft"] .empty h2{font-family:var(--pixel);font-weight:400;font-size:24px;color:#fff;text-shadow:3px 3px 0 #3f3f3f}
|
||||||
|
:root[data-design="minecraft"] .empty p{color:#d0d0d0}
|
||||||
|
|
||||||
|
/* Seitenleiste: Knöpfe gestapelt wie im Pausenmenü */
|
||||||
|
:root[data-design="minecraft"] .side{background:url(assets/design/schiefer.png) 0 0/64px 64px;image-rendering:pixelated;border-left:2px solid #000;padding:10px 6px}
|
||||||
|
:root[data-design="minecraft"] .side>*{image-rendering:auto}
|
||||||
|
:root[data-design="minecraft"] .side-title{font-family:var(--pixel);font-size:16px;font-weight:400;text-shadow:var(--mc-schatten)}
|
||||||
|
:root[data-design="minecraft"] .side-grp{color:#86d75e}
|
||||||
|
:root[data-design="minecraft"] .side-ico img{border-radius:0}
|
||||||
|
:root[data-design="minecraft"] .side-empty{color:#d0d0d0}
|
||||||
|
:root[data-design="minecraft"] .hr{background:#000;height:2px;margin:6px 0}
|
||||||
|
:root[data-design="minecraft"] .sbtn{margin-bottom:3px;padding:5px 2px}
|
||||||
|
:root[data-design="minecraft"] .sbtn.play{font-family:var(--pixel);font-size:16px;font-weight:400;padding:6px 4px 7px;text-shadow:var(--mc-schatten)}
|
||||||
|
|
||||||
|
/* Statusleiste */
|
||||||
|
:root[data-design="minecraft"] .statusbar{background:url(assets/design/schiefer.png) 0 0/64px 64px;image-rendering:pixelated;border-top:2px solid #000}
|
||||||
|
:root[data-design="minecraft"] .st-row{color:#e6e6e6;text-shadow:1px 1px 0 #1a1a1a}
|
||||||
|
:root[data-design="minecraft"] .st-row2{background:rgba(0,0,0,.4);border-top:2px solid #000}
|
||||||
|
|
||||||
|
/* Dialoge: dunkles Steinfenster mit Holzbalken als Kopf */
|
||||||
|
:root[data-design="minecraft"] .modal{background:rgba(0,0,0,.62)}
|
||||||
|
:root[data-design="minecraft"] .modal-card{background:#313233;border:2px solid #000;border-radius:0;box-shadow:inset 2px 2px 0 #5a5b5d,inset -2px -2px 0 #1e1e1f,0 10px 30px rgba(0,0,0,.6)}
|
||||||
|
:root[data-design="minecraft"] .modal-head{background:url(assets/design/bretter.png) 0 0/64px 64px;image-rendering:pixelated;border-bottom:2px solid #000;margin:2px 2px 0}
|
||||||
|
:root[data-design="minecraft"] .modal-head h3{font-family:var(--pixel);font-size:16px;font-weight:400;color:#fff;text-shadow:var(--mc-schatten)}
|
||||||
|
:root[data-design="minecraft"] .modal-head>*{image-rendering:auto}
|
||||||
|
:root[data-design="minecraft"] .modal-body{background:#313233;margin:0 2px}
|
||||||
|
:root[data-design="minecraft"] .modal-foot{background:#262627;border-top:2px solid #000;margin:0 2px 2px}
|
||||||
|
:root[data-design="minecraft"] .modal-close{color:#fff;text-shadow:1px 1px 0 #3f3f3f;font-size:16px}
|
||||||
|
:root[data-design="minecraft"] .modal-close:hover{color:#ffff7f}
|
||||||
|
:root[data-design="minecraft"] .popout-host .modal-card{box-shadow:inset 2px 2px 0 #5a5b5d,inset -2px -2px 0 #1e1e1f}
|
||||||
|
|
||||||
|
/* Listen, Kacheln, Tabs */
|
||||||
|
:root[data-design="minecraft"] .mods-list,
|
||||||
|
:root[data-design="minecraft"] .account-item,
|
||||||
|
:root[data-design="minecraft"] .screen-thumb,
|
||||||
|
:root[data-design="minecraft"] .chat-verlauf,
|
||||||
|
:root[data-design="minecraft"] .console-out{background:#000;border:2px solid #000;border-radius:0;box-shadow:inset 1px 1px 0 #3a3a3c}
|
||||||
|
:root[data-design="minecraft"] .account-item{background:#1e1e1f}
|
||||||
|
:root[data-design="minecraft"] .account-item.active{border-color:#6cc349}
|
||||||
|
:root[data-design="minecraft"] .mod-icon,
|
||||||
|
:root[data-design="minecraft"] .account-av,
|
||||||
|
:root[data-design="minecraft"] .icon-preview{background:#48494a;border-radius:0}
|
||||||
|
:root[data-design="minecraft"] .mod-item{border-radius:0}
|
||||||
|
:root[data-design="minecraft"] .mod-item+.mod-item{border-color:#2a2a2b}
|
||||||
|
:root[data-design="minecraft"] .mod-item:hover{background:#262627;outline:1px solid #8b8b8b;outline-offset:-1px}
|
||||||
|
:root[data-design="minecraft"] .mods-tabs{border-bottom:2px solid #000}
|
||||||
|
:root[data-design="minecraft"] .mtab{border-radius:0;color:#c8c8c8;margin-bottom:-2px;border-width:2px}
|
||||||
|
:root[data-design="minecraft"] .mtab.on{background:#48494a;border-color:#000;color:#fff;text-shadow:1px 1px 0 #2a2a2a}
|
||||||
|
:root[data-design="minecraft"] .advanced,
|
||||||
|
:root[data-design="minecraft"] .stat-tile,
|
||||||
|
:root[data-design="minecraft"] .crash-fund,
|
||||||
|
:root[data-design="minecraft"] .share-code-box,
|
||||||
|
:root[data-design="minecraft"] .share-preview,
|
||||||
|
:root[data-design="minecraft"] .clip-bar,
|
||||||
|
:root[data-design="minecraft"] .crash-unklar{background:#262627;border:2px solid #000;border-radius:0;box-shadow:inset 1px 1px 0 #48494a}
|
||||||
|
:root[data-design="minecraft"] .crash-fund{border-left:4px solid #3c8527}
|
||||||
|
:root[data-design="minecraft"] .badge{border-radius:0;background:#48494a;color:#e6e6e6;border-color:#000}
|
||||||
|
:root[data-design="minecraft"] .chat-blase{border-radius:0;background:#48494a;border:2px solid #000}
|
||||||
|
:root[data-design="minecraft"] .chat-blase.ich{background:#3c8527;border-color:#000}
|
||||||
|
:root[data-design="minecraft"] .stat-bar{border-radius:0;box-shadow:inset 0 -2px 0 #3c8527}
|
||||||
|
:root[data-design="minecraft"] .mod-new{border-radius:0}
|
||||||
|
:root[data-design="minecraft"] .friend-avatar{border-radius:0}
|
||||||
|
|
||||||
|
/* Einstellungen */
|
||||||
|
:root[data-design="minecraft"] .settings-body{background:url(assets/design/erde-dunkel.png) 0 0/64px 64px;image-rendering:pixelated}
|
||||||
|
:root[data-design="minecraft"] .settings-body>*{image-rendering:auto}
|
||||||
|
:root[data-design="minecraft"] .settings-nav{background:rgba(0,0,0,.45);border-right:2px solid #000;gap:4px}
|
||||||
|
:root[data-design="minecraft"] .settings-nav-label{color:#d0d0d0;text-shadow:1px 1px 0 #1a1a1a}
|
||||||
|
:root[data-design="minecraft"] .snav{padding:8px 12px}
|
||||||
|
:root[data-design="minecraft"] .settings-hero h4{font-family:var(--pixel);font-size:24px;font-weight:400;text-shadow:3px 3px 0 #3f3f3f}
|
||||||
|
:root[data-design="minecraft"] .settings-hero p{color:#d0d0d0}
|
||||||
|
:root[data-design="minecraft"] .settings-card{background:#313233;border:2px solid #000;border-radius:0;box-shadow:inset 2px 2px 0 #5a5b5d,inset -2px -2px 0 #1e1e1f}
|
||||||
|
:root[data-design="minecraft"] .settings-card h5{font-family:var(--pixel);font-size:16px;font-weight:400;text-shadow:var(--mc-schatten)}
|
||||||
|
:root[data-design="minecraft"] .settings-kv-row,
|
||||||
|
:root[data-design="minecraft"] .about-brand{border-bottom-color:#1a1a1b}
|
||||||
|
:root[data-design="minecraft"] .appinfo-item{background:#262627;border:2px solid #000;border-radius:0}
|
||||||
|
:root[data-design="minecraft"] .info-meta-link{color:#86d75e}
|
||||||
|
:root[data-design="minecraft"] .spane h4{font-family:var(--pixel);font-size:16px;font-weight:400;text-shadow:var(--mc-schatten)}
|
||||||
|
|
||||||
|
/* Kontextmenü, Hinweis, Bildlaufleisten */
|
||||||
|
:root[data-design="minecraft"] .ctx{background:#1e1e1f;border:2px solid #000;border-radius:0;box-shadow:inset 1px 1px 0 #48494a,0 8px 24px rgba(0,0,0,.5)}
|
||||||
|
:root[data-design="minecraft"] .ctx-item{border-radius:0;text-shadow:1px 1px 0 #000}
|
||||||
|
:root[data-design="minecraft"] .ctx-item:hover{background:#3c8527}
|
||||||
|
:root[data-design="minecraft"] .ctx-danger:hover{background:#8c2b2b}
|
||||||
|
:root[data-design="minecraft"] .ctx-sep{background:#000;height:2px}
|
||||||
|
:root[data-design="minecraft"] .toast{background:#212122;border:2px solid #000;border-radius:0;box-shadow:inset 2px 2px 0 #4a4a4c,inset -2px -2px 0 #111,0 6px 20px rgba(0,0,0,.5);text-shadow:1px 1px 0 #000}
|
||||||
|
:root[data-design="minecraft"] .toast.err{background:#5c1a1a}
|
||||||
|
:root[data-design="minecraft"] ::-webkit-scrollbar-thumb{background:#8b8b8b;border:3px solid var(--area);border-radius:0;box-shadow:inset -2px -2px 0 #555}
|
||||||
|
:root[data-design="minecraft"] ::-webkit-scrollbar-thumb:hover{background:#a8a8a8}
|
||||||
|
|
||||||
|
/* Skin-Editor */
|
||||||
|
:root[data-design="minecraft"] .se-modus{border:2px solid #000;border-radius:0;background:#2b2b2c}
|
||||||
|
:root[data-design="minecraft"] .se-modus-btn{color:#d0d0d0;text-shadow:1px 1px 0 #000}
|
||||||
|
:root[data-design="minecraft"] .se-modus-btn+.se-modus-btn{border-left:2px solid #000}
|
||||||
|
:root[data-design="minecraft"] .se-modus-btn.on{background:#3c8527;color:#fff;box-shadow:inset 2px 2px 0 #6cc349,inset -2px -2px 0 #1d4d13}
|
||||||
|
:root[data-design="minecraft"] .se-ebenen{border:2px solid #000;border-radius:0}
|
||||||
|
:root[data-design="minecraft"] .se-ebene{background:#2b2b2c;color:#d0d0d0}
|
||||||
|
:root[data-design="minecraft"] .se-ebene+.se-ebene{border-left:2px solid #000}
|
||||||
|
:root[data-design="minecraft"] .se-ebene.on{background:#3c8527;color:#fff}
|
||||||
|
:root[data-design="minecraft"] .se-buehne,:root[data-design="minecraft"] .se-mini{border:2px solid #000;border-radius:0;background:#1e1e1f}
|
||||||
|
:root[data-design="minecraft"] .se-farbfeld{border-radius:0;border-color:#000}
|
||||||
|
:root[data-design="minecraft"] .se-titel{color:#d0d0d0}
|
||||||
|
:root[data-design="minecraft"] .design-wahl{border-radius:0;border:2px solid #000;background:#262627}
|
||||||
|
:root[data-design="minecraft"] .design-wahl.on{border-color:#fff;box-shadow:none;background:#2f6b1f}
|
||||||
|
:root[data-design="minecraft"] .dv{border-radius:0}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* discordpresence.js – Discord Rich Presence ("Spielt Minecraft über AeroMC")
|
||||||
|
* -----------------------------------------------------------------------------
|
||||||
|
* Verbindet sich rein lokal über die IPC-Pipe des laufenden Discord-Clients.
|
||||||
|
* SET_ACTIVITY braucht dafür bewusst KEINE Authentifizierung/OAuth (anders als
|
||||||
|
* der Rest der RPC-API) – daher hier absichtlich client.request(...) direkt
|
||||||
|
* statt client.user.setActivity(...), das erst nach einem Login funktioniert.
|
||||||
|
*
|
||||||
|
* Immer aktiv, ohne eigene Einstellung – braucht also keine Zustimmung/kein
|
||||||
|
* Häkchen. Läuft trotzdem komplett unauffällig: ist Discord nicht installiert
|
||||||
|
* oder nicht gestartet, bleibt es einfach inaktiv, ohne den Launcher oder den
|
||||||
|
* Spielstart zu stören – jeder Fehler hier wird bewusst verschluckt.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { Client } = require('@xhayper/discord-rpc');
|
||||||
|
|
||||||
|
// Von M_Viper unter discord.com/developers/applications angelegte AeroMC-App.
|
||||||
|
// Öffentliche Anwendungs-ID, kein Geheimnis (genau wie DEFAULT_CLIENT_ID bei
|
||||||
|
// der Microsoft-Anmeldung in auth.js) – gilt für alle AeroMC-Nutzer gleich.
|
||||||
|
const DEFAULT_CLIENT_ID = '1538173087730630686';
|
||||||
|
|
||||||
|
const LAUNCHER_WEBSITE = 'https://aeromc.viper.ipv64.net/';
|
||||||
|
|
||||||
|
let clientId = DEFAULT_CLIENT_ID;
|
||||||
|
let client = null;
|
||||||
|
let connecting = null;
|
||||||
|
|
||||||
|
function resolveClientId() {
|
||||||
|
return (clientId && String(clientId).trim()) || DEFAULT_CLIENT_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connect() {
|
||||||
|
if (client && client.isConnected) return client;
|
||||||
|
if (connecting) return connecting;
|
||||||
|
|
||||||
|
const id = resolveClientId();
|
||||||
|
const c = new Client({ clientId: id });
|
||||||
|
c.on('disconnected', () => { if (client === c) client = null; });
|
||||||
|
connecting = c.connect()
|
||||||
|
.then(() => { client = c; connecting = null; return c; })
|
||||||
|
.catch((err) => { connecting = null; throw err; });
|
||||||
|
return connecting;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* activity: { details, state, startTimestamp }
|
||||||
|
* details/state -> die beiden Zeilen unter dem Profilbild in Discord
|
||||||
|
* startTimestamp -> Discord zeigt die Spielzeit dann selbst laufend an
|
||||||
|
*/
|
||||||
|
async function setActivity(activity) {
|
||||||
|
try {
|
||||||
|
const c = await connect();
|
||||||
|
await c.request('SET_ACTIVITY', {
|
||||||
|
pid: process.pid,
|
||||||
|
activity: {
|
||||||
|
details: activity.details,
|
||||||
|
state: activity.state,
|
||||||
|
timestamps: activity.startTimestamp ? { start: activity.startTimestamp } : undefined,
|
||||||
|
assets: { large_image: 'aeromc_logo', large_text: 'AeroMC Launcher' },
|
||||||
|
// Discord zeigt Buttons Betrachtern des Profils an (nicht sich selbst) -
|
||||||
|
// maximal 2 sind erlaubt, hier reicht einer.
|
||||||
|
buttons: [{ label: 'AeroMC herunterladen', url: LAUNCHER_WEBSITE }],
|
||||||
|
instance: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Discord nicht installiert/nicht gestartet o. Ä. - bewusst lautlos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearActivity() {
|
||||||
|
try {
|
||||||
|
if (!client || !client.isConnected) return;
|
||||||
|
await client.request('SET_ACTIVITY', { pid: process.pid });
|
||||||
|
} catch { /* egal */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function shutdown() {
|
||||||
|
const c = client;
|
||||||
|
client = null;
|
||||||
|
connecting = null;
|
||||||
|
if (!c) return;
|
||||||
|
try { await c.destroy(); } catch { /* egal */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Verbindet direkt beim Start. settings.discordClientId erlaubt weiterhin
|
||||||
|
* (rein optional, per Hand in settings.json) eine eigene App-ID zu setzen -
|
||||||
|
* ohne eigenes Einstellungsfeld, ohne dass es dafür einen Schalter braucht.
|
||||||
|
*/
|
||||||
|
function configure(settings) {
|
||||||
|
const nextId = (settings && settings.discordClientId) || DEFAULT_CLIENT_ID;
|
||||||
|
const idChanged = nextId !== clientId;
|
||||||
|
clientId = nextId;
|
||||||
|
|
||||||
|
if (idChanged) { shutdown().then(() => connect().catch(() => {})); }
|
||||||
|
else if (!client) { connect().catch(() => {}); }
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { configure, setActivity, clearActivity, shutdown, DEFAULT_CLIENT_ID };
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* friends.js – Anbindung an das AeroMC-Freundesystem (WordPress-Plugin)
|
||||||
|
* -----------------------------------------------------------------------------
|
||||||
|
* Schlanke Fetch-Wrapper gegen die REST-Routen des "AeroMC Freunde"-Plugins
|
||||||
|
* (siehe wordpress-plugin/aeromc-friends/). Jeder Aufruf trägt die
|
||||||
|
* Minecraft-UUID des Kontos und – sobald angemeldet – ein Sitzungs-Token.
|
||||||
|
*
|
||||||
|
* Anmeldung wie bei einem Minecraft-Server, ohne eigenes Passwort: Aufgabe
|
||||||
|
* vom Plugin holen, mit dem Minecraft-Zugang des Kontos bei Mojang "beitreten",
|
||||||
|
* Plugin prüft bei Mojang nach und gibt ein Token aus. Läuft im Hintergrund
|
||||||
|
* vor der ersten Anfrage eines Kontos; kennt das Plugin die Anmeldung noch
|
||||||
|
* nicht (ältere Fassung), geht es ohne Token weiter wie bisher.
|
||||||
|
*
|
||||||
|
* Genau wie bei discordpresence.js: jeder Fehler (Plugin nicht installiert,
|
||||||
|
* kein Internet, Seite down) wird abgefangen und nach oben als { ok:false,
|
||||||
|
* message } gereicht statt den Launcher zu stören - ein soziales Extra darf
|
||||||
|
* nie den Spielstart oder die Bedienung blockieren.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const BASIS_URL = 'https://viper-network.de/wp-json/aeromc/v1';
|
||||||
|
const MOJANG_JOIN = 'https://sessionserver.mojang.com/session/minecraft/join';
|
||||||
|
|
||||||
|
// liefert für eine Konto-UUID { accessToken, name } (vom Hauptprozess gesetzt)
|
||||||
|
let zugangsHelfer = null;
|
||||||
|
const sitzungen = new Map(); // uuid -> Token
|
||||||
|
const laufendeAnmeldung = new Map(); // uuid -> Promise (nicht doppelt anmelden)
|
||||||
|
const fehlversuch = new Map(); // uuid -> Zeitpunkt, um nicht ständig neu zu versuchen
|
||||||
|
const NEUVERSUCH_MS = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
function setzeZugangsHelfer(fn) { zugangsHelfer = fn; }
|
||||||
|
|
||||||
|
async function roheAnfrage(pfad, { method = 'GET', body, token } = {}) {
|
||||||
|
const url = method === 'GET' && body
|
||||||
|
? `${BASIS_URL}${pfad}?${new URLSearchParams(body).toString()}`
|
||||||
|
: `${BASIS_URL}${pfad}`;
|
||||||
|
const headers = { 'Content-Type': 'application/json' };
|
||||||
|
if (token) headers['X-AeroMC-Token'] = token;
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body: method === 'GET' ? undefined : JSON.stringify(body || {}),
|
||||||
|
});
|
||||||
|
const daten = await res.json().catch(() => null);
|
||||||
|
return { res, daten };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mojang-Sitzungsprüfung durchlaufen und ein Token holen; null bei Fehlschlag
|
||||||
|
async function anmelden(uuid) {
|
||||||
|
if (!zugangsHelfer) return null;
|
||||||
|
const letzter = fehlversuch.get(uuid);
|
||||||
|
if (letzter && Date.now() - letzter < NEUVERSUCH_MS) return null;
|
||||||
|
if (laufendeAnmeldung.has(uuid)) return laufendeAnmeldung.get(uuid);
|
||||||
|
const ablauf = (async () => {
|
||||||
|
try {
|
||||||
|
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 } });
|
||||||
|
if (!aufgabe.res.ok || !aufgabe.daten || !aufgabe.daten.serverId) throw new Error('keine Aufgabe');
|
||||||
|
const join = await fetch(MOJANG_JOIN, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
accessToken: zugang.accessToken,
|
||||||
|
selectedProfile: String(uuid).replace(/-/g, ''),
|
||||||
|
serverId: aufgabe.daten.serverId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
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,
|
||||||
|
};
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* javadl.js – Java bei Bedarf selbst beschaffen
|
||||||
|
* ----------------------------------------------
|
||||||
|
* services.js sucht nur nach vorhandenen Java-Installationen. Wer keine hat,
|
||||||
|
* kam bisher nicht ins Spiel. Dieses Modul lädt die passende Laufzeit von
|
||||||
|
* Adoptium (Eclipse Temurin) und legt sie unter <userData>/java ab.
|
||||||
|
*
|
||||||
|
* Ablauf: Release suchen -> ZIP laden -> entpacken -> java.exe zurückgeben.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const unzipper = require('unzipper');
|
||||||
|
|
||||||
|
const API = 'https://api.adoptium.net/v3';
|
||||||
|
const UA = 'AeroMC-Launcher';
|
||||||
|
|
||||||
|
let javaRoot = null;
|
||||||
|
|
||||||
|
// Ablageort festlegen (üblicherweise <userData>/java)
|
||||||
|
function setJavaDir(dir) {
|
||||||
|
javaRoot = dir;
|
||||||
|
return javaRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getJavaDir() { return javaRoot; }
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Sucht das neueste Release für eine Major-Version.
|
||||||
|
* Bevorzugt die JRE (rund halb so groß), fällt sonst auf das JDK zurück.
|
||||||
|
*/
|
||||||
|
async function findRelease(major) {
|
||||||
|
for (const imageType of ['jre', 'jdk']) {
|
||||||
|
const url = new URL(`${API}/assets/latest/${major}/hotspot`);
|
||||||
|
url.searchParams.set('architecture', 'x64');
|
||||||
|
url.searchParams.set('image_type', imageType);
|
||||||
|
url.searchParams.set('os', 'windows');
|
||||||
|
url.searchParams.set('vendor', 'eclipse');
|
||||||
|
|
||||||
|
const res = await fetch(url, { headers: { 'User-Agent': UA, Accept: 'application/json' } });
|
||||||
|
if (!res.ok) continue;
|
||||||
|
const liste = await res.json();
|
||||||
|
const treffer = (Array.isArray(liste) ? liste : [])
|
||||||
|
.find((a) => a && a.binary && a.binary.package && a.binary.package.link);
|
||||||
|
if (!treffer) continue;
|
||||||
|
|
||||||
|
return {
|
||||||
|
major,
|
||||||
|
imageType,
|
||||||
|
releaseName: treffer.release_name || `temurin-${major}`,
|
||||||
|
url: treffer.binary.package.link,
|
||||||
|
size: Number(treffer.binary.package.size) || 0,
|
||||||
|
sha256: treffer.binary.package.checksum || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ordnername, unter dem eine Laufzeit abgelegt wird
|
||||||
|
function zielOrdner(release) {
|
||||||
|
const safe = String(release.releaseName).replace(/[^\w.+-]/g, '_');
|
||||||
|
return path.join(javaRoot, `${safe}-${release.imageType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// java.exe innerhalb einer entpackten Laufzeit finden (bin/ liegt eine Ebene tiefer)
|
||||||
|
function findeJavaExe(wurzel) {
|
||||||
|
const direkt = path.join(wurzel, 'bin', 'java.exe');
|
||||||
|
if (fs.existsSync(direkt)) return direkt;
|
||||||
|
let eintraege = [];
|
||||||
|
try { eintraege = fs.readdirSync(wurzel, { withFileTypes: true }); } catch { return null; }
|
||||||
|
for (const e of eintraege) {
|
||||||
|
if (!e.isDirectory()) continue;
|
||||||
|
const p = path.join(wurzel, e.name, 'bin', 'java.exe');
|
||||||
|
if (fs.existsSync(p)) return p;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Bereits heruntergeladene Laufzeiten auflisten.
|
||||||
|
* -> [{ path, dir, major }]
|
||||||
|
*/
|
||||||
|
function listManaged() {
|
||||||
|
if (!javaRoot || !fs.existsSync(javaRoot)) return [];
|
||||||
|
const out = [];
|
||||||
|
for (const name of fs.readdirSync(javaRoot)) {
|
||||||
|
const dir = path.join(javaRoot, name);
|
||||||
|
let exe = null;
|
||||||
|
try { if (fs.statSync(dir).isDirectory()) exe = findeJavaExe(dir); } catch { continue; }
|
||||||
|
if (!exe) continue;
|
||||||
|
const m = name.match(/(?:jdk-?|temurin-?)(\d+)/i);
|
||||||
|
out.push({ path: exe, dir, major: m ? Number(m[1]) : null });
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// schon eine passende Laufzeit da?
|
||||||
|
function findManaged(major) {
|
||||||
|
return listManaged().find((j) => j.major === major) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ladeDatei(url, ziel, onProgress) {
|
||||||
|
const res = await fetch(url, { headers: { 'User-Agent': UA } });
|
||||||
|
if (!res.ok) throw new Error('Java-Download fehlgeschlagen: HTTP ' + res.status);
|
||||||
|
const gesamt = Number(res.headers.get('content-length')) || 0;
|
||||||
|
|
||||||
|
fs.mkdirSync(path.dirname(ziel), { recursive: true });
|
||||||
|
const out = fs.createWriteStream(ziel);
|
||||||
|
let geladen = 0;
|
||||||
|
let letzte = 0;
|
||||||
|
try {
|
||||||
|
for await (const stueck of res.body) {
|
||||||
|
const buf = Buffer.from(stueck);
|
||||||
|
geladen += buf.length;
|
||||||
|
if (!out.write(buf)) await new Promise((resolve, reject) => {
|
||||||
|
const onDrain = () => { out.off('error', onError); resolve(); };
|
||||||
|
const onError = (e) => { out.off('drain', onDrain); reject(e); };
|
||||||
|
out.once('drain', onDrain);
|
||||||
|
out.once('error', onError);
|
||||||
|
});
|
||||||
|
if (onProgress && Date.now() - letzte >= 250) {
|
||||||
|
letzte = Date.now();
|
||||||
|
onProgress({ phase: 'java-download', geladen, gesamt, percent: gesamt ? Math.round((geladen / gesamt) * 100) : 0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
out.once('error', reject);
|
||||||
|
out.end((err) => (err ? reject(err) : resolve()));
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
out.destroy();
|
||||||
|
fs.rmSync(ziel, { force: true });
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
return geladen;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pfade aus dem Archiv gegen Ausbrüche absichern
|
||||||
|
function saubererPfad(eintrag) {
|
||||||
|
const p = String(eintrag || '').replace(/\\/g, '/');
|
||||||
|
if (!p || p.startsWith('/') || /^[a-z]:/i.test(p)) return null;
|
||||||
|
if (p.split('/').includes('..')) return null;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function entpacken(zipPfad, zielDir, onProgress) {
|
||||||
|
const archiv = await unzipper.Open.file(zipPfad);
|
||||||
|
const dateien = (archiv.files || []).filter((e) => e.type !== 'Directory');
|
||||||
|
fs.mkdirSync(zielDir, { recursive: true });
|
||||||
|
|
||||||
|
let fertig = 0;
|
||||||
|
let letzte = 0;
|
||||||
|
for (const eintrag of archiv.files || []) {
|
||||||
|
const rel = saubererPfad(eintrag.path);
|
||||||
|
if (!rel) continue;
|
||||||
|
const ziel = path.join(zielDir, rel);
|
||||||
|
if (eintrag.type === 'Directory') { fs.mkdirSync(ziel, { recursive: true }); continue; }
|
||||||
|
fs.mkdirSync(path.dirname(ziel), { recursive: true });
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
eintrag.stream()
|
||||||
|
.pipe(fs.createWriteStream(ziel))
|
||||||
|
.on('finish', resolve)
|
||||||
|
.on('error', reject);
|
||||||
|
});
|
||||||
|
fertig++;
|
||||||
|
if (onProgress && Date.now() - letzte >= 250) {
|
||||||
|
letzte = Date.now();
|
||||||
|
onProgress({ phase: 'java-entpacken', fertig, gesamt: dateien.length,
|
||||||
|
percent: dateien.length ? Math.round((fertig / dateien.length) * 100) : 0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fertig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Stellt sicher, dass eine Java-Laufzeit der gewünschten Major-Version da ist.
|
||||||
|
* -> { path, major, geladen } oder null
|
||||||
|
*/
|
||||||
|
async function ensureJava(major, onProgress) {
|
||||||
|
if (!javaRoot) throw new Error('Java-Ablageort ist nicht gesetzt.');
|
||||||
|
const vorhanden = findManaged(major);
|
||||||
|
if (vorhanden) return { path: vorhanden.path, major, geladen: false };
|
||||||
|
|
||||||
|
const release = await findRelease(major);
|
||||||
|
if (!release) throw new Error(`Für Java ${major} wird bei Adoptium keine Windows-Fassung angeboten.`);
|
||||||
|
|
||||||
|
const ziel = zielOrdner(release);
|
||||||
|
const tmpZip = path.join(javaRoot, `.download-${Date.now()}.zip`);
|
||||||
|
fs.mkdirSync(javaRoot, { recursive: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (onProgress) onProgress({ phase: 'java-download', geladen: 0, gesamt: release.size, percent: 0 });
|
||||||
|
await ladeDatei(release.url, tmpZip, onProgress);
|
||||||
|
// in einen Nebenordner entpacken und erst am Ende umbenennen -> kein
|
||||||
|
// halbfertiges Java, falls unterwegs etwas schiefgeht
|
||||||
|
const tmpDir = ziel + '.teil';
|
||||||
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
await entpacken(tmpZip, tmpDir, onProgress);
|
||||||
|
fs.rmSync(ziel, { recursive: true, force: true });
|
||||||
|
fs.renameSync(tmpDir, ziel);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tmpZip, { force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const exe = findeJavaExe(ziel);
|
||||||
|
if (!exe) throw new Error('Im heruntergeladenen Archiv wurde keine java.exe gefunden.');
|
||||||
|
return { path: exe, major, geladen: true, dir: ziel };
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { setJavaDir, getJavaDir, ensureJava, findRelease, listManaged, findManaged, findeJavaExe };
|
||||||
@@ -21,6 +21,7 @@ const { spawn, spawnSync } = require('child_process');
|
|||||||
const services = require('./services');
|
const services = require('./services');
|
||||||
const loaders = require('./loaders');
|
const loaders = require('./loaders');
|
||||||
const forge = require('./forge');
|
const forge = require('./forge');
|
||||||
|
const processwatchdog = require('./processwatchdog');
|
||||||
|
|
||||||
const UA = 'AeroMC-Launcher/0.2';
|
const UA = 'AeroMC-Launcher/0.2';
|
||||||
const RESOURCES = 'https://resources.download.minecraft.net';
|
const RESOURCES = 'https://resources.download.minecraft.net';
|
||||||
@@ -150,6 +151,36 @@ async function prepareLibraries(vj, sharedDir, nativesDir, onProgress) {
|
|||||||
return classpath;
|
return classpath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function classpathArtifactKey(filePath, sharedDir) {
|
||||||
|
const libDir = path.join(sharedDir, 'libraries');
|
||||||
|
const rel = path.relative(libDir, filePath);
|
||||||
|
if (rel.startsWith('..') || path.isAbsolute(rel)) return path.normalize(filePath).toLowerCase();
|
||||||
|
|
||||||
|
const parts = rel.split(path.sep);
|
||||||
|
if (parts.length < 4) return rel.replace(/\\/g, '/').toLowerCase();
|
||||||
|
|
||||||
|
const fileName = parts[parts.length - 1];
|
||||||
|
const version = parts[parts.length - 2];
|
||||||
|
const artifact = parts[parts.length - 3];
|
||||||
|
const group = parts.slice(0, -3).join('/').toLowerCase();
|
||||||
|
const base = fileName.replace(/\.[^.]+$/, '');
|
||||||
|
const prefix = `${artifact}-${version}`;
|
||||||
|
const classifier = base.startsWith(prefix + '-') ? base.slice(prefix.length + 1).toLowerCase() : '';
|
||||||
|
return `${group}:${artifact.toLowerCase()}:${classifier}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dedupeClasspath(classpath, sharedDir) {
|
||||||
|
const seen = new Set();
|
||||||
|
const out = [];
|
||||||
|
for (const filePath of classpath) {
|
||||||
|
const key = classpathArtifactKey(filePath, sharedDir);
|
||||||
|
if (seen.has(key)) continue;
|
||||||
|
seen.add(key);
|
||||||
|
out.push(filePath);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
// entpackt eine Natives-JAR (ZIP) nach nativesDir (ohne externe Abhängigkeit)
|
// entpackt eine Natives-JAR (ZIP) nach nativesDir (ohne externe Abhängigkeit)
|
||||||
async function extractNatives(jarPath, nativesDir, extractRule) {
|
async function extractNatives(jarPath, nativesDir, extractRule) {
|
||||||
ensureDir(nativesDir);
|
ensureDir(nativesDir);
|
||||||
@@ -223,20 +254,28 @@ async function prepareAssets(vj, sharedDir, onProgress) {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function pickJava(vj, preferredPath) {
|
function pickJava(vj, preferredPath) {
|
||||||
|
const required = (vj.javaVersion && vj.javaVersion.majorVersion) || 8;
|
||||||
|
let preferred = null;
|
||||||
if (preferredPath && preferredPath.trim()) {
|
if (preferredPath && preferredPath.trim()) {
|
||||||
const info = services.probeJava(preferredPath);
|
const info = services.probeJava(preferredPath);
|
||||||
if (info) return { path: preferredPath, major: info.major, forced: true };
|
if (info) {
|
||||||
|
preferred = { path: preferredPath, major: info.major, forced: true };
|
||||||
|
if (info.major === required) return preferred;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const required = (vj.javaVersion && vj.javaVersion.majorVersion) || 8;
|
|
||||||
const jvms = services.scanJava();
|
const jvms = services.scanJava();
|
||||||
// exakt passende oder nächsthöhere 64-bit-Version
|
// Mod-Loader und Mods unterstützen neuere Java-Versionen nicht automatisch.
|
||||||
|
// Deshalb ausschließlich die von Mojang angegebene Major-Version verwenden.
|
||||||
const x64 = jvms.filter((j) => j.arch === 'x64');
|
const x64 = jvms.filter((j) => j.arch === 'x64');
|
||||||
const exact = x64.find((j) => j.major === required);
|
const exact = x64.find((j) => j.major === required);
|
||||||
if (exact) return { path: exact.path, major: exact.major };
|
if (exact) return { path: exact.path, major: exact.major };
|
||||||
const higher = x64.filter((j) => j.major >= required).sort((a, b) => a.major - b.major)[0];
|
const fallback = preferred || x64[0] || jvms[0];
|
||||||
if (higher) return { path: higher.path, major: higher.major };
|
return fallback ? {
|
||||||
const any = jvms[0];
|
path: fallback.path,
|
||||||
return any ? { path: any.path, major: any.major, mismatch: required } : null;
|
major: fallback.major,
|
||||||
|
forced: !!fallback.forced,
|
||||||
|
mismatch: required,
|
||||||
|
} : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -318,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));
|
||||||
|
|
||||||
@@ -343,8 +389,28 @@ async function prepareAndLaunch(opts, onProgress) {
|
|||||||
|
|
||||||
// Java (VOR dem Loader – Forge-Prozessoren brauchen Java)
|
// Java (VOR dem Loader – Forge-Prozessoren brauchen Java)
|
||||||
emit('java', { detail: 'Java wählen …' });
|
emit('java', { detail: 'Java wählen …' });
|
||||||
const java = pickJava(vj, opts.javaPath);
|
let java = pickJava(vj, opts.javaPath);
|
||||||
|
// Nichts Passendes da? Dann darf der Launcher die richtige Fassung selbst
|
||||||
|
// holen – opts.ensureJava wird vom Hauptprozess gestellt.
|
||||||
|
if ((!java || java.mismatch) && opts.ensureJava) {
|
||||||
|
const benoetigt = (vj.javaVersion && vj.javaVersion.majorVersion) || 8;
|
||||||
|
emit('java', { detail: `Java ${benoetigt} wird geladen …` });
|
||||||
|
try {
|
||||||
|
const geholt = await opts.ensureJava(benoetigt, (p) => emit('java', p));
|
||||||
|
if (geholt && geholt.path) java = { path: geholt.path, major: geholt.major };
|
||||||
|
} catch (err) {
|
||||||
|
emit('log', { line: '[Java] Automatischer Download fehlgeschlagen: ' + err.message + '\n' });
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!java) return { ok: false, message: 'Keine Java-Installation gefunden. Bitte in den Einstellungen Java festlegen.' };
|
if (!java) return { ok: false, message: 'Keine Java-Installation gefunden. Bitte in den Einstellungen Java festlegen.' };
|
||||||
|
if (java.mismatch) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
requiredJava: java.mismatch,
|
||||||
|
message: `Minecraft ${vj.id} benötigt Java ${java.mismatch}, gefunden wurde nur Java ${java.major || 'unbekannt'}. `
|
||||||
|
+ 'Bitte Auto-Java aktivieren oder eine passende Java-Installation auswählen.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Mod-Loader
|
// Mod-Loader
|
||||||
let effectiveVj = vj;
|
let effectiveVj = vj;
|
||||||
@@ -405,8 +471,41 @@ async function prepareAndLaunch(opts, onProgress) {
|
|||||||
classpath.unshift(path.join(libDir, rel.replace(/\//g, path.sep)));
|
classpath.unshift(path.join(libDir, rel.replace(/\//g, path.sep)));
|
||||||
}
|
}
|
||||||
effectiveVj = mergeLoader(vj, fvj);
|
effectiveVj = mergeLoader(vj, fvj);
|
||||||
|
|
||||||
|
} else if (opts.loader === 'optifine') {
|
||||||
|
/*
|
||||||
|
* OptiFine hat keine öffentliche Installations-API (Downloads laufen bewusst
|
||||||
|
* über eine Werbeseite). AeroMC lädt hier nichts selbst herunter – der Nutzer
|
||||||
|
* hat den offiziellen Installer bereits einmal sichtbar laufen lassen (siehe
|
||||||
|
* optifine:setup/optifine:confirm in main.js), der dabei ein normales
|
||||||
|
* Versions-Profil unter <sharedDir>/versions/<loaderVersion>/ abgelegt hat,
|
||||||
|
* genau wie Fabric/Forge es tun. Wir lesen es nur noch ein und mergen es.
|
||||||
|
*/
|
||||||
|
const ofId = opts.loaderVersion;
|
||||||
|
if (!ofId) {
|
||||||
|
return { ok: false, message: 'Für diese Instanz ist keine OptiFine-Version hinterlegt. Bitte im Bearbeiten-Dialog einrichten.' };
|
||||||
|
}
|
||||||
|
const ofJsonPath = path.join(opts.sharedDir, 'versions', ofId, ofId + '.json');
|
||||||
|
if (!fs.existsSync(ofJsonPath)) {
|
||||||
|
return { ok: false, message: `OptiFine-Profil "${ofId}" wurde nicht gefunden. Bitte die Einrichtung im Bearbeiten-Dialog wiederholen.` };
|
||||||
|
}
|
||||||
|
let ofvj;
|
||||||
|
try { ofvj = JSON.parse(fs.readFileSync(ofJsonPath, 'utf8')); }
|
||||||
|
catch (err) { return { ok: false, message: 'OptiFine-Profil ist beschädigt: ' + err.message }; }
|
||||||
|
for (const lib of ofvj.libraries || []) {
|
||||||
|
const art = lib.downloads && lib.downloads.artifact;
|
||||||
|
const rel = (art && art.path) || forge.mavenPath(lib.name);
|
||||||
|
const dest = path.join(libDir, rel.replace(/\//g, path.sep));
|
||||||
|
if (!fs.existsSync(dest) && art && art.url) {
|
||||||
|
await downloadIfNeeded(art.url, dest, art.sha1 || null);
|
||||||
|
}
|
||||||
|
classpath.unshift(dest);
|
||||||
|
}
|
||||||
|
effectiveVj = mergeLoader(vj, ofvj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const finalClasspath = dedupeClasspath(classpath, opts.sharedDir);
|
||||||
|
|
||||||
// Ohne Login: alles vorbereitet, aber kein Start (kein Crack-Bypass)
|
// Ohne Login: alles vorbereitet, aber kein Start (kein Crack-Bypass)
|
||||||
if (!opts.auth) {
|
if (!opts.auth) {
|
||||||
return {
|
return {
|
||||||
@@ -425,7 +524,7 @@ async function prepareAndLaunch(opts, onProgress) {
|
|||||||
gameDir: opts.gameDir,
|
gameDir: opts.gameDir,
|
||||||
sharedDir: opts.sharedDir,
|
sharedDir: opts.sharedDir,
|
||||||
nativesDir,
|
nativesDir,
|
||||||
classpath,
|
classpath: finalClasspath,
|
||||||
minMemMb: opts.minMemMb || 2048,
|
minMemMb: opts.minMemMb || 2048,
|
||||||
maxMemMb: opts.maxMemMb || 4096,
|
maxMemMb: opts.maxMemMb || 4096,
|
||||||
extraArgs: opts.extraArgs || '',
|
extraArgs: opts.extraArgs || '',
|
||||||
@@ -434,6 +533,18 @@ 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));
|
||||||
|
// 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) {
|
||||||
|
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);
|
||||||
|
|
||||||
@@ -455,10 +566,33 @@ async function prepareAndLaunch(opts, onProgress) {
|
|||||||
spawnArgs = [...w.slice(1), java.path, ...args];
|
spawnArgs = [...w.slice(1), java.path, ...args];
|
||||||
}
|
}
|
||||||
const child = spawn(cmd, spawnArgs, { cwd: opts.gameDir, detached: false });
|
const child = spawn(cmd, spawnArgs, { cwd: opts.gameDir, detached: false });
|
||||||
child.stdout.on('data', (d) => emit('log', { line: d.toString() }));
|
const shutdownWatchdog = opts.wrapperCommand && opts.wrapperCommand.trim()
|
||||||
child.stderr.on('data', (d) => emit('log', { line: d.toString() }));
|
? null
|
||||||
child.on('exit', (code) => {
|
: processwatchdog.createProcessWatchdog({
|
||||||
emit('exit', { code });
|
isRunning: () => child.exitCode === null && child.signalCode === null,
|
||||||
|
kill: () => child.kill(),
|
||||||
|
onForce: (graceMs) => emit('log', {
|
||||||
|
line: `[Launcher] Regulärer Client-Shutdown war nach ${graceMs / 1000} Sekunden noch nicht beendet; `
|
||||||
|
+ 'verbliebenen Java-Prozess beendet.\n',
|
||||||
|
}),
|
||||||
|
onError: () => emit('log', {
|
||||||
|
line: '[Launcher] Der nach dem regulären Client-Shutdown verbliebene Java-Prozess konnte nicht beendet werden.\n',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
child.stdout.on('data', (d) => {
|
||||||
|
emit('log', { line: d.toString() });
|
||||||
|
if (shutdownWatchdog) shutdownWatchdog.feed(d, 'stdout');
|
||||||
|
});
|
||||||
|
child.stderr.on('data', (d) => {
|
||||||
|
emit('log', { line: d.toString() });
|
||||||
|
if (shutdownWatchdog) shutdownWatchdog.feed(d, 'stderr');
|
||||||
|
});
|
||||||
|
// Ende genau einmal melden – 'exit' und 'error' können beide kommen
|
||||||
|
let beendetGemeldet = false;
|
||||||
|
const meldeEnde = (code, cleanShutdown) => {
|
||||||
|
if (beendetGemeldet) return;
|
||||||
|
beendetGemeldet = true;
|
||||||
|
emit('exit', { code, cleanShutdown: !!cleanShutdown });
|
||||||
// Post-Exit-Befehl
|
// Post-Exit-Befehl
|
||||||
if (opts.postExitCommand && opts.postExitCommand.trim()) {
|
if (opts.postExitCommand && opts.postExitCommand.trim()) {
|
||||||
emit('log', { line: '[Post-Exit] ' + opts.postExitCommand + '\n' });
|
emit('log', { line: '[Post-Exit] ' + opts.postExitCommand + '\n' });
|
||||||
@@ -468,6 +602,20 @@ async function prepareAndLaunch(opts, onProgress) {
|
|||||||
if (po.stderr) emit('log', { line: po.stderr });
|
if (po.stderr) emit('log', { line: po.stderr });
|
||||||
} catch { /* egal */ }
|
} catch { /* egal */ }
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
child.on('exit', (code) => {
|
||||||
|
// vor end() lesen: sawCleanShutdown wird zwar nicht von end()/clear()
|
||||||
|
// zurückgesetzt, aber so bleibt die Reihenfolge unabhängig von internen Details
|
||||||
|
const cleanShutdown = !!(shutdownWatchdog && shutdownWatchdog.sawCleanShutdown);
|
||||||
|
if (shutdownWatchdog) shutdownWatchdog.end();
|
||||||
|
meldeEnde(shutdownWatchdog && shutdownWatchdog.forced ? 0 : code, cleanShutdown);
|
||||||
|
});
|
||||||
|
// ohne diesen Listener würde ein Fehlstart (z. B. falscher Java-Pfad) als
|
||||||
|
// unbehandeltes 'error'-Ereignis den Hauptprozess beenden
|
||||||
|
child.on('error', (err) => {
|
||||||
|
if (shutdownWatchdog) shutdownWatchdog.end();
|
||||||
|
emit('log', { line: '[Fehler] Minecraft konnte nicht gestartet werden: ' + err.message + '\n' });
|
||||||
|
meldeEnde(-1, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
return { ok: true, pid: child.pid, java };
|
return { ok: true, pid: child.pid, java };
|
||||||
@@ -475,5 +623,5 @@ async function prepareAndLaunch(opts, onProgress) {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
prepareAndLaunch, getVersionJson, buildArgs, pickJava, rulesAllow, prepareLibraries, prepareAssets,
|
prepareAndLaunch, getVersionJson, buildArgs, pickJava, rulesAllow, prepareLibraries, prepareAssets,
|
||||||
downloadIfNeeded, extractNatives,
|
downloadIfNeeded, extractNatives, mergeLoader, dedupeClasspath, classpathArtifactKey,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,217 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const os = require('os');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const PROFILE_FILES = [
|
||||||
|
'launcher_profiles.json',
|
||||||
|
'launcher_profiles_microsoft_store.json',
|
||||||
|
'launcher_profiles_microsoft_store_2.json',
|
||||||
|
];
|
||||||
|
|
||||||
|
const ROOT_COPY_DIRS = new Set([
|
||||||
|
'config', 'defaultconfigs', 'kubejs', 'mods', 'resourcepacks', 'screenshots', 'shaderpacks', 'saves',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const ROOT_COPY_FILE_PATTERNS = [
|
||||||
|
/^options.*\.(txt|of)$/i,
|
||||||
|
/^servers\.dat(?:_old)?$/i,
|
||||||
|
/^usercache\.json$/i,
|
||||||
|
/^tl_skin_cape\.json$/i,
|
||||||
|
/^journeymap.*\.(json|txt|cfg)$/i,
|
||||||
|
];
|
||||||
|
|
||||||
|
function readJson(file, fallback) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
|
||||||
|
} catch {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveLauncherRoot(inputPath) {
|
||||||
|
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
||||||
|
const defaultRoot = path.join(appData, '.minecraft');
|
||||||
|
const candidates = [];
|
||||||
|
|
||||||
|
if (inputPath && fs.existsSync(inputPath)) candidates.push(inputPath);
|
||||||
|
candidates.push(defaultRoot);
|
||||||
|
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
for (const profileFile of PROFILE_FILES) {
|
||||||
|
if (fs.existsSync(path.join(candidate, profileFile))) return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveProfileFile(rootDir) {
|
||||||
|
return PROFILE_FILES.map((name) => path.join(rootDir, name)).find((file) => fs.existsSync(file)) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeLoaderVersion(loader, rawValue, mcVersion) {
|
||||||
|
let value = String(rawValue || '').trim();
|
||||||
|
if (!value) return '';
|
||||||
|
|
||||||
|
if (loader === 'forge') {
|
||||||
|
value = value.replace(/^forge-/i, '');
|
||||||
|
if (mcVersion && value.startsWith(mcVersion + '-')) value = value.slice(mcVersion.length + 1);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (loader === 'neoforge') return value.replace(/^neoforge-/i, '');
|
||||||
|
if (loader === 'fabric') {
|
||||||
|
value = value.replace(/^fabric(?:-loader)?-/i, '');
|
||||||
|
if (mcVersion && value.endsWith('-' + mcVersion)) value = value.slice(0, -1 * (mcVersion.length + 1));
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (loader === 'quilt') {
|
||||||
|
value = value.replace(/^quilt(?:-loader)?-/i, '');
|
||||||
|
if (mcVersion && value.endsWith('-' + mcVersion)) value = value.slice(0, -1 * (mcVersion.length + 1));
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLastVersionId(lastVersionId) {
|
||||||
|
const raw = String(lastVersionId || '').trim();
|
||||||
|
if (!raw) return { version: '', loader: 'vanilla', loaderVersion: '' };
|
||||||
|
|
||||||
|
let match = raw.match(/^fabric-loader-([^-]+(?:\.[^-]+)*)-(.+)$/i);
|
||||||
|
if (match) return { version: match[2], loader: 'fabric', loaderVersion: normalizeLoaderVersion('fabric', match[1], match[2]) };
|
||||||
|
|
||||||
|
match = raw.match(/^quilt-loader-([^-]+(?:\.[^-]+)*)-(.+)$/i);
|
||||||
|
if (match) return { version: match[2], loader: 'quilt', loaderVersion: normalizeLoaderVersion('quilt', match[1], match[2]) };
|
||||||
|
|
||||||
|
match = raw.match(/^(.+)-forge-([\w.-]+)$/i);
|
||||||
|
if (match) return { version: match[1], loader: 'forge', loaderVersion: normalizeLoaderVersion('forge', match[2], match[1]) };
|
||||||
|
|
||||||
|
match = raw.match(/^forge-(.+)-([\w.-]+)$/i);
|
||||||
|
if (match) return { version: match[1], loader: 'forge', loaderVersion: normalizeLoaderVersion('forge', match[2], match[1]) };
|
||||||
|
|
||||||
|
match = raw.match(/^(.+)-neoforge-([\w.-]+)$/i);
|
||||||
|
if (match) return { version: match[1], loader: 'neoforge', loaderVersion: normalizeLoaderVersion('neoforge', match[2], match[1]) };
|
||||||
|
|
||||||
|
if (/^\d+(?:\.\d+)+(?:-[\w.]+)?$/i.test(raw)) return { version: raw, loader: 'vanilla', loaderVersion: '' };
|
||||||
|
return { version: raw, loader: 'vanilla', loaderVersion: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJavaSettings(javaDir, javaArgs) {
|
||||||
|
const args = String(javaArgs || '').trim();
|
||||||
|
const minMatch = args.match(/(?:^|\s)-Xms(\d+)([mMgG])/);
|
||||||
|
const maxMatch = args.match(/(?:^|\s)-Xmx(\d+)([mMgG])/);
|
||||||
|
const toMb = (match) => {
|
||||||
|
if (!match) return null;
|
||||||
|
const num = Number(match[1]);
|
||||||
|
if (!Number.isFinite(num)) return null;
|
||||||
|
return match[2].toLowerCase() === 'g' ? num * 1024 : num;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: String(javaDir || '').trim(),
|
||||||
|
minMemMb: toMb(minMatch),
|
||||||
|
maxMemMb: toMb(maxMatch),
|
||||||
|
extraArgs: args
|
||||||
|
.replace(/(?:^|\s)-Xms\d+[mMgG]/g, ' ')
|
||||||
|
.replace(/(?:^|\s)-Xmx\d+[mMgG]/g, ' ')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isImportableProfile(profile) {
|
||||||
|
const type = String((profile && profile.type) || '').trim().toLowerCase();
|
||||||
|
if (type === 'latest-release' || type === 'latest-snapshot') return false;
|
||||||
|
const name = String((profile && profile.name) || '').trim();
|
||||||
|
const lastVersionId = String((profile && profile.lastVersionId) || '').trim();
|
||||||
|
return !!(name || lastVersionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldCopyRootEntry(name) {
|
||||||
|
if (ROOT_COPY_DIRS.has(name)) return true;
|
||||||
|
return ROOT_COPY_FILE_PATTERNS.some((pattern) => pattern.test(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
function scan(inputPath) {
|
||||||
|
const rootDir = resolveLauncherRoot(inputPath);
|
||||||
|
if (!rootDir) return { ok: false, reason: 'not-found' };
|
||||||
|
|
||||||
|
const profileFile = resolveProfileFile(rootDir);
|
||||||
|
const json = readJson(profileFile, {});
|
||||||
|
const profiles = json.profiles || {};
|
||||||
|
const list = [];
|
||||||
|
const defaultRoot = path.normalize(rootDir).toLowerCase();
|
||||||
|
|
||||||
|
for (const [id, profile] of Object.entries(profiles)) {
|
||||||
|
if (!isImportableProfile(profile)) continue;
|
||||||
|
|
||||||
|
const name = String(profile.name || '').trim() || String(profile.lastVersionId || '').trim() || id;
|
||||||
|
const parsed = parseLastVersionId(profile.lastVersionId);
|
||||||
|
const gameDir = path.normalize(String(profile.gameDir || rootDir));
|
||||||
|
const java = parseJavaSettings(profile.javaDir, profile.javaArgs);
|
||||||
|
|
||||||
|
list.push({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
group: '',
|
||||||
|
notes: 'Importiert aus dem Minecraft Launcher',
|
||||||
|
version: parsed.version,
|
||||||
|
loader: parsed.loader,
|
||||||
|
loaderVersion: parsed.loaderVersion,
|
||||||
|
javaPath: java.path,
|
||||||
|
minMemMb: java.minMemMb,
|
||||||
|
maxMemMb: java.maxMemMb,
|
||||||
|
extraJavaArgs: java.extraArgs,
|
||||||
|
gameDir,
|
||||||
|
hasGameData: fs.existsSync(gameDir),
|
||||||
|
usesDefaultGameDir: gameDir.toLowerCase() === defaultRoot,
|
||||||
|
rootDir,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
list.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
return { ok: true, rootDir, count: list.length, instances: list };
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyInto(sourceDir, targetDir, usesDefaultGameDir) {
|
||||||
|
fs.mkdirSync(targetDir, { recursive: true });
|
||||||
|
for (const name of fs.readdirSync(sourceDir)) {
|
||||||
|
if (/^launcher_profiles.*\.json$/i.test(name)) continue;
|
||||||
|
if (name === 'versions' || name === 'libraries' || name === 'assets' || name === 'runtime' || name === 'webcache2') continue;
|
||||||
|
if (usesDefaultGameDir && !shouldCopyRootEntry(name)) continue;
|
||||||
|
fs.cpSync(path.join(sourceDir, name), path.join(targetDir, name), { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function importOne(store, entry, copyData = true) {
|
||||||
|
const created = store.createOrReplaceImportedInstance({
|
||||||
|
name: entry.name,
|
||||||
|
group: entry.group,
|
||||||
|
notes: entry.notes,
|
||||||
|
minecraft: {
|
||||||
|
version: entry.version,
|
||||||
|
loader: entry.loader,
|
||||||
|
loaderVersion: entry.loaderVersion,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const patch = {};
|
||||||
|
if (entry.javaPath || entry.minMemMb || entry.maxMemMb || entry.extraJavaArgs) {
|
||||||
|
patch.java = {
|
||||||
|
path: entry.javaPath || '',
|
||||||
|
minMemMb: entry.minMemMb || null,
|
||||||
|
maxMemMb: entry.maxMemMb || null,
|
||||||
|
extraArgs: entry.extraJavaArgs || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (Object.keys(patch).length) store.updateInstance(created.id, patch);
|
||||||
|
|
||||||
|
let copied = false;
|
||||||
|
if (copyData && entry.gameDir && fs.existsSync(entry.gameDir)) {
|
||||||
|
copyInto(entry.gameDir, store.gameDir(created.id), !!entry.usesDefaultGameDir);
|
||||||
|
copied = true;
|
||||||
|
}
|
||||||
|
return { id: created.id, name: created.name, copied };
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { scan, importOne, parseLastVersionId, resolveLauncherRoot };
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* mcping.js – "Server List Ping" (SLP), das Protokoll, das auch der
|
||||||
|
* Vanilla-Mehrspieler-Bildschirm nutzt, um MOTD/Spielerzahl/Version
|
||||||
|
* abzufragen, ohne sich wirklich zu verbinden.
|
||||||
|
*
|
||||||
|
* Ablauf (siehe minecraft.wiki/w/Server_List_Ping):
|
||||||
|
* 1) Handshake-Paket senden (Protokollversion, Adresse, Port, next state=1)
|
||||||
|
* 2) Status-Request-Paket senden (leer)
|
||||||
|
* 3) Status-Response-Paket empfangen: enthält einen JSON-String
|
||||||
|
* Alle Pakete sind mit ihrer Länge als VarInt vorangestellt.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const net = require('net');
|
||||||
|
|
||||||
|
function encodeVarInt(value) {
|
||||||
|
const bytes = [];
|
||||||
|
let v = value;
|
||||||
|
do {
|
||||||
|
let temp = v & 0b01111111;
|
||||||
|
v = v >>> 7;
|
||||||
|
if (v !== 0) temp |= 0b10000000;
|
||||||
|
bytes.push(temp);
|
||||||
|
} while (v !== 0);
|
||||||
|
return Buffer.from(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeString(str) {
|
||||||
|
const strBuf = Buffer.from(str, 'utf8');
|
||||||
|
return Buffer.concat([encodeVarInt(strBuf.length), strBuf]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeUShort(n) {
|
||||||
|
const b = Buffer.alloc(2);
|
||||||
|
b.writeUInt16BE(n & 0xffff, 0);
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
// liest einen VarInt aus buf ab offset -> { value, next } oder null,
|
||||||
|
// wenn noch nicht genug Bytes angekommen sind
|
||||||
|
function decodeVarInt(buf, offset) {
|
||||||
|
let numRead = 0;
|
||||||
|
let result = 0;
|
||||||
|
let read;
|
||||||
|
do {
|
||||||
|
if (offset + numRead >= buf.length) return null;
|
||||||
|
read = buf[offset + numRead];
|
||||||
|
result |= (read & 0b01111111) << (7 * numRead);
|
||||||
|
numRead++;
|
||||||
|
if (numRead > 5) throw new Error('VarInt zu groß');
|
||||||
|
} while ((read & 0b10000000) !== 0);
|
||||||
|
return { value: result >>> 0, next: offset + numRead };
|
||||||
|
}
|
||||||
|
|
||||||
|
// "description" kann ein reiner String oder eine Chat-Komponente
|
||||||
|
// ({text, extra:[...]}) sein – hier auf reinen Text reduzieren
|
||||||
|
function extractMotdText(desc) {
|
||||||
|
if (!desc) return '';
|
||||||
|
if (typeof desc === 'string') return desc;
|
||||||
|
let text = String(desc.text || '');
|
||||||
|
if (Array.isArray(desc.extra)) {
|
||||||
|
for (const part of desc.extra) text += extractMotdText(part);
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formatierungscodes (§a, §l, …) sind fürs Spiel gedacht, nicht für unsere Liste
|
||||||
|
function stripFormatting(text) {
|
||||||
|
return String(text || '').replace(/§./g, '').replace(/\s+/g, ' ').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Fragt einen Server ab.
|
||||||
|
* -> { ok, motd, version, protocol, online, max, sample, favicon, latencyMs }
|
||||||
|
* oder { ok: false, message }
|
||||||
|
*/
|
||||||
|
function pingServer(host, port = 25565, timeoutMs = 4000) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const start = Date.now();
|
||||||
|
const socket = new net.Socket();
|
||||||
|
let buffer = Buffer.alloc(0);
|
||||||
|
let finished = false;
|
||||||
|
|
||||||
|
const done = (result) => {
|
||||||
|
if (finished) return;
|
||||||
|
finished = true;
|
||||||
|
try { socket.destroy(); } catch { /* egal */ }
|
||||||
|
resolve(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.setTimeout(timeoutMs);
|
||||||
|
socket.on('timeout', () => done({ ok: false, message: 'Zeitüberschreitung.' }));
|
||||||
|
socket.on('error', (err) => done({ ok: false, message: err.code === 'ENOTFOUND' ? 'Adresse nicht gefunden.' : err.message }));
|
||||||
|
|
||||||
|
socket.connect(port, host, () => {
|
||||||
|
const handshakeBody = Buffer.concat([
|
||||||
|
encodeVarInt(0x00),
|
||||||
|
encodeVarInt(-1), // Protokollversion "unbekannt" – für den Status-Abfrage-Zweck egal
|
||||||
|
encodeString(host),
|
||||||
|
encodeUShort(port),
|
||||||
|
encodeVarInt(1), // next state: 1 = status
|
||||||
|
]);
|
||||||
|
const handshakePacket = Buffer.concat([encodeVarInt(handshakeBody.length), handshakeBody]);
|
||||||
|
|
||||||
|
const requestBody = encodeVarInt(0x00);
|
||||||
|
const requestPacket = Buffer.concat([encodeVarInt(requestBody.length), requestBody]);
|
||||||
|
|
||||||
|
socket.write(Buffer.concat([handshakePacket, requestPacket]));
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('data', (chunk) => {
|
||||||
|
if (finished) return;
|
||||||
|
buffer = Buffer.concat([buffer, chunk]);
|
||||||
|
try {
|
||||||
|
const lenInfo = decodeVarInt(buffer, 0);
|
||||||
|
if (!lenInfo) return; // Länge noch nicht vollständig angekommen
|
||||||
|
const packetEnd = lenInfo.next + lenInfo.value;
|
||||||
|
if (buffer.length < packetEnd) return; // Paket noch nicht vollständig da
|
||||||
|
|
||||||
|
const idInfo = decodeVarInt(buffer, lenInfo.next);
|
||||||
|
if (!idInfo) return;
|
||||||
|
const jsonLenInfo = decodeVarInt(buffer, idInfo.next);
|
||||||
|
if (!jsonLenInfo) return;
|
||||||
|
const jsonStart = jsonLenInfo.next;
|
||||||
|
const jsonEnd = jsonStart + jsonLenInfo.value;
|
||||||
|
if (buffer.length < jsonEnd) return;
|
||||||
|
|
||||||
|
const data = JSON.parse(buffer.toString('utf8', jsonStart, jsonEnd));
|
||||||
|
done({
|
||||||
|
ok: true,
|
||||||
|
latencyMs: Date.now() - start,
|
||||||
|
motd: stripFormatting(extractMotdText(data.description)),
|
||||||
|
version: (data.version && data.version.name) || '',
|
||||||
|
protocol: data.version ? data.version.protocol : null,
|
||||||
|
online: data.players ? data.players.online : null,
|
||||||
|
max: data.players ? data.players.max : null,
|
||||||
|
sample: (data.players && Array.isArray(data.players.sample))
|
||||||
|
? data.players.sample.map((p) => String(p.name || '')).filter(Boolean)
|
||||||
|
: [],
|
||||||
|
favicon: typeof data.favicon === 'string' ? data.favicon : '',
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
done({ ok: false, message: 'Ungültige Antwort: ' + err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { pingServer };
|
||||||
@@ -66,6 +66,38 @@ function resolveInstancesDir(inputPath) {
|
|||||||
return fs.existsSync(inputPath) ? inputPath : null;
|
return fs.existsSync(inputPath) ? inputPath : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Liest einen einzelnen Instanzordner aus. null, wenn dort keine Instanz liegt.
|
||||||
|
* Wird sowohl beim Ordner-Import als auch beim ZIP-Import verwendet.
|
||||||
|
*/
|
||||||
|
function readInstance(dir, folderName, group) {
|
||||||
|
const cfgFile = path.join(dir, 'instance.cfg');
|
||||||
|
if (!fs.existsSync(cfgFile)) return null; // kein Instanzordner
|
||||||
|
|
||||||
|
const cfg = parseCfg(cfgFile);
|
||||||
|
const pack = readPack(dir);
|
||||||
|
// MultiMC nutzt .minecraft, Prism minecraft
|
||||||
|
const gameDir = ['.minecraft', 'minecraft']
|
||||||
|
.map((n) => path.join(dir, n))
|
||||||
|
.find((p) => fs.existsSync(p)) || null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
folder: folderName,
|
||||||
|
dir,
|
||||||
|
name: cfg.name || folderName,
|
||||||
|
group: group || '',
|
||||||
|
notes: cfg.notes || '',
|
||||||
|
version: pack.version || cfg.IntendedVersion || '',
|
||||||
|
loader: pack.loader,
|
||||||
|
loaderVersion: pack.loaderVersion,
|
||||||
|
javaPath: cfg.OverrideJavaLocation === 'true' ? (cfg.JavaPath || '') : '',
|
||||||
|
minMemMb: cfg.OverrideMemory === 'true' ? Number(cfg.MinMemAlloc) || null : null,
|
||||||
|
maxMemMb: cfg.OverrideMemory === 'true' ? Number(cfg.MaxMemAlloc) || null : null,
|
||||||
|
gameDir,
|
||||||
|
hasGameData: !!gameDir,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Listet die gefundenen Instanzen auf (ohne etwas zu kopieren).
|
* Listet die gefundenen Instanzen auf (ohne etwas zu kopieren).
|
||||||
*/
|
*/
|
||||||
@@ -77,43 +109,51 @@ function scan(inputPath) {
|
|||||||
const list = [];
|
const list = [];
|
||||||
for (const entry of fs.readdirSync(instancesDir, { withFileTypes: true })) {
|
for (const entry of fs.readdirSync(instancesDir, { withFileTypes: true })) {
|
||||||
if (!entry.isDirectory()) continue;
|
if (!entry.isDirectory()) continue;
|
||||||
const dir = path.join(instancesDir, entry.name);
|
const inst = readInstance(path.join(instancesDir, entry.name), entry.name, groups[entry.name]);
|
||||||
const cfgFile = path.join(dir, 'instance.cfg');
|
if (inst) list.push(inst);
|
||||||
if (!fs.existsSync(cfgFile)) continue; // kein Instanzordner
|
|
||||||
|
|
||||||
const cfg = parseCfg(cfgFile);
|
|
||||||
const pack = readPack(dir);
|
|
||||||
// MultiMC nutzt .minecraft, Prism minecraft
|
|
||||||
const gameDir = ['.minecraft', 'minecraft']
|
|
||||||
.map((n) => path.join(dir, n))
|
|
||||||
.find((p) => fs.existsSync(p)) || null;
|
|
||||||
|
|
||||||
list.push({
|
|
||||||
folder: entry.name,
|
|
||||||
dir,
|
|
||||||
name: cfg.name || entry.name,
|
|
||||||
group: groups[entry.name] || '',
|
|
||||||
notes: cfg.notes || '',
|
|
||||||
version: pack.version || cfg.IntendedVersion || '',
|
|
||||||
loader: pack.loader,
|
|
||||||
loaderVersion: pack.loaderVersion,
|
|
||||||
javaPath: cfg.OverrideJavaLocation === 'true' ? (cfg.JavaPath || '') : '',
|
|
||||||
minMemMb: cfg.OverrideMemory === 'true' ? Number(cfg.MinMemAlloc) || null : null,
|
|
||||||
maxMemMb: cfg.OverrideMemory === 'true' ? Number(cfg.MaxMemAlloc) || null : null,
|
|
||||||
gameDir,
|
|
||||||
hasGameData: !!gameDir,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
list.sort((a, b) => a.name.localeCompare(b.name));
|
list.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
return { ok: true, instancesDir, count: list.length, instances: list };
|
return { ok: true, instancesDir, count: list.length, instances: list };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Sucht einen Instanzordner unterhalb von root. Deckt beide Zip-Varianten ab:
|
||||||
|
* mit umgebendem Ordner ("26.2/instance.cfg") und ohne ("instance.cfg").
|
||||||
|
* Die Tiefe ist begrenzt, damit nicht das ganze .minecraft durchsucht wird.
|
||||||
|
*/
|
||||||
|
function findInstanceDir(root, tiefe = 2) {
|
||||||
|
if (!root || !fs.existsSync(root)) return null;
|
||||||
|
if (fs.existsSync(path.join(root, 'instance.cfg'))) return root;
|
||||||
|
if (tiefe <= 0) return null;
|
||||||
|
let eintraege = [];
|
||||||
|
try { eintraege = fs.readdirSync(root, { withFileTypes: true }); } catch { return null; }
|
||||||
|
for (const e of eintraege) {
|
||||||
|
if (!e.isDirectory()) continue;
|
||||||
|
// .minecraft enthält nie eine Instanz und ist der mit Abstand größte Ordner
|
||||||
|
if (e.name === '.minecraft' || e.name === 'minecraft') continue;
|
||||||
|
const treffer = findInstanceDir(path.join(root, e.name), tiefe - 1);
|
||||||
|
if (treffer) return treffer;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Liest eine einzelne, ausgepackte MultiMC/Prism-Instanz.
|
||||||
|
* -> { ok, instance } oder { ok: false, reason: 'not-found' }
|
||||||
|
*/
|
||||||
|
function scanSingle(root) {
|
||||||
|
const dir = findInstanceDir(root);
|
||||||
|
if (!dir) return { ok: false, reason: 'not-found' };
|
||||||
|
const inst = readInstance(dir, path.basename(dir), '');
|
||||||
|
return inst ? { ok: true, instance: inst } : { ok: false, reason: 'not-found' };
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Übernimmt eine Instanz in den Launcher.
|
* Übernimmt eine Instanz in den Launcher.
|
||||||
* store: unser Datenspeicher; copyData: Spielverzeichnis mitkopieren
|
* store: unser Datenspeicher; copyData: Spielverzeichnis mitkopieren
|
||||||
*/
|
*/
|
||||||
function importOne(store, entry, copyData = true) {
|
function importOne(store, entry, copyData = true) {
|
||||||
const created = store.createInstance({
|
const created = store.createOrReplaceImportedInstance({
|
||||||
name: entry.name,
|
name: entry.name,
|
||||||
group: entry.group,
|
group: entry.group,
|
||||||
notes: entry.notes,
|
notes: entry.notes,
|
||||||
@@ -145,4 +185,4 @@ function importOne(store, entry, copyData = true) {
|
|||||||
return { id: created.id, name: created.name, copied };
|
return { id: created.id, name: created.name, copied };
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { scan, importOne, resolveInstancesDir };
|
module.exports = { scan, scanSingle, importOne, resolveInstancesDir, readInstance, findInstanceDir };
|
||||||
|
|||||||
@@ -0,0 +1,437 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* mrimport.js – Instanzen aus der Modrinth App übernehmen
|
||||||
|
* --------------------------------------------------------
|
||||||
|
* Die Modrinth App legt ihre Daten unter %APPDATA%\ModrinthApp ab (sehr alte
|
||||||
|
* Fassungen: %APPDATA%\com.modrinth.theseus). Die Spielordner liegen in
|
||||||
|
* "profiles\<Ordner>", die Angaben dazu (Name, Version, Loader, Gruppen,
|
||||||
|
* Arbeitsspeicher …) in der SQLite-Datenbank "app.db". Wurde der Datenordner in
|
||||||
|
* der App verlegt, steht der neue Ort in settings.custom_dir.
|
||||||
|
*
|
||||||
|
* Unterstützte Datenbank-Fassungen:
|
||||||
|
* - ab Mitte 2026: Tabelle "instances" + "instance_content_sets" (Version/Loader),
|
||||||
|
* Gruppen in "instance_group_memberships"/"instance_groups",
|
||||||
|
* Starteinstellungen als JSONB in "instance_launch_overrides"
|
||||||
|
* - 2024 bis Mitte 2026: eine Tabelle "profiles"
|
||||||
|
* - vorher (ohne app.db): je Instanz eine profile.json
|
||||||
|
*
|
||||||
|
* Gelesen wird mit sql.js (SQLite als WebAssembly) auf einer Kopie im Speicher –
|
||||||
|
* die Datenbank der Modrinth App wird nie verändert.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const os = require('os');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const LOADER = new Set(['vanilla', 'fabric', 'forge', 'neoforge', 'quilt']);
|
||||||
|
|
||||||
|
let sqlPromise = null;
|
||||||
|
function sqlJs() {
|
||||||
|
if (!sqlPromise) {
|
||||||
|
const initSqlJs = require('sql.js');
|
||||||
|
const wasmDir = path.dirname(require.resolve('sql.js'));
|
||||||
|
sqlPromise = initSqlJs({ locateFile: (datei) => path.join(wasmDir, datei) });
|
||||||
|
}
|
||||||
|
return sqlPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appDatenOrdner() {
|
||||||
|
const appdata = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
||||||
|
return [path.join(appdata, 'ModrinthApp'), path.join(appdata, 'com.modrinth.theseus')];
|
||||||
|
}
|
||||||
|
|
||||||
|
function standardOrdner() {
|
||||||
|
return appDatenOrdner().find((d) => fs.existsSync(d)) || appDatenOrdner()[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function readJson(file) {
|
||||||
|
try { return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^/, '')); } catch { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aus der Eingabe (App-Ordner, profiles-Ordner oder leer) die Orte ermitteln
|
||||||
|
function ermittleOrte(eingabe) {
|
||||||
|
let basis = eingabe && eingabe.trim() ? eingabe.trim() : standardOrdner();
|
||||||
|
if (!fs.existsSync(basis)) return null;
|
||||||
|
// direkt den profiles-Ordner angegeben -> eine Ebene höher liegt die app.db
|
||||||
|
if (path.basename(basis).toLowerCase() === 'profiles' && !fs.existsSync(path.join(basis, 'app.db'))) {
|
||||||
|
const hoeher = path.dirname(basis);
|
||||||
|
if (fs.existsSync(path.join(hoeher, 'app.db')) || fs.existsSync(path.join(hoeher, 'profiles'))) basis = hoeher;
|
||||||
|
}
|
||||||
|
const db = path.join(basis, 'app.db');
|
||||||
|
return { basis, db: fs.existsSync(db) ? db : null };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Datenbank ----------
|
||||||
|
/*
|
||||||
|
* Läuft die Modrinth App, stehen die neuesten Änderungen oft noch im
|
||||||
|
* Schreibprotokoll (app.db-wal) statt in app.db. Dessen abgeschlossene
|
||||||
|
* Transaktionen spielen wir hier selbst auf die Kopie im Speicher ein – nach
|
||||||
|
* dem SQLite-Dateiformat (Kopf 32 Bytes, je Rahmen 24 Bytes Kopf + eine Seite),
|
||||||
|
* mit Prüfung von Salz und laufender Prüfsumme wie SQLite selbst.
|
||||||
|
* -> { daten, rahmen } oder null, wenn das Protokoll nicht passt
|
||||||
|
*/
|
||||||
|
function spieleWalEin(daten, walPfad) {
|
||||||
|
let wal;
|
||||||
|
try { wal = fs.readFileSync(walPfad); } catch { return null; }
|
||||||
|
if (wal.length < 32) return null;
|
||||||
|
const magie = wal.readUInt32BE(0);
|
||||||
|
if (magie !== 0x377f0682 && magie !== 0x377f0683) return null;
|
||||||
|
const bigEndian = magie === 0x377f0683;
|
||||||
|
const seite = wal.readUInt32BE(8);
|
||||||
|
const dbSeite = daten.readUInt16BE(16) === 1 ? 65536 : daten.readUInt16BE(16);
|
||||||
|
if (!seite || seite !== dbSeite) return null;
|
||||||
|
const salz1 = wal.readUInt32BE(16), salz2 = wal.readUInt32BE(20);
|
||||||
|
|
||||||
|
const wort = (buf, i) => (bigEndian ? buf.readUInt32BE(i) : buf.readUInt32LE(i));
|
||||||
|
let s0 = 0, s1 = 0;
|
||||||
|
const pruefe = (buf, start, ende) => {
|
||||||
|
for (let i = start; i < ende; i += 8) {
|
||||||
|
s0 = (s0 + wort(buf, i) + s1) >>> 0;
|
||||||
|
s1 = (s1 + wort(buf, i + 4) + s0) >>> 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
pruefe(wal, 0, 24);
|
||||||
|
if (s0 !== wal.readUInt32BE(24) || s1 !== wal.readUInt32BE(28)) return null;
|
||||||
|
|
||||||
|
const fertig = new Map(); // Seitennummer -> Inhalt (abgeschlossene Transaktionen)
|
||||||
|
let offen = new Map();
|
||||||
|
let seiten = Math.floor(daten.length / seite);
|
||||||
|
let rahmen = 0;
|
||||||
|
for (let pos = 32; pos + 24 + seite <= wal.length; pos += 24 + seite) {
|
||||||
|
if (wal.readUInt32BE(pos + 8) !== salz1 || wal.readUInt32BE(pos + 12) !== salz2) break;
|
||||||
|
pruefe(wal, pos, pos + 8);
|
||||||
|
pruefe(wal, pos + 24, pos + 24 + seite);
|
||||||
|
if (s0 !== wal.readUInt32BE(pos + 16) || s1 !== wal.readUInt32BE(pos + 20)) break;
|
||||||
|
offen.set(wal.readUInt32BE(pos), wal.subarray(pos + 24, pos + 24 + seite));
|
||||||
|
const nachCommit = wal.readUInt32BE(pos + 4);
|
||||||
|
if (nachCommit) { // Transaktion abgeschlossen
|
||||||
|
for (const [nr, inhalt] of offen) fertig.set(nr, inhalt);
|
||||||
|
offen = new Map();
|
||||||
|
seiten = nachCommit;
|
||||||
|
rahmen++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!rahmen) return null;
|
||||||
|
const neu = Buffer.alloc(seiten * seite);
|
||||||
|
daten.copy(neu, 0, 0, Math.min(daten.length, neu.length));
|
||||||
|
for (const [nr, inhalt] of fertig) if (nr >= 1 && nr <= seiten) inhalt.copy(neu, (nr - 1) * seite);
|
||||||
|
return { daten: neu, rahmen };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function oeffneDb(dbPfad) {
|
||||||
|
const SQL = await sqlJs();
|
||||||
|
let daten = fs.readFileSync(dbPfad);
|
||||||
|
let walEingespielt = false;
|
||||||
|
if (daten.length > 100 && daten[18] === 2) {
|
||||||
|
const mitWal = spieleWalEin(daten, dbPfad + '-wal');
|
||||||
|
if (mitWal) { daten = mitWal.daten; walEingespielt = true; }
|
||||||
|
// WAL-Datenbanken lassen sich ohne geteilten Speicher nicht öffnen – die
|
||||||
|
// Kopie im Speicher als normale Datenbank kennzeichnen (Bytes 18/19)
|
||||||
|
daten[18] = 1; daten[19] = 1;
|
||||||
|
}
|
||||||
|
const db = new SQL.Database(daten);
|
||||||
|
db.walEingespielt = walEingespielt;
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
|
function zeilen(db, sql, params) {
|
||||||
|
const st = db.prepare(sql);
|
||||||
|
try {
|
||||||
|
if (params) st.bind(params);
|
||||||
|
const out = [];
|
||||||
|
while (st.step()) out.push(st.getAsObject());
|
||||||
|
return out;
|
||||||
|
} finally {
|
||||||
|
st.free();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function tabellen(db) {
|
||||||
|
return new Set(zeilen(db, "SELECT name FROM sqlite_master WHERE type = 'table'").map((r) => r.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
function spalten(db, tabelle) {
|
||||||
|
return new Set(zeilen(db, `PRAGMA table_info(${tabelle})`).map((r) => r.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsonFeld(wert) {
|
||||||
|
if (wert === null || wert === undefined || wert === '') return null;
|
||||||
|
try { return JSON.parse(wert); } catch { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spalte nur abfragen, wenn es sie gibt (ältere Fassungen kennen nicht alle)
|
||||||
|
function sp(vorhanden, ausdruck, name, ersatz = 'NULL') {
|
||||||
|
return vorhanden.has(name) ? `${ausdruck} AS ${name}` : `${ersatz} AS ${name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function eigenerOrdner(db, t) {
|
||||||
|
if (!t.has('settings') || !spalten(db, 'settings').has('custom_dir')) return null;
|
||||||
|
const r = zeilen(db, 'SELECT custom_dir FROM settings LIMIT 1')[0];
|
||||||
|
return r && r.custom_dir ? String(r.custom_dir) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function leseNeu(db, t) {
|
||||||
|
const si = spalten(db, 'instances');
|
||||||
|
const liste = zeilen(db, `
|
||||||
|
SELECT i.id AS id, i.path AS path, i.name AS name,
|
||||||
|
${sp(si, 'i.icon_path', 'icon_path')}, ${sp(si, 'i.install_stage', 'install_stage')},
|
||||||
|
${sp(si, 'i.last_played', 'last_played')},
|
||||||
|
${sp(si, 'i.submitted_time_played', 'submitted_time_played', '0')},
|
||||||
|
${sp(si, 'i.recent_time_played', 'recent_time_played', '0')},
|
||||||
|
cs.game_version AS game_version, cs.loader AS loader, cs.loader_version AS loader_version
|
||||||
|
FROM instances i
|
||||||
|
LEFT JOIN instance_content_sets cs ON cs.id = i.applied_content_set_id`);
|
||||||
|
|
||||||
|
const overrides = new Map();
|
||||||
|
if (t.has('instance_launch_overrides')) {
|
||||||
|
for (const r of zeilen(db, 'SELECT instance_id, json(overrides) AS o FROM instance_launch_overrides')) {
|
||||||
|
overrides.set(r.instance_id, jsonFeld(r.o) || {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const gruppen = new Map();
|
||||||
|
const merke = (id, name) => { if (name && !gruppen.has(id)) gruppen.set(id, String(name)); };
|
||||||
|
if (t.has('instance_group_memberships') && t.has('instance_groups')) {
|
||||||
|
for (const r of zeilen(db, `SELECT m.instance_id AS id, g.name AS name FROM instance_group_memberships m
|
||||||
|
JOIN instance_groups g ON g.id = m.group_id ORDER BY g.display_order`)) merke(r.id, r.name);
|
||||||
|
} else if (t.has('instance_groups') && spalten(db, 'instance_groups').has('group_name')) {
|
||||||
|
for (const r of zeilen(db, 'SELECT instance_id AS id, group_name AS name FROM instance_groups ORDER BY group_name')) merke(r.id, r.name);
|
||||||
|
}
|
||||||
|
const links = new Map();
|
||||||
|
if (t.has('instance_links')) {
|
||||||
|
for (const r of zeilen(db, 'SELECT instance_id, link_kind, modrinth_project_id, modrinth_version_id FROM instance_links')) {
|
||||||
|
if (r.modrinth_project_id && r.modrinth_version_id) links.set(r.instance_id, { projectId: r.modrinth_project_id, versionId: r.modrinth_version_id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return liste.map((r) => {
|
||||||
|
const o = overrides.get(r.id) || {};
|
||||||
|
const hooks = o.hooks || {};
|
||||||
|
return {
|
||||||
|
path: r.path,
|
||||||
|
name: r.name,
|
||||||
|
iconPath: r.icon_path,
|
||||||
|
installiert: !r.install_stage || r.install_stage === 'installed',
|
||||||
|
lastPlayed: r.last_played,
|
||||||
|
sekunden: (Number(r.submitted_time_played) || 0) + (Number(r.recent_time_played) || 0),
|
||||||
|
gameVersion: r.game_version,
|
||||||
|
loader: r.loader,
|
||||||
|
loaderVersion: r.loader_version,
|
||||||
|
gruppe: gruppen.get(r.id) || '',
|
||||||
|
javaPath: o.java_path || '',
|
||||||
|
maxMemMb: o.memory && Number(o.memory.maximum) ? Number(o.memory.maximum) : null,
|
||||||
|
extraArgs: Array.isArray(o.extra_launch_args) ? o.extra_launch_args.join(' ') : '',
|
||||||
|
vorStart: hooks.pre_launch || '',
|
||||||
|
nachEnde: hooks.post_exit || '',
|
||||||
|
modpack: links.get(r.id) || null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function leseProfile(db) {
|
||||||
|
const s = spalten(db, 'profiles');
|
||||||
|
const liste = zeilen(db, `SELECT path, name,
|
||||||
|
${sp(s, 'icon_path', 'icon_path')}, ${sp(s, 'install_stage', 'install_stage')},
|
||||||
|
${sp(s, 'game_version', 'game_version')}, ${sp(s, 'mod_loader', 'mod_loader')},
|
||||||
|
${sp(s, 'mod_loader_version', 'mod_loader_version')}, ${sp(s, 'json(groups)', 'groups')},
|
||||||
|
${sp(s, 'last_played', 'last_played')},
|
||||||
|
${sp(s, 'submitted_time_played', 'submitted_time_played', '0')},
|
||||||
|
${sp(s, 'recent_time_played', 'recent_time_played', '0')},
|
||||||
|
${sp(s, 'override_java_path', 'override_java_path')},
|
||||||
|
${sp(s, 'json(override_extra_launch_args)', 'override_extra_launch_args')},
|
||||||
|
${sp(s, 'override_mc_memory_max', 'override_mc_memory_max')},
|
||||||
|
${sp(s, 'override_hook_pre_launch', 'override_hook_pre_launch')},
|
||||||
|
${sp(s, 'override_hook_post_exit', 'override_hook_post_exit')},
|
||||||
|
${sp(s, 'linked_project_id', 'linked_project_id')}, ${sp(s, 'linked_version_id', 'linked_version_id')}
|
||||||
|
FROM profiles`);
|
||||||
|
return liste.map((r) => {
|
||||||
|
const gruppen = jsonFeld(r.groups);
|
||||||
|
const args = jsonFeld(r.override_extra_launch_args);
|
||||||
|
return {
|
||||||
|
path: r.path,
|
||||||
|
name: r.name,
|
||||||
|
iconPath: r.icon_path,
|
||||||
|
installiert: !r.install_stage || r.install_stage === 'installed',
|
||||||
|
lastPlayed: r.last_played,
|
||||||
|
sekunden: (Number(r.submitted_time_played) || 0) + (Number(r.recent_time_played) || 0),
|
||||||
|
gameVersion: r.game_version,
|
||||||
|
loader: r.mod_loader,
|
||||||
|
loaderVersion: r.mod_loader_version,
|
||||||
|
gruppe: Array.isArray(gruppen) && gruppen.length ? String(gruppen[0]) : '',
|
||||||
|
javaPath: r.override_java_path || '',
|
||||||
|
maxMemMb: Number(r.override_mc_memory_max) || null,
|
||||||
|
extraArgs: Array.isArray(args) ? args.join(' ') : '',
|
||||||
|
vorStart: r.override_hook_pre_launch || '',
|
||||||
|
nachEnde: r.override_hook_post_exit || '',
|
||||||
|
modpack: r.linked_project_id && r.linked_version_id ? { projectId: r.linked_project_id, versionId: r.linked_version_id } : null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// sehr alte Fassungen: profiles\<Ordner>\profile.json
|
||||||
|
function leseProfileJson(profilesDir) {
|
||||||
|
const liste = [];
|
||||||
|
for (const e of fs.readdirSync(profilesDir, { withFileTypes: true })) {
|
||||||
|
if (!e.isDirectory()) continue;
|
||||||
|
const p = readJson(path.join(profilesDir, e.name, 'profile.json'));
|
||||||
|
if (!p) continue;
|
||||||
|
const m = p.metadata || p;
|
||||||
|
const lv = m.loader_version;
|
||||||
|
liste.push({
|
||||||
|
path: e.name,
|
||||||
|
name: m.name || e.name,
|
||||||
|
iconPath: m.icon || null,
|
||||||
|
installiert: !p.install_stage || p.install_stage === 'installed',
|
||||||
|
lastPlayed: m.last_played || null,
|
||||||
|
sekunden: 0,
|
||||||
|
gameVersion: m.game_version,
|
||||||
|
loader: m.loader,
|
||||||
|
loaderVersion: lv && typeof lv === 'object' ? lv.id : lv,
|
||||||
|
gruppe: Array.isArray(m.groups) && m.groups.length ? String(m.groups[0]) : '',
|
||||||
|
javaPath: (p.java && (p.java.override_version && p.java.override_version.path)) || '',
|
||||||
|
maxMemMb: p.memory && Number(p.memory.maximum) ? Number(p.memory.maximum) : null,
|
||||||
|
extraArgs: p.java && Array.isArray(p.java.extra_arguments) ? p.java.extra_arguments.join(' ') : '',
|
||||||
|
vorStart: (p.hooks && p.hooks.pre_launch) || '',
|
||||||
|
nachEnde: (p.hooks && p.hooks.post_exit) || '',
|
||||||
|
modpack: m.linked_data && m.linked_data.project_id && m.linked_data.version_id
|
||||||
|
? { projectId: m.linked_data.project_id, versionId: m.linked_data.version_id } : null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return liste;
|
||||||
|
}
|
||||||
|
|
||||||
|
// "1.20.1-47.2.0" -> "47.2.0" usw. – AeroMC erwartet die reine Loader-Version
|
||||||
|
function loaderVersion(loader, wert, mc) {
|
||||||
|
let v = String(wert || '').trim();
|
||||||
|
if (!v || v === 'stable' || v === 'latest') return '';
|
||||||
|
if (loader === 'forge') {
|
||||||
|
v = v.replace(/^forge-/i, '');
|
||||||
|
if (mc && v.startsWith(mc + '-')) v = v.slice(mc.length + 1);
|
||||||
|
} else if (loader === 'neoforge') {
|
||||||
|
v = v.replace(/^neoforge-/i, '');
|
||||||
|
} else if (loader === 'fabric' || loader === 'quilt') {
|
||||||
|
v = v.replace(/^(fabric|quilt)(-loader)?-/i, '');
|
||||||
|
if (mc && v.endsWith('-' + mc)) v = v.slice(0, -(mc.length + 1));
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
function zeitstempel(wert) {
|
||||||
|
if (!wert) return null;
|
||||||
|
const n = Number(wert);
|
||||||
|
if (Number.isFinite(n) && n > 0) return new Date(n < 1e12 ? n * 1000 : n).toISOString();
|
||||||
|
const d = new Date(wert);
|
||||||
|
return Number.isNaN(d.getTime()) ? null : d.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Durchsucht die Modrinth App. Leere Eingabe = Standardort.
|
||||||
|
* -> { ok, basis, count, instances, hinweis }
|
||||||
|
*/
|
||||||
|
async function scan(eingabe) {
|
||||||
|
const orte = ermittleOrte(eingabe);
|
||||||
|
if (!orte) return { ok: false, reason: 'not-found' };
|
||||||
|
|
||||||
|
let roh = [];
|
||||||
|
let profilesDir = path.join(orte.basis, 'profiles');
|
||||||
|
let hinweis = '';
|
||||||
|
if (orte.db) {
|
||||||
|
const db = await oeffneDb(orte.db);
|
||||||
|
try {
|
||||||
|
const t = tabellen(db);
|
||||||
|
const eigener = eigenerOrdner(db, t);
|
||||||
|
if (eigener && fs.existsSync(path.join(eigener, 'profiles'))) profilesDir = path.join(eigener, 'profiles');
|
||||||
|
if (t.has('instances')) roh = leseNeu(db, t);
|
||||||
|
else if (t.has('profiles')) roh = leseProfile(db);
|
||||||
|
} finally {
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
// Schreibprotokoll vorhanden, ließ sich aber nicht einspielen -> Kopie
|
||||||
|
// ist evtl. nicht ganz aktuell
|
||||||
|
try {
|
||||||
|
const wal = orte.db + '-wal';
|
||||||
|
if (!db.walEingespielt && fs.existsSync(wal) && fs.statSync(wal).size > 32) {
|
||||||
|
hinweis = 'Die Modrinth App scheint gerade zu laufen – schließe sie, damit auch die neuesten Änderungen übernommen werden.';
|
||||||
|
}
|
||||||
|
} catch { /* egal */ }
|
||||||
|
} else if (fs.existsSync(profilesDir)) {
|
||||||
|
roh = leseProfileJson(profilesDir);
|
||||||
|
} else {
|
||||||
|
return { ok: false, reason: 'not-found' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const instances = [];
|
||||||
|
for (const r of roh) {
|
||||||
|
const dir = path.join(profilesDir, String(r.path || ''));
|
||||||
|
if (!r.path || !fs.existsSync(dir)) continue;
|
||||||
|
const loader = LOADER.has(String(r.loader || '').toLowerCase()) ? String(r.loader).toLowerCase() : 'vanilla';
|
||||||
|
const version = String(r.gameVersion || '').trim();
|
||||||
|
const icon = r.iconPath && fs.existsSync(String(r.iconPath)) ? String(r.iconPath) : '';
|
||||||
|
instances.push({
|
||||||
|
folder: r.path,
|
||||||
|
dir,
|
||||||
|
gameDir: dir,
|
||||||
|
name: String(r.name || r.path),
|
||||||
|
group: r.gruppe || '',
|
||||||
|
version,
|
||||||
|
loader,
|
||||||
|
loaderVersion: loader === 'vanilla' ? '' : loaderVersion(loader, r.loaderVersion, version),
|
||||||
|
installiert: r.installiert,
|
||||||
|
iconPath: icon,
|
||||||
|
lastPlayed: zeitstempel(r.lastPlayed),
|
||||||
|
playSeconds: Math.max(0, Math.round(r.sekunden || 0)),
|
||||||
|
javaPath: String(r.javaPath || '').trim(),
|
||||||
|
maxMemMb: r.maxMemMb || null,
|
||||||
|
extraArgs: String(r.extraArgs || '').trim(),
|
||||||
|
preLaunchCommand: String(r.vorStart || '').trim(),
|
||||||
|
postExitCommand: String(r.nachEnde || '').trim(),
|
||||||
|
modpack: r.modpack,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
instances.sort((a, b) => a.name.localeCompare(b.name, 'de'));
|
||||||
|
return { ok: true, basis: orte.basis, profilesDir, count: instances.length, instances, hinweis };
|
||||||
|
}
|
||||||
|
|
||||||
|
// gehört zur Modrinth App bzw. wird ohnehin neu erzeugt
|
||||||
|
const NICHT_KOPIEREN = new Set(['profile.json', 'logs', '.fabric', '.quilt']);
|
||||||
|
|
||||||
|
function copyInto(quelle, ziel) {
|
||||||
|
fs.mkdirSync(ziel, { recursive: true });
|
||||||
|
for (const name of fs.readdirSync(quelle)) {
|
||||||
|
if (NICHT_KOPIEREN.has(name.toLowerCase())) continue;
|
||||||
|
// dereference: die App verlinkt Dateien teils aus ihrem gemeinsamen Speicher
|
||||||
|
fs.cpSync(path.join(quelle, name), path.join(ziel, name), { recursive: true, dereference: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function importOne(store, entry, copyData = true) {
|
||||||
|
const created = store.createOrReplaceImportedInstance({
|
||||||
|
name: entry.name,
|
||||||
|
group: entry.group,
|
||||||
|
minecraft: { version: entry.version, loader: entry.loader, loaderVersion: entry.loaderVersion },
|
||||||
|
});
|
||||||
|
|
||||||
|
const patch = {};
|
||||||
|
if (entry.javaPath || entry.maxMemMb || entry.extraArgs) {
|
||||||
|
patch.java = { path: entry.javaPath || '', minMemMb: null, maxMemMb: entry.maxMemMb || null, extraArgs: entry.extraArgs || '' };
|
||||||
|
}
|
||||||
|
if (entry.preLaunchCommand) patch.preLaunchCommand = entry.preLaunchCommand;
|
||||||
|
if (entry.postExitCommand) patch.postExitCommand = entry.postExitCommand;
|
||||||
|
// Spielzeit und letzten Start mitnehmen (falls es die Instanz schon gab: das Größere)
|
||||||
|
const vorher = store.getInstance(created.id) || {};
|
||||||
|
if (entry.playSeconds > (vorher.totalPlaySeconds || 0)) patch.totalPlaySeconds = entry.playSeconds;
|
||||||
|
if (entry.lastPlayed && (!vorher.lastPlayed || entry.lastPlayed > vorher.lastPlayed)) patch.lastPlayed = entry.lastPlayed;
|
||||||
|
if (Object.keys(patch).length) store.updateInstance(created.id, patch);
|
||||||
|
|
||||||
|
let copied = false;
|
||||||
|
if (copyData && entry.gameDir && fs.existsSync(entry.gameDir)) {
|
||||||
|
copyInto(entry.gameDir, store.gameDir(created.id));
|
||||||
|
copied = true;
|
||||||
|
}
|
||||||
|
if (entry.iconPath) {
|
||||||
|
try { store.setCustomImage(created.id, entry.iconPath); } catch { /* Bild ist kein Muss */ }
|
||||||
|
}
|
||||||
|
return { id: created.id, name: created.name, copied };
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { scan, importOne, standardOrdner };
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* nbt.js – minimaler NBT-Leser und -Schreiber
|
||||||
|
* --------------------------------------------
|
||||||
|
* Nur so viel, wie servers.dat braucht. Diese Datei ist unkomprimiertes NBT
|
||||||
|
* mit einem Wurzel-Compound, darin eine Liste "servers" aus Compounds mit
|
||||||
|
* name, ip, icon und acceptTextures.
|
||||||
|
*
|
||||||
|
* Bewusst kein Fremdpaket: Der benötigte Ausschnitt ist klein und gut
|
||||||
|
* prüfbar, eine Abhängigkeit für zwei Datentypen wäre unverhältnismäßig.
|
||||||
|
*
|
||||||
|
* Alle Zahlen liegen in NBT als Big-Endian vor, Zeichenketten als
|
||||||
|
* modifiziertes UTF-8 mit vorangestellter Länge (2 Byte).
|
||||||
|
*/
|
||||||
|
|
||||||
|
const TAG = {
|
||||||
|
END: 0, BYTE: 1, SHORT: 2, INT: 3, LONG: 4, FLOAT: 5, DOUBLE: 6,
|
||||||
|
BYTE_ARRAY: 7, STRING: 8, LIST: 9, COMPOUND: 10, INT_ARRAY: 11, LONG_ARRAY: 12,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Lesen
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class Leser {
|
||||||
|
constructor(buf) { this.buf = buf; this.pos = 0; }
|
||||||
|
pruefe(n) { if (this.pos + n > this.buf.length) throw new Error('NBT endet unerwartet'); }
|
||||||
|
byte() { this.pruefe(1); return this.buf.readInt8(this.pos++); }
|
||||||
|
ubyte() { this.pruefe(1); return this.buf.readUInt8(this.pos++); }
|
||||||
|
short() { this.pruefe(2); const v = this.buf.readInt16BE(this.pos); this.pos += 2; return v; }
|
||||||
|
ushort() { this.pruefe(2); const v = this.buf.readUInt16BE(this.pos); this.pos += 2; return v; }
|
||||||
|
int() { this.pruefe(4); const v = this.buf.readInt32BE(this.pos); this.pos += 4; return v; }
|
||||||
|
long() { this.pruefe(8); const v = this.buf.readBigInt64BE(this.pos); this.pos += 8; return v; }
|
||||||
|
float() { this.pruefe(4); const v = this.buf.readFloatBE(this.pos); this.pos += 4; return v; }
|
||||||
|
double() { this.pruefe(8); const v = this.buf.readDoubleBE(this.pos); this.pos += 8; return v; }
|
||||||
|
text() {
|
||||||
|
const laenge = this.ushort();
|
||||||
|
this.pruefe(laenge);
|
||||||
|
const s = this.buf.toString('utf8', this.pos, this.pos + laenge);
|
||||||
|
this.pos += laenge;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
bytes(n) { this.pruefe(n); const b = this.buf.subarray(this.pos, this.pos + n); this.pos += n; return Buffer.from(b); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function leseWert(l, typ) {
|
||||||
|
switch (typ) {
|
||||||
|
case TAG.BYTE: return l.byte();
|
||||||
|
case TAG.SHORT: return l.short();
|
||||||
|
case TAG.INT: return l.int();
|
||||||
|
case TAG.LONG: return l.long();
|
||||||
|
case TAG.FLOAT: return l.float();
|
||||||
|
case TAG.DOUBLE: return l.double();
|
||||||
|
case TAG.STRING: return l.text();
|
||||||
|
case TAG.BYTE_ARRAY: return l.bytes(l.int());
|
||||||
|
case TAG.INT_ARRAY: { const n = l.int(); const a = []; for (let i = 0; i < n; i++) a.push(l.int()); return a; }
|
||||||
|
case TAG.LONG_ARRAY: { const n = l.int(); const a = []; for (let i = 0; i < n; i++) a.push(l.long()); return a; }
|
||||||
|
case TAG.LIST: {
|
||||||
|
const innerTyp = l.ubyte();
|
||||||
|
const n = l.int();
|
||||||
|
const a = [];
|
||||||
|
for (let i = 0; i < n; i++) a.push(leseWert(l, innerTyp));
|
||||||
|
a.__typ = innerTyp; // Typ merken, damit Schreiben ihn wiederherstellt
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
case TAG.COMPOUND: {
|
||||||
|
const o = {};
|
||||||
|
o.__typen = {};
|
||||||
|
for (;;) {
|
||||||
|
const t = l.ubyte();
|
||||||
|
if (t === TAG.END) break;
|
||||||
|
const name = l.text();
|
||||||
|
o.__typen[name] = t;
|
||||||
|
o[name] = leseWert(l, t);
|
||||||
|
}
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
default: throw new Error('Unbekannter NBT-Typ: ' + typ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Liest unkomprimiertes NBT.
|
||||||
|
* -> { name, wert }
|
||||||
|
*/
|
||||||
|
function lese(buf) {
|
||||||
|
const l = new Leser(buf);
|
||||||
|
const typ = l.ubyte();
|
||||||
|
if (typ !== TAG.COMPOUND) throw new Error('NBT beginnt nicht mit einem Compound');
|
||||||
|
const name = l.text();
|
||||||
|
return { name, wert: leseWert(l, TAG.COMPOUND) };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Schreiben
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class Schreiber {
|
||||||
|
constructor() { this.teile = []; }
|
||||||
|
byte(v) { const b = Buffer.alloc(1); b.writeInt8(v, 0); this.teile.push(b); }
|
||||||
|
ubyte(v) { const b = Buffer.alloc(1); b.writeUInt8(v, 0); this.teile.push(b); }
|
||||||
|
short(v) { const b = Buffer.alloc(2); b.writeInt16BE(v, 0); this.teile.push(b); }
|
||||||
|
int(v) { const b = Buffer.alloc(4); b.writeInt32BE(v, 0); this.teile.push(b); }
|
||||||
|
long(v) { const b = Buffer.alloc(8); b.writeBigInt64BE(BigInt(v), 0); this.teile.push(b); }
|
||||||
|
float(v) { const b = Buffer.alloc(4); b.writeFloatBE(v, 0); this.teile.push(b); }
|
||||||
|
double(v) { const b = Buffer.alloc(8); b.writeDoubleBE(v, 0); this.teile.push(b); }
|
||||||
|
text(s) {
|
||||||
|
const d = Buffer.from(String(s), 'utf8');
|
||||||
|
const l = Buffer.alloc(2); l.writeUInt16BE(d.length, 0);
|
||||||
|
this.teile.push(l, d);
|
||||||
|
}
|
||||||
|
roh(b) { this.teile.push(b); }
|
||||||
|
fertig() { return Buffer.concat(this.teile); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Typ raten, wenn er nicht aus dem Einlesen bekannt ist
|
||||||
|
function typVon(wert) {
|
||||||
|
if (typeof wert === 'string') return TAG.STRING;
|
||||||
|
if (typeof wert === 'bigint') return TAG.LONG;
|
||||||
|
if (typeof wert === 'boolean') return TAG.BYTE;
|
||||||
|
if (Buffer.isBuffer(wert)) return TAG.BYTE_ARRAY;
|
||||||
|
if (Array.isArray(wert)) return TAG.LIST;
|
||||||
|
if (wert && typeof wert === 'object') return TAG.COMPOUND;
|
||||||
|
if (Number.isInteger(wert)) return TAG.INT;
|
||||||
|
return TAG.DOUBLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
function schreibeWert(s, typ, wert) {
|
||||||
|
switch (typ) {
|
||||||
|
case TAG.BYTE: s.byte(wert ? Number(wert) : 0); break;
|
||||||
|
case TAG.SHORT: s.short(Number(wert) || 0); break;
|
||||||
|
case TAG.INT: s.int(Number(wert) || 0); break;
|
||||||
|
case TAG.LONG: s.long(wert || 0); break;
|
||||||
|
case TAG.FLOAT: s.float(Number(wert) || 0); break;
|
||||||
|
case TAG.DOUBLE: s.double(Number(wert) || 0); break;
|
||||||
|
case TAG.STRING: s.text(wert == null ? '' : wert); break;
|
||||||
|
case TAG.BYTE_ARRAY: { const b = Buffer.isBuffer(wert) ? wert : Buffer.alloc(0); s.int(b.length); s.roh(b); break; }
|
||||||
|
case TAG.INT_ARRAY: { s.int(wert.length); for (const v of wert) s.int(v); break; }
|
||||||
|
case TAG.LONG_ARRAY: { s.int(wert.length); for (const v of wert) s.long(v); break; }
|
||||||
|
case TAG.LIST: {
|
||||||
|
const liste = wert || [];
|
||||||
|
// leere Listen bekommen END als Typ – so macht es Minecraft auch
|
||||||
|
const innerTyp = liste.__typ || (liste.length ? typVon(liste[0]) : TAG.END);
|
||||||
|
s.ubyte(innerTyp);
|
||||||
|
s.int(liste.length);
|
||||||
|
for (const v of liste) schreibeWert(s, innerTyp, v);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TAG.COMPOUND: {
|
||||||
|
const typen = (wert && wert.__typen) || {};
|
||||||
|
for (const schluessel of Object.keys(wert || {})) {
|
||||||
|
if (schluessel === '__typen' || schluessel === '__typ') continue;
|
||||||
|
const t = typen[schluessel] || typVon(wert[schluessel]);
|
||||||
|
s.ubyte(t);
|
||||||
|
s.text(schluessel);
|
||||||
|
schreibeWert(s, t, wert[schluessel]);
|
||||||
|
}
|
||||||
|
s.ubyte(TAG.END);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: throw new Error('Unbekannter NBT-Typ beim Schreiben: ' + typ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function schreibe(name, wert) {
|
||||||
|
const s = new Schreiber();
|
||||||
|
s.ubyte(TAG.COMPOUND);
|
||||||
|
s.text(name || '');
|
||||||
|
schreibeWert(s, TAG.COMPOUND, wert);
|
||||||
|
return s.fertig();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { lese, schreibe, TAG };
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
// AudioWorklet-Prozessor für die isolierte "Nur Minecraft"-Tonaufnahme.
|
||||||
|
//
|
||||||
|
// loopback-capture liefert PCM-Häppchen unregelmäßig getaktet über IPC (mal
|
||||||
|
// etwas früher, mal etwas später). Würde man sie direkt nacheinander als
|
||||||
|
// einzelne AudioBufferSourceNode-Aufrufe abspielen, würde jede kleine
|
||||||
|
// Verzögerung als hörbares Knacksen/Aussetzer auffallen. Stattdessen landen
|
||||||
|
// ankommende Häppchen hier in einem Ringpuffer; der Audio-Rendering-Thread
|
||||||
|
// liest in gleichmäßigem Takt (process() läuft alle 128 Samples) daraus.
|
||||||
|
// Kleine Zeitschwankungen bei der Anlieferung werden so unhörbar
|
||||||
|
// abgefedert. Läuft der Puffer doch mal leer, gibt es kurz Stille statt
|
||||||
|
// eines Knacksens.
|
||||||
|
class PcmRingBufferProcessor extends AudioWorkletProcessor {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.capacity = sampleRate * 2; // 2 Sekunden Puffer je Kanal
|
||||||
|
this.ring = [new Float32Array(this.capacity), new Float32Array(this.capacity)];
|
||||||
|
this.writeIdx = 0;
|
||||||
|
this.readIdx = 0;
|
||||||
|
this.available = 0; // gepufferte Frames (nicht Samples)
|
||||||
|
this.port.onmessage = (e) => this._enqueue(e.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
_enqueue([left, right]) {
|
||||||
|
const n = left.length;
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const idx = (this.writeIdx + i) % this.capacity;
|
||||||
|
this.ring[0][idx] = left[i];
|
||||||
|
this.ring[1][idx] = right[i];
|
||||||
|
}
|
||||||
|
this.writeIdx = (this.writeIdx + n) % this.capacity;
|
||||||
|
this.available = Math.min(this.capacity, this.available + n);
|
||||||
|
}
|
||||||
|
|
||||||
|
process(_inputs, outputs) {
|
||||||
|
const output = outputs[0];
|
||||||
|
const frames = output[0].length;
|
||||||
|
const playable = Math.min(frames, this.available);
|
||||||
|
for (let i = 0; i < playable; i++) {
|
||||||
|
const idx = (this.readIdx + i) % this.capacity;
|
||||||
|
output[0][i] = this.ring[0][idx];
|
||||||
|
output[1][i] = this.ring[1][idx];
|
||||||
|
}
|
||||||
|
for (let i = playable; i < frames; i++) { output[0][i] = 0; output[1][i] = 0; }
|
||||||
|
this.readIdx = (this.readIdx + playable) % this.capacity;
|
||||||
|
this.available -= playable;
|
||||||
|
return true; // Prozessor am Leben halten, auch wenn gerade nichts ankommt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerProcessor('pcm-player', PcmRingBufferProcessor);
|
||||||