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 |
@@ -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
|
||||
@@ -4,9 +4,11 @@
|
||||
|
||||
# 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.4
|
||||
Aktueller Stand: Version 1.1.6
|
||||
|
||||
<img src="docs/aeromc.png" alt="AeroMC – Instanzliste mit Seitenleiste und Skin-Vorschau" width="820" />
|
||||
|
||||
</div>
|
||||
|
||||
@@ -14,31 +16,87 @@ Aktueller Stand: Version 1.0.4
|
||||
|
||||
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.
|
||||
|
||||
Der Launcher konzentriert sich auf den Alltag: Instanz anlegen, Konto auswählen, Mods installieren, starten.
|
||||
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.
|
||||
|
||||
## Funktionen
|
||||
|
||||
**Instanzen**
|
||||
- Beliebig viele getrennte Minecraft-Instanzen
|
||||
- Unterstützung für Vanilla, Fabric, Quilt, Forge und NeoForge
|
||||
- Unterstützung für Vanilla, Fabric, Quilt, Forge, NeoForge und OptiFine
|
||||
- Versionsliste mit Filter: Releases, Snapshots oder alle
|
||||
- Modrinth-Integration für **Mods, Ressourcenpakete und Shader** (inkl. Pflicht-Abhängigkeiten und Update-Prüfung)
|
||||
- Microsoft-Login mit mehreren Konten
|
||||
- Gemeinsamer Import für AeroMC-ZIP, Modrinth-Modpacks (`.mrpack`), Minecraft Launcher, CurseForge sowie MultiMC/Prism
|
||||
- Reimport überschreibt vorhandene Instanzen mit gleichem Namen
|
||||
- Performance-Paket beim Anlegen: Sodium, Lithium, FerriteCore, ImmediatelyFast, Entity Culling und ModernFix per Häkchen
|
||||
- Export, Kopieren, Gruppieren, Löschen und Desktop-Verknüpfung pro Instanz
|
||||
- Mehrfachauswahl mit Strg-Klick für Sammellöschen
|
||||
- Globales Backup inkl. automatischer Zeitplan-Sicherungen
|
||||
- Welten, Ressourcenpakete, Screenshots, Serverliste und Logdateien verwalten
|
||||
- Online- und Offline-Start
|
||||
- Eingebaute Konsole/Log-Ansicht
|
||||
- Skin-Hintergrund des aktiven Kontos (mehrere Posen, einstellbare Deckkraft)
|
||||
- Eigene Icons (inkl. eigenes Bild) und Notizen pro Instanz
|
||||
- Eigene Icons pro Instanz – vorgefertigte Symbole, aus Minecraft extrahierte Block-/Item-Icons oder ein eigenes Bild – plus Notizen
|
||||
- Mehrfachauswahl mit Strg-Klick, Tastenkürzel für die wichtigsten Aktionen
|
||||
- Schnellstart-Tasten: Lieblings-Instanzen auf **Strg+1** bis **Strg+5** legen
|
||||
- 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
|
||||
- Spielzeit-Statistik: Gesamtspielzeit, Spielstarts und Spielzeit pro Instanz
|
||||
|
||||
**Mods, Modpacks und Inhalte**
|
||||
- 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
|
||||
- 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
|
||||
- Warnung beim Entfernen eines Mods, der noch als Pflicht-Abhängigkeit gebraucht wird
|
||||
|
||||
**Import**
|
||||
- Gemeinsamer Import für AeroMC-ZIP, Modrinth-Modpacks (`.mrpack`), Minecraft Launcher, CurseForge, **Modrinth App**, MultiMC/Prism sowie Technic
|
||||
- Schon bei der Installation: der Installer erkennt andere Launcher und AeroMC bietet deren Instanzen beim ersten Start zum Übernehmen an
|
||||
- Von Freunden geteilte Instanzen per Code einlösen
|
||||
|
||||
**Spielaufzeichnung**
|
||||
- Nimmt nur das Minecraft-Fenster auf – ohne Desktop-Ränder und ohne Mauszeiger – und speichert als MP4
|
||||
- Auflösung, Qualität und Bildrate einstellbar, mehrere Tonspuren (Systemton, nur Minecraft, Mikrofone) mit eigener Lautstärke
|
||||
- Clip-Modus: läuft im Hintergrund mit, **Alt+F10** speichert die letzten 30, 60 oder 90 Sekunden
|
||||
- Servername im Aufnahmetitel, Aufzeichnungen-Galerie mit Vorschaubildern
|
||||
|
||||
**Freunde**
|
||||
- Freunde per Minecraft-Name hinzufügen, mit 3D-Spielerkopf, Online-Status und Spitznamen
|
||||
- Meldung im Spiel, sobald ein Freund online kommt – bei mehreren Konten im jeweils richtigen Fenster
|
||||
- **Beitreten**: startet eine passende Instanz und verbindet direkt mit dem Server eines Freundes
|
||||
- **Einladen**: schickt Freunden deinen aktuellen Server – als Hinweis im Launcher und als Meldung im Spiel
|
||||
- **Kurznachrichten** mit Schnellantworten wie „Bin in 10 Min. online“, auch als Meldung im Spiel
|
||||
- **Screenshots** direkt aus der Galerie an Freunde schicken
|
||||
- Anmeldung über Mojangs Sitzungsprüfung – niemand kann sich als ein anderer Spieler ausgeben
|
||||
- Eigener Server auf Wunsch verborgen, Freundesystem komplett abschaltbar
|
||||
|
||||
**Konten, Welten und Backups**
|
||||
- Microsoft-Login mit mehreren Konten
|
||||
- Eigenen Skin im Launcher hochladen, mit Live-Vorschau anpassen (klassisch/schlank) und zurücksetzen
|
||||
- **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)
|
||||
- Welten, Ressourcenpakete, Datenpakete, Screenshots, Serverliste und Logdateien verwalten
|
||||
- Serverliste mit Live-Status und Desktop-Verknüpfung pro Server (startet die Instanz und verbindet direkt)
|
||||
- Welten mit einem Klick in eine andere Instanz kopieren
|
||||
- Globales Backup inkl. automatischer Zeitplan-Sicherungen, dazu Schnellsicherung einzelner Welten
|
||||
- Automatische Welt-Sicherung nach jeder Spielsitzung (die letzten 5 Stände je Welt)
|
||||
|
||||
**Technik und Komfort**
|
||||
- Java pro Instanz oder global, inkl. automatischem Download fehlender Versionen (Eclipse Temurin)
|
||||
- RAM-, JVM-, Pre-Launch- und Post-Exit-Einstellungen (global und pro Instanz)
|
||||
- Optionaler Wrapper-Befehl und HTTP-Proxy für Downloads
|
||||
- Bedrock-Bereich für vorhandene Bedrock-Installation unter Windows
|
||||
- Update-Funktion für neue Launcher-Versionen
|
||||
- Design Hell/Dunkel und Sortierung der Instanzen
|
||||
- RAM-, JVM-, Pre-Launch- und Post-Exit-Einstellungen (global und pro Instanz), Ein-Klick-Knopf für empfohlene GC-Flags
|
||||
- **Absturz-Assistent**: erklärt Abstürze in einfachen Worten und behebt sie oft mit einem Klick; auf Wunsch geht der Bericht an den Entwickler
|
||||
- Eingebaute Konsole/Log-Ansicht inkl. Absturzberichte pro Instanz
|
||||
- Bedrock-Bereich für eine vorhandene Bedrock-Installation, inkl. Add-on-Installation
|
||||
- Update-Suche sofort beim Start, Updates installieren sich ohne Rückfragen
|
||||
- Optionaler Autostart mit Windows (im Infobereich statt als Fenster)
|
||||
- Rechtsklick-Menü in allen Textfeldern (Ausschneiden, Kopieren, Einfügen)
|
||||
- **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
|
||||
|
||||
## 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
|
||||
|
||||
@@ -47,11 +105,15 @@ 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 x64
|
||||
- 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
|
||||
@@ -59,11 +121,10 @@ Automatische Updates bleiben getrennt: Setup erhält nur Setup-Updates, Portable
|
||||
## Erste Schritte
|
||||
|
||||
1. AeroMC installieren oder die portable Datei starten.
|
||||
2. Über **Konten** ein Microsoft-Konto hinzufügen.
|
||||
3. Eine neue Instanz anlegen.
|
||||
4. Minecraft-Version und optional einen Loader wählen.
|
||||
5. Optional Mods/Ressourcenpakete/Shader hinzufügen oder bestehende Setups über **Importieren** übernehmen.
|
||||
6. Instanz starten (online oder offline).
|
||||
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
|
||||
|
||||
@@ -71,8 +132,8 @@ Jede Instanz hat:
|
||||
|
||||
- Namen und optionale Gruppe
|
||||
- Minecraft-Version und optionalen Mod-Loader
|
||||
- optionales Icon (vorgegeben oder eigenes Bild) und Notizen
|
||||
- eigene Mods, Ressourcenpakete, Shader, Welten, Screenshots und Serverliste
|
||||
- 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.:
|
||||
@@ -80,15 +141,42 @@ Jede Instanz hat:
|
||||
- Starten / Offline starten
|
||||
- Instanz und Notizen bearbeiten
|
||||
- Mods, Ressourcenpakete und Shader
|
||||
- Welten und Bildschirmfotos
|
||||
- Server verwalten
|
||||
- Logdateien ansehen
|
||||
- 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, Kopieren, Gruppe ändern, Löschen
|
||||
- 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.
|
||||
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
|
||||
|
||||
@@ -97,26 +185,100 @@ In der Instanzliste: Suche, Sortierung und **Strg + Klick** für Mehrfachauswahl
|
||||
| Quelle | Typische Nutzung |
|
||||
|---|---|
|
||||
| ZIP-Archiv | AeroMC-Export (`instance.json`) sowie einzelne MultiMC/Prism-Instanzen (auch gezippt) |
|
||||
| Modpack | Modrinth `.mrpack` |
|
||||
| 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 (Sortierung, Kategorien)
|
||||
- Installation inkl. Abhängigkeiten
|
||||
- Installierte Einträge aktivieren, deaktivieren, entfernen
|
||||
- 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.
|
||||
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
|
||||
|
||||
@@ -129,23 +291,29 @@ Unter **Optionen → Backup**:
|
||||
|
||||
Speicherort: `Dokumente\AeroMC Launcher\Backups`.
|
||||
|
||||
**Welten einzeln:** In **Welten verwalten** sichert „Sichern“ eine einzelne Welt als ZIP. Zusätzlich sichert AeroMC nach jeder Spielsitzung automatisch die zuletzt gespielte Welt; pro Welt bleiben die letzten 5 Stände erhalten (`Dokumente\AeroMC Launcher\Welten-Sicherungen\<Instanz>\Automatisch`). Abschaltbar unter **Optionen → Backup → Welten**.
|
||||
|
||||
**Wichtig:** Konten und Tokens werden nicht mitgesichert. Beim Wiederherstellen werden aktuelle Instanzen und Launcher-Einstellungen durch den Backup-Inhalt ersetzt.
|
||||
|
||||
## Einstellungen
|
||||
|
||||
### Allgemein
|
||||
- Design (Dunkel / Hell)
|
||||
- Instanzen sortieren (nach letztem Start / nach Name)
|
||||
- Instanz-Verzeichnis
|
||||
- Bedrock-Bereich anzeigen
|
||||
- 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, Deckkraft
|
||||
- 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
|
||||
@@ -165,13 +333,27 @@ Ohne eigenen Key und ohne Weiterleitung funktionieren Modrinth-Inhalte weiterhin
|
||||
|
||||
### Backup
|
||||
- Manuell, Zeitplan und Wiederherstellung
|
||||
- Welten: nach jeder Spielsitzung die zuletzt gespielte Welt sichern (Standard: an)
|
||||
|
||||
### Info
|
||||
- Version und manuelle Update-Prüfung
|
||||
- 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. AeroMC installiert Bedrock nicht selbst. Fehlt die Installation, wird der Bereich automatisch ausgeblendet.
|
||||
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
|
||||
|
||||
@@ -180,12 +362,15 @@ Der Bedrock-Bereich startet die vorhandene Windows-Store-Version und erleichtert
|
||||
|
||||
## Datenort
|
||||
|
||||
Nutzerdaten liegen lokal unter `%APPDATA%\aeromc`.
|
||||
- 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
|
||||
- Gitseite: https://git.viper.ipv64.net
|
||||
- Launcher-Webseite: https://aeromc.viper.ipv64.net
|
||||
- Autor: M_Viper
|
||||
|
||||
## Rechtliches
|
||||
@@ -193,6 +378,14 @@ Nutzerdaten liegen lokal unter `%APPDATA%\aeromc`.
|
||||
AeroMC ist kein offizielles Produkt von Mojang oder Microsoft.
|
||||
Minecraft ist eine Marke von Mojang AB.
|
||||
|
||||
## Lizenz
|
||||
### Urheberrecht & Nutzungsbedingungen
|
||||
|
||||
MIT
|
||||
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,16 +1,21 @@
|
||||
{
|
||||
"name": "aeromc",
|
||||
"version": "1.0.4-beta",
|
||||
"version": "1.1.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "aeromc",
|
||||
"version": "1.0.4-beta",
|
||||
"license": "MIT",
|
||||
"version": "1.1.6",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"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",
|
||||
"sql.js": "^1.14.2",
|
||||
"undici": "^6.28.0",
|
||||
"unzipper": "^0.12.5"
|
||||
},
|
||||
@@ -19,6 +24,21 @@
|
||||
"electron-builder": "^24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@derhuerst/http-basic": {
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@derhuerst/http-basic/-/http-basic-8.2.4.tgz",
|
||||
"integrity": "sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"caseless": "^0.12.0",
|
||||
"concat-stream": "^2.0.0",
|
||||
"http-response-object": "^3.0.1",
|
||||
"parse-cache-control": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@develar/schema-utils": {
|
||||
"version": "2.6.5",
|
||||
"resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
|
||||
@@ -37,6 +57,56 @@
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/collection": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
|
||||
"integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/rest": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.3.tgz",
|
||||
"integrity": "sha512-wvOylxNYJkwKjctS/Mn5GP1w9r3/rzyH+ThD1JlAca6zEdlHs8QWBBUQJpU5Q+W6DoIj/Ljh1IPlZs7hTU+UAg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@discordjs/collection": "^2.1.1",
|
||||
"@discordjs/util": "^1.2.0",
|
||||
"@sapphire/async-queue": "^1.5.3",
|
||||
"@sapphire/snowflake": "^3.5.5",
|
||||
"@vladfrangu/async_event_emitter": "^2.4.6",
|
||||
"discord-api-types": "^0.38.50",
|
||||
"magic-bytes.js": "^1.13.0",
|
||||
"tslib": "^2.6.3",
|
||||
"undici": "^6.27.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/util": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz",
|
||||
"integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"discord-api-types": "^0.38.33"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/asar": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz",
|
||||
@@ -494,6 +564,26 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@sapphire/async-queue": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz",
|
||||
"integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=v14.0.0",
|
||||
"npm": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sapphire/snowflake": {
|
||||
"version": "3.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.5.tgz",
|
||||
"integrity": "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=v14.0.0",
|
||||
"npm": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sindresorhus/is": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
|
||||
@@ -662,6 +752,31 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@vladfrangu/async_event_emitter": {
|
||||
"version": "2.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz",
|
||||
"integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=v14.0.0",
|
||||
"npm": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@xhayper/discord-rpc": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@xhayper/discord-rpc/-/discord-rpc-1.3.4.tgz",
|
||||
"integrity": "sha512-ff0uEXuibh9wi+l4vOj7xInLUjtlTaQBje/SCyQkeXZ0j2V0y+Zge5PQIQFRHH9TjjGaYJkTofEcQhncM2q7/w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@discordjs/rest": "^2.6.1",
|
||||
"@vladfrangu/async_event_emitter": "^2.4.7",
|
||||
"discord-api-types": "^0.38.47",
|
||||
"ws": "^8.20.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@xmldom/xmldom": {
|
||||
"version": "0.9.10",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz",
|
||||
@@ -695,7 +810,6 @@
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "4"
|
||||
@@ -1128,6 +1242,15 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bindings": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"file-uri-to-path": "1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||
@@ -1243,7 +1366,6 @@
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/builder-util": {
|
||||
@@ -1366,6 +1488,12 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/caseless": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
|
||||
"integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
@@ -1536,6 +1664,35 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concat-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
|
||||
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
|
||||
"engines": [
|
||||
"node >= 6.0"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.0.2",
|
||||
"typedarray": "^0.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-stream/node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/config-file-ts": {
|
||||
"version": "0.2.6",
|
||||
"resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.6.tgz",
|
||||
@@ -1655,7 +1812,6 @@
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
@@ -1799,6 +1955,15 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/discord-api-types": {
|
||||
"version": "0.38.53",
|
||||
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.53.tgz",
|
||||
"integrity": "sha512-HL1zz/UuZ+bbJjA/X8Kbxx9gk8v9rJAbTeWRNYKmIdjwJ7EovjlHgoJTxcLpATfNJ+AonOtMdy3Y5MVIJAAd/A==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"scripts/actions/documentation"
|
||||
]
|
||||
},
|
||||
"node_modules/dmg-builder": {
|
||||
"version": "24.13.3",
|
||||
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.13.3.tgz",
|
||||
@@ -2372,7 +2537,6 @@
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
|
||||
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -2493,6 +2657,22 @@
|
||||
"bare-events": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/extract-file-icon": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/extract-file-icon/-/extract-file-icon-0.3.2.tgz",
|
||||
"integrity": "sha512-We5/HaMDlioOx3c9zDjit8RtKWEvT94sJF8jTujaKVOyWL8Kkqu4QGnwDr5fID6XsbKvWT0JJNuWuV2Tmr7yiw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-addon-api": "1.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/extract-file-icon/node_modules/node-addon-api": {
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.1.tgz",
|
||||
"integrity": "sha512-2+DuKodWvwRTrCfKOeR24KIc5unKjOh8mz17NCzVnHWfjAdDqbfbjqh7gUT+BkXBRQM52+xCHciKWonJ3CbJMQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/extract-zip": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
||||
@@ -2561,6 +2741,28 @@
|
||||
"integrity": "sha512-3JyEFWGjFn7zHmoa9+zG1BmW7X2okcmAB+0Cnu9UFbVs/jCBnl2A8o065ZlXiw145K3eBM3uLuzrYXC0RK7eDg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ffmpeg-static": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ffmpeg-static/-/ffmpeg-static-5.3.0.tgz",
|
||||
"integrity": "sha512-H+K6sW6TiIX6VGend0KQwthe+kaceeH/luE8dIZyOP35ik7ahYojDuqlTV1bOrtEwl01sy2HFNGQfi5IDJvotg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@derhuerst/http-basic": "^8.2.0",
|
||||
"env-paths": "^2.2.0",
|
||||
"https-proxy-agent": "^5.0.0",
|
||||
"progress": "^2.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/filelist": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz",
|
||||
@@ -2978,6 +3180,21 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/http-response-object": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz",
|
||||
"integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "^10.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/http-response-object/node_modules/@types/node": {
|
||||
"version": "10.17.60",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
|
||||
"integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/http2-wrapper": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
|
||||
@@ -2996,7 +3213,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "6",
|
||||
@@ -3340,6 +3556,25 @@
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/loopback-capture": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/loopback-capture/-/loopback-capture-3.0.2.tgz",
|
||||
"integrity": "sha512-65qyF9L7e+InnbEZP3Ic4C4J+SszoxEq84XpIZWJYH3/IgUpTKqBzJ4wCF+ShL3bvqV7I2wOEgQv9VfpaO2AMw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"os": [
|
||||
"linux",
|
||||
"win32"
|
||||
],
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lowercase-keys": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
|
||||
@@ -3363,6 +3598,12 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-bytes.js": {
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.13.1.tgz",
|
||||
"integrity": "sha512-x5sn4UX2k5gCWlcfmoFwG4TPie8+dctESyqOBdhB5p6MsgWXdBKGmt9nXPObj/JI50TTL928lc5Yt1WntMn1bw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/matcher": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
|
||||
@@ -3514,7 +3755,6 @@
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
@@ -3525,12 +3765,41 @@
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/node-gyp-build": {
|
||||
"version": "4.8.4",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"node-gyp-build": "bin.js",
|
||||
"node-gyp-build-optional": "optional.js",
|
||||
"node-gyp-build-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/node-int64": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
||||
"integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-window-manager": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/node-window-manager/-/node-window-manager-2.2.4.tgz",
|
||||
"integrity": "sha512-Dgq9a7qx6+Gi+BifWM79leteNz76QUtwSsdiqEe7SjvoJsc8TYQsRl1IA5uxqz5iSLeHS1iA/8YI/snoq0DblA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"extract-file-icon": "^0.3.2",
|
||||
"node-addon-api": "2.0.0",
|
||||
"node-gyp-build": "^4.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/node-window-manager/node_modules/node-addon-api": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.0.tgz",
|
||||
"integrity": "sha512-ASCL5U13as7HhOExbT6OlWJJUV/lLzL2voOSP1UVehpRD8FbSrSDjfScK/KwAvVTI5AS6r4VwbOMlIqtvRidnA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
@@ -3590,6 +3859,11 @@
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||
"license": "BlueOak-1.0.0"
|
||||
},
|
||||
"node_modules/parse-cache-control": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz",
|
||||
"integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg=="
|
||||
},
|
||||
"node_modules/path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
@@ -3679,7 +3953,6 @@
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
@@ -4074,6 +4347,12 @@
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/sql.js": {
|
||||
"version": "1.14.2",
|
||||
"resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.14.2.tgz",
|
||||
"integrity": "sha512-3ZGPovObMFrdw79zrUHbfdE/DLIsy8jdNdssmMSQuRAymedU6q84asPt0kgiqrdMYlPegDItiIMfmIXzZnYFcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stat-mode": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz",
|
||||
@@ -4318,6 +4597,12 @@
|
||||
"utf8-byte-length": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/type-fest": {
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
|
||||
@@ -4332,6 +4617,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/typedarray": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
@@ -4517,6 +4808,27 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.21.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
|
||||
"integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xmlbuilder": {
|
||||
"version": "15.1.1",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
{
|
||||
"name": "aeromc",
|
||||
"productName": "AeroMC",
|
||||
"version": "1.0.4",
|
||||
"version": "1.1.6",
|
||||
"description": "AeroMC – Instanz-basierter Minecraft-Launcher (à la MultiMC/Prism)",
|
||||
"homepage": "https://m-viper.de",
|
||||
"launcherWebsite": "https://aeromc.viper.ipv64.net",
|
||||
"main": "src/main.js",
|
||||
"author": "M_Viper",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.viper.ipv64.net"
|
||||
},
|
||||
"license": "MIT",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
"prebuild": "node scripts/write-build-config.js",
|
||||
"prebuild:portable": "node scripts/write-build-config.js",
|
||||
"build": "electron-builder --win nsis --x64 && node -e \"const fs=require('fs');const path=require('path');['dist/builder-debug.yml','dist/builder-effective-config.yaml','dist/win-unpacked'].forEach((target)=>fs.rmSync(target,{recursive:true,force:true}));if(fs.existsSync('dist')){const source=path.join('dist','AeroMC.exe');const target=path.join('dist','AeroMC-Setup.exe');if(fs.existsSync(target))fs.rmSync(target,{force:true});if(fs.existsSync(source))fs.renameSync(source,target);for(const name of fs.readdirSync('dist')){if((name.endsWith('.exe')&&name!=='AeroMC-Setup.exe'&&name!=='AeroMC-Portable.exe')||name.endsWith('.blockmap')){fs.rmSync(path.join('dist',name),{force:true})}}}\"",
|
||||
"build": "electron-builder --win dir && node scripts/build-setup.js",
|
||||
"build:portable": "electron-builder --win portable --x64 && node -e \"const fs=require('fs');const path=require('path');['dist/builder-debug.yml','dist/builder-effective-config.yaml','dist/win-unpacked'].forEach((target)=>fs.rmSync(target,{recursive:true,force:true}));if(fs.existsSync('dist')){const source=path.join('dist','AeroMC.exe');const target=path.join('dist','AeroMC-Portable.exe');if(fs.existsSync(target))fs.rmSync(target,{force:true});if(fs.existsSync(source))fs.renameSync(source,target);for(const name of fs.readdirSync('dist')){if((name.endsWith('.exe')&&name!=='AeroMC-Setup.exe'&&name!=='AeroMC-Portable.exe')||name.endsWith('.blockmap')){fs.rmSync(path.join('dist',name),{force:true})}}}\""
|
||||
},
|
||||
"build": {
|
||||
@@ -25,15 +23,17 @@
|
||||
"compression": "store",
|
||||
"files": [
|
||||
"src/**/*",
|
||||
"package.json"
|
||||
"package.json",
|
||||
"LICENSE"
|
||||
],
|
||||
"asarUnpack": [
|
||||
"node_modules/loopback-capture/**/*",
|
||||
"node_modules/node-window-manager/**/*",
|
||||
"node_modules/ffmpeg-static/**/*"
|
||||
],
|
||||
"win": {
|
||||
"target": "nsis",
|
||||
"target": "dir",
|
||||
"icon": "src/assets/icon.ico"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"allowToChangeInstallationDirectory": true
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -41,8 +41,13 @@
|
||||
"electron-builder": "^24.0.0"
|
||||
},
|
||||
"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",
|
||||
"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`);
|
||||
|
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 |
@@ -354,6 +354,10 @@ async function restoreGlobalBackup(options) {
|
||||
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);
|
||||
@@ -382,6 +386,7 @@ async function restoreGlobalBackup(options) {
|
||||
endPercent: 40,
|
||||
detailPrefix: 'Sichere aktuelle Instanzen',
|
||||
});
|
||||
originalMovedAway = true;
|
||||
settingsPreviouslyExisted = fs.existsSync(settingsFile);
|
||||
if (settingsPreviouslyExisted) fs.copyFileSync(settingsFile, rollbackSettings);
|
||||
|
||||
@@ -414,16 +419,23 @@ async function restoreGlobalBackup(options) {
|
||||
restoredSettings,
|
||||
};
|
||||
} catch (err) {
|
||||
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);
|
||||
// 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 */ }
|
||||
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 */ }
|
||||
|
||||
@@ -14,7 +14,12 @@ const path = require('path');
|
||||
const { execFile, spawn } = require('child_process');
|
||||
|
||||
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() {
|
||||
return path.join(process.env.LOCALAPPDATA || '', 'Packages', PKG_FAMILY,
|
||||
@@ -49,30 +54,87 @@ async function detect() {
|
||||
// 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.
|
||||
* Wichtig: Der AppsFolder-Pfad darf NICHT in Anführungszeichen stehen – sonst
|
||||
* öffnet der Explorer nur ein Ordnerfenster. Daher windowsVerbatimArguments.
|
||||
*/
|
||||
function launch() {
|
||||
const target = 'shell:appsFolder\\' + APP_ID;
|
||||
async function launch() {
|
||||
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 = [
|
||||
// 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',
|
||||
`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) {
|
||||
try {
|
||||
const p = spawn(cmd, args, Object.assign({ detached: true, stdio: 'ignore', windowsHide: true }, extra));
|
||||
p.unref();
|
||||
return { ok: true, method: cmd };
|
||||
} catch { /* nächsten Weg versuchen */ }
|
||||
methode = cmd;
|
||||
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,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 };
|
||||
@@ -47,11 +47,18 @@ function neuesterBericht(gameDir, seit) {
|
||||
|
||||
/*
|
||||
* Zieht die aussagekräftigen Zeilen aus einem Bericht.
|
||||
* -> { beschreibung, fehler, verdaechtigeMods, verlauf: [] }
|
||||
* -> { beschreibung, fehler, verdaechtigeMods, diagnose, empfehlung, verlauf: [] }
|
||||
*/
|
||||
function auswerten(text) {
|
||||
const zeilen = String(text || '').split(/\r?\n/);
|
||||
const befund = { beschreibung: '', fehler: '', verdaechtigeMods: '', verlauf: [] };
|
||||
const befund = {
|
||||
beschreibung: '',
|
||||
fehler: '',
|
||||
verdaechtigeMods: '',
|
||||
diagnose: '',
|
||||
empfehlung: '',
|
||||
verlauf: [],
|
||||
};
|
||||
|
||||
for (let i = 0; i < zeilen.length; i++) {
|
||||
const z = zeilen[i];
|
||||
@@ -86,6 +93,22 @@ function auswerten(text) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -6,20 +6,29 @@
|
||||
content="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'; img-src 'self' data: https:; connect-src 'self';" />
|
||||
<title>AeroMC</title>
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
<link rel="stylesheet" href="designs.css" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- Eigene Titelleiste (Fensterknöpfe zeichnet Windows rechts darüber) -->
|
||||
<div class="titelleiste">
|
||||
<img src="assets/logo.png" alt="" class="titel-logo" />
|
||||
<span class="titel-text">AeroMC</span>
|
||||
</div>
|
||||
|
||||
<!-- Obere Werkzeugleiste (MultiMC-Stil: Icon + Text) -->
|
||||
<header class="toolbar">
|
||||
<button id="btn-new" class="tbtn" data-ic="plus">Instanz hinzufügen</button>
|
||||
<button id="btn-new" class="tbtn" data-ic="plus" title="Neue Instanz (Strg+N)">Instanz hinzufügen</button>
|
||||
<button id="btn-import" class="tbtn" data-ic="import" title="Instanzen importieren">Importieren</button>
|
||||
<button id="btn-open-dir" class="tbtn" data-ic="folder" title="Ordner öffnen">Ordner</button>
|
||||
<span class="tbsep"></span>
|
||||
<button id="btn-bedrock" class="tbtn" data-ic="blocks" title="Minecraft Bedrock (Windows-Ausgabe)">Bedrock</button>
|
||||
<button id="btn-console" class="tbtn" data-ic="terminal" title="Konsole / Log">Konsole</button>
|
||||
<button id="btn-skin" class="tbtn" data-ic="user" title="Skin-Hintergrund ein/aus">Skin</button>
|
||||
<button id="btn-stats" class="tbtn" data-ic="chart" title="Spielzeit-Statistik">Statistik</button>
|
||||
<button id="btn-friends" class="tbtn" data-ic="users" title="Freunde (ingame: Strg+Alt+F zeigt, wer online ist)">Freunde<span id="friends-badge" class="tb-badge hidden"></span></button>
|
||||
<span class="tbgrow"></span>
|
||||
<div class="search-wrap">
|
||||
<input id="search" type="text" placeholder="Suchen…" autocomplete="off" />
|
||||
<input id="search" type="text" placeholder="Suchen… (Strg+F)" autocomplete="off" title="Strg+F fokussiert die Suche" />
|
||||
</div>
|
||||
<button id="btn-account" class="tbtn acct-btn" title="Microsoft-Konto"><span id="account-av" class="acct-av"></span><span id="account-name">Konten</span></button>
|
||||
<button id="btn-settings" class="tbtn" data-ic="settings" title="Einstellungen">Optionen</button>
|
||||
@@ -51,6 +60,7 @@
|
||||
<button id="p-mods" class="sbtn">Mods anzeigen</button>
|
||||
<button id="p-worlds" class="sbtn">Welten verwalten</button>
|
||||
<button id="p-screens" class="sbtn">Bildschirmfotos verwalten</button>
|
||||
<button id="p-record" class="sbtn">Spiel aufzeichnen</button>
|
||||
<button id="p-modpack" class="sbtn hidden">Modpack aktualisieren</button>
|
||||
<button id="p-logs" class="sbtn">Logdateien ansehen</button>
|
||||
<button id="p-servers" class="sbtn">Server verwalten</button>
|
||||
@@ -61,7 +71,8 @@
|
||||
<div class="hr"></div>
|
||||
<button id="p-shortcut" class="sbtn">Verknüpfung erstellen</button>
|
||||
<button id="p-export" class="sbtn">Instanz exportieren</button>
|
||||
<button id="p-delete" class="sbtn del">Löschen</button>
|
||||
<button id="p-share" class="sbtn" title="Code erzeugen, mit dem Freunde diese Instanz samt Mods nachbauen können">Instanz teilen</button>
|
||||
<button id="p-delete" class="sbtn del" title="Entf">Löschen</button>
|
||||
<button id="p-copy" class="sbtn">Instanz kopieren</button>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -130,6 +141,7 @@
|
||||
<option value="forge">Forge</option>
|
||||
<option value="neoforge">NeoForge</option>
|
||||
<option value="quilt">Quilt</option>
|
||||
<option value="optifine">OptiFine</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">
|
||||
@@ -139,6 +151,30 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- nur beim Anlegen einer Fabric-/Quilt-/NeoForge-Instanz -->
|
||||
<label id="f-perf-row" class="checkbox-field hidden" style="margin:-2px 0 10px">
|
||||
<input id="f-perf" type="checkbox" />
|
||||
<span>Performance-Paket mitinstallieren
|
||||
<small class="hint">Sodium, Lithium, FerriteCore, ImmediatelyFast, Entity Culling, ModernFix – für mehr FPS und kürzere Ladezeiten</small></span>
|
||||
</label>
|
||||
|
||||
<div id="f-optifine-tools" class="field-row hidden" style="align-items:flex-start">
|
||||
<div class="field" style="flex:1">
|
||||
<span>OptiFine einrichten</span>
|
||||
<p class="hint" style="margin:2px 0 8px">
|
||||
OptiFine bietet kein automatisches Download-API (Downloads laufen bei OptiFine bewusst über eine Werbeseite).
|
||||
Lade den Installer zuerst selbst von <b>optifine.net</b> herunter, wähle ihn dann hier aus – AeroMC bereitet
|
||||
die passende Minecraft-Version vor und startet den echten OptiFine-Installer. Wähle darin als Zielordner den
|
||||
automatisch in die Zwischenablage kopierten Pfad, klicke „Install" und bestätige danach hier.
|
||||
</p>
|
||||
<div class="path-row">
|
||||
<button type="button" id="f-optifine-install" class="btn">Installer wählen und starten…</button>
|
||||
<button type="button" id="f-optifine-confirm" class="btn btn-primary hidden">Fertig – Version übernehmen</button>
|
||||
</div>
|
||||
<small id="f-optifine-status" class="hint"></small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="field">
|
||||
<span>Notizen <small class="opt">(optional)</small></span>
|
||||
<textarea id="f-notes" rows="2" placeholder="Modliste, Server-IP, To-dos …"></textarea>
|
||||
@@ -171,7 +207,11 @@
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Zusätzliche JVM-Argumente</span>
|
||||
<input id="f-extra-args" type="text" placeholder="z. B. -XX:+UseG1GC" />
|
||||
<div class="path-row">
|
||||
<input id="f-extra-args" type="text" placeholder="z. B. -XX:+UseG1GC" />
|
||||
<button id="f-extra-args-aikar" class="btn btn-mini" type="button"
|
||||
title="Fügt eine bewährte Sammlung von Garbage-Collector-Flags ein (bekannt als „Aikar's Flags"), die bei größeren Modpacks Ruckler durch Garbage Collection reduzieren kann.">GC-Flags einfügen</button>
|
||||
</div>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Pre-Launch-Befehl <small class="opt">(läuft vor dem Start)</small></span>
|
||||
@@ -217,14 +257,7 @@
|
||||
</div>
|
||||
<div class="settings-grid">
|
||||
<section class="settings-card">
|
||||
<h5>Oberfläche</h5>
|
||||
<label class="field">
|
||||
<span>Design</span>
|
||||
<select id="s-theme">
|
||||
<option value="dark">Dunkel</option>
|
||||
<option value="light">Hell</option>
|
||||
</select>
|
||||
</label>
|
||||
<h5>Instanzliste</h5>
|
||||
<label class="field">
|
||||
<span>Instanzen sortieren</span>
|
||||
<select id="s-sortby">
|
||||
@@ -253,6 +286,34 @@
|
||||
</label>
|
||||
<small id="s-bedrock-hint" class="hint">Wird automatisch ausgeblendet, wenn Bedrock nicht installiert ist.</small>
|
||||
</section>
|
||||
|
||||
<section class="settings-card">
|
||||
<h5>Benachrichtigungen</h5>
|
||||
<label class="checkbox-field">
|
||||
<input id="s-notify" type="checkbox" /> <span>Windows-Benachrichtigung, wenn ein großer Download oder ein Backup im Hintergrund fertig wird</span>
|
||||
</label>
|
||||
<small class="hint">Erscheint nur, während das Launcher-Fenster nicht im Vordergrund ist.</small>
|
||||
</section>
|
||||
|
||||
<section class="settings-card">
|
||||
<h5>Freundesystem</h5>
|
||||
<label class="checkbox-field">
|
||||
<input id="s-friends" type="checkbox" /> <span>Freundesystem aktivieren</span>
|
||||
</label>
|
||||
<small class="hint">Ausgeschaltet: du bist für niemanden sichtbar und kannst von niemandem als Freund gefunden/hinzugefügt werden. Bestehende Freundschaften bleiben erhalten, zeigen dich aber als offline.</small>
|
||||
<label class="checkbox-field" style="margin-top:10px">
|
||||
<input id="s-friends-server" type="checkbox" /> <span>Server, auf dem ich spiele, für Freunde sichtbar</span>
|
||||
</label>
|
||||
<small class="hint">Ausgeschaltet sehen Freunde nur, dass du spielst – nicht wo. „Beitreten“ ist dann bei dir nicht möglich, Einladungen kannst du aber weiterhin verschicken.</small>
|
||||
</section>
|
||||
|
||||
<section class="settings-card">
|
||||
<h5>Autostart</h5>
|
||||
<label class="checkbox-field">
|
||||
<input id="s-autostart" type="checkbox" /> <span>Mit Windows starten (im Infobereich)</span>
|
||||
</label>
|
||||
<small class="hint">Startet AeroMC beim Anmelden automatisch im Hintergrund. Solange aktiv, landet der Launcher beim Schließen (X) nur im Infobereich neben der Uhr, statt sich zu beenden – über das Symbol dort lässt er sich jederzeit wieder öffnen oder wirklich beenden.</small>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -262,6 +323,37 @@
|
||||
<p>Optik der Oberfläche und visuelle Extras für die Instanzansicht.</p>
|
||||
</div>
|
||||
<div class="settings-grid">
|
||||
<section class="settings-card settings-card-wide">
|
||||
<h5>Design</h5>
|
||||
<div class="field">
|
||||
<select id="s-theme" class="hidden">
|
||||
<option value="dark">Klassisch – Dunkel</option>
|
||||
<option value="light">Klassisch – Hell</option>
|
||||
<option value="aero">AeroMC</option>
|
||||
<option value="lila">Nachtlila</option>
|
||||
<option value="minecraft">Minecraft</option>
|
||||
</select>
|
||||
<div class="design-raster">
|
||||
<button type="button" class="design-wahl" data-design="dark" title="Der bewährte Launcher-Look in Dunkelgrau">
|
||||
<span class="dv dv-dark"><i></i><b></b><em></em></span><small>Klassisch Dunkel</small>
|
||||
</button>
|
||||
<button type="button" class="design-wahl" data-design="light" title="Der bewährte Launcher-Look in Hell">
|
||||
<span class="dv dv-light"><i></i><b></b><em></em></span><small>Klassisch Hell</small>
|
||||
</button>
|
||||
<button type="button" class="design-wahl" data-design="aero" title="Nachtblau mit Cyan-Akzenten – wie die AeroMC-Webseite">
|
||||
<span class="dv dv-aero"><i></i><b></b><em></em></span><small>AeroMC</small>
|
||||
</button>
|
||||
<button type="button" class="design-wahl" data-design="lila" title="Wie AeroMC, nur in dunklem Lila">
|
||||
<span class="dv dv-lila"><i></i><b></b><em></em></span><small>Nachtlila</small>
|
||||
</button>
|
||||
<button type="button" class="design-wahl" data-design="minecraft" title="Block-Optik wie im Spiel">
|
||||
<span class="dv dv-mc"><i></i><b></b><em></em></span><small>Minecraft</small>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<small class="hint">Ein Klick zeigt das Design sofort an – mit „Speichern“ gilt es in allen Fenstern.</small>
|
||||
</section>
|
||||
|
||||
<section class="settings-card">
|
||||
<h5>Icons</h5>
|
||||
<label class="checkbox-field">
|
||||
@@ -289,11 +381,27 @@
|
||||
<option value="spooky">Unheimlich</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Armform</span>
|
||||
<select id="s-bgskin-model" title="Bei „Automatisch“ wird die Armform übernommen, die beim Konto eingestellt ist">
|
||||
<option value="auto">Automatisch (vom Konto)</option>
|
||||
<option value="classic">Normal (breite Arme, Steve)</option>
|
||||
<option value="slim">Schmal (Alex)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Deckkraft: <span id="s-bgskin-val">55</span> %</span>
|
||||
<input id="s-bgskin-opacity" type="range" min="10" max="100" step="5" />
|
||||
</label>
|
||||
</div>
|
||||
<label class="checkbox-field">
|
||||
<input id="s-bgskin-cycle" type="checkbox" /> <span>Bei mehreren angemeldeten Konten automatisch durchwechseln</span>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Wechsel alle <small class="opt">(Sekunden)</small></span>
|
||||
<input id="s-bgskin-cycle-seconds" type="number" min="5" step="5" />
|
||||
<small class="hint">Läuft nur, wenn 2 oder mehr Konten angemeldet sind, und beginnt nach dem letzten Konto wieder von vorn.</small>
|
||||
</label>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@@ -329,6 +437,14 @@
|
||||
<label class="field"><span>Fensterhöhe</span><input id="s-mc-height" type="number" min="240" step="10" /></label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-card">
|
||||
<h5>Mods</h5>
|
||||
<label class="checkbox-field">
|
||||
<input id="s-auto-mod-updates" type="checkbox" /> <span>Vor jedem Start nach Mod-Updates suchen und installieren</span>
|
||||
</label>
|
||||
<small class="hint">Gilt für Mods aus Modrinth. Dauert die Suche länger als 20 Sekunden, startet Minecraft ohne Update. Abgeschaltete Mods bleiben abgeschaltet.</small>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -507,6 +623,14 @@
|
||||
<small id="s-autobackup-last" class="hint">Noch keine automatische Sicherung.</small>
|
||||
</section>
|
||||
|
||||
<section class="settings-card">
|
||||
<h5>Welten</h5>
|
||||
<label class="checkbox-field">
|
||||
<input id="s-auto-world" type="checkbox" /> <span>Nach jeder Spielsitzung die zuletzt gespielte Welt sichern</span>
|
||||
</label>
|
||||
<small class="hint">Läuft im Hintergrund, sobald Minecraft beendet ist. Pro Welt bleiben die letzten 5 Stände erhalten – unter Dokumente\AeroMC Launcher\Welten-Sicherungen\<Instanz>\Automatisch.</small>
|
||||
</section>
|
||||
|
||||
<section class="settings-card settings-card-wide">
|
||||
<h5>Wiederherstellen</h5>
|
||||
<small class="hint">Wähle ein Backup aus Dokumente/AeroMC Launcher/Backups aus. Achtung: Alle vorhandenen Instanzen werden beim Wiederherstellen mit dem Inhalt des Backups überschrieben. Launcher-Einstellungen werden ebenfalls wiederhergestellt. Konten und Tokens bleiben unverändert.</small>
|
||||
@@ -614,6 +738,7 @@
|
||||
<option value="mod">Mods</option>
|
||||
<option value="resourcepack">Ressourcenpakete</option>
|
||||
<option value="shader">Shader</option>
|
||||
<option value="datapack">Datenpakete</option>
|
||||
</select>
|
||||
<input id="m-query" type="text" placeholder="Mods durchsuchen … (z. B. Sodium, JEI, Create)" />
|
||||
<button id="m-search-btn" class="btn btn-primary">Suchen</button>
|
||||
@@ -659,8 +784,14 @@
|
||||
</div>
|
||||
<div id="m-pane-installed" class="hidden">
|
||||
<div class="mmc-bar">
|
||||
<button id="m-add-file" class="btn btn-mini" data-ic="plus">Datei hinzufügen…</button>
|
||||
<button id="m-check-updates" class="btn btn-mini">Nach Updates suchen</button>
|
||||
<button id="m-apply-updates" class="btn btn-primary btn-mini hidden">Alle aktualisieren</button>
|
||||
<select id="m-installed-sort" title="Sortierung">
|
||||
<option value="name-asc">Name (A-Z)</option>
|
||||
<option value="name-desc">Name (Z-A)</option>
|
||||
<option value="status">Status (aktiv zuerst)</option>
|
||||
</select>
|
||||
<span id="m-update-status" class="hint"></span>
|
||||
</div>
|
||||
<div id="m-installed" class="mods-list"><div class="mods-empty">Noch keine Mods.</div></div>
|
||||
@@ -752,6 +883,141 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===================== Dialog: Spielaufzeichnung ===================== -->
|
||||
<div id="modal-record" class="modal hidden">
|
||||
<div class="modal-card modal-lg">
|
||||
<div class="modal-head">
|
||||
<h3 id="record-title">Spielaufzeichnung</h3>
|
||||
<button class="modal-close" data-close>✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="record-search" class="record-search">
|
||||
<p id="record-search-text">Suche Spielfenster …</p>
|
||||
<button id="record-search-retry" class="btn hidden">Erneut suchen</button>
|
||||
</div>
|
||||
|
||||
<div id="record-picker" class="screens-grid hidden"></div>
|
||||
|
||||
<div id="record-stage" class="record-stage hidden">
|
||||
<video id="record-preview" class="record-video" autoplay muted></video>
|
||||
<div class="record-audio-bar">
|
||||
<span class="hint">Video: <span id="record-video-summary">Nativ · Mittel</span></span>
|
||||
<button id="record-video-edit" class="btn btn-mini">Video-Einstellungen…</button>
|
||||
</div>
|
||||
<div class="record-audio-bar">
|
||||
<span class="hint">Audio: <span id="record-audio-summary">Systemton</span></span>
|
||||
<button id="record-audio-edit" class="btn btn-mini">Audioquellen wählen…</button>
|
||||
</div>
|
||||
<!-- Clip-Modus: läuft im Hintergrund mit, Tastenkürzel speichert die letzten Sekunden -->
|
||||
<div id="clip-bar" class="clip-bar">
|
||||
<label class="clip-switch" title="Nimmt im Hintergrund laufend auf und behält nur die letzten Sekunden">
|
||||
<input id="clip-toggle" type="checkbox" />
|
||||
<span class="clip-switch-track" aria-hidden="true"><span class="clip-switch-knob"></span></span>
|
||||
<span class="clip-switch-label">Clip-Modus</span>
|
||||
</label>
|
||||
<label class="clip-length">
|
||||
<span>Länge</span>
|
||||
<select id="clip-length" title="Länge eines Clips">
|
||||
<option value="30">30 Sek.</option>
|
||||
<option value="60" selected>60 Sek.</option>
|
||||
<option value="90">90 Sek.</option>
|
||||
</select>
|
||||
</label>
|
||||
<span id="clip-status" class="clip-status">Speichert auf Tastendruck die letzten Sekunden – auch bei geschlossenem Fenster.</span>
|
||||
<button id="clip-save" class="btn btn-mini" disabled>Clip speichern</button>
|
||||
</div>
|
||||
<div class="record-toolbar">
|
||||
<div id="record-live-badge" class="record-live-badge hidden">
|
||||
<span class="rec-dot"></span>
|
||||
<span>Aufnahme läuft · <span id="record-timer">00:00</span></span>
|
||||
</div>
|
||||
<button id="record-toggle" class="btn btn-primary">Aufnahme starten</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<button id="record-open-folder" class="btn" data-ic="folder">Ordner öffnen</button>
|
||||
<div class="spacer"></div>
|
||||
<button class="btn" data-close>Schließen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===================== Dialog: Video-Einstellungen der Aufzeichnung ===================== -->
|
||||
<div id="modal-record-video" class="modal hidden">
|
||||
<div class="modal-card modal-sm">
|
||||
<div class="modal-head">
|
||||
<h3>Video-Einstellungen</h3>
|
||||
<button class="modal-close" data-close>✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="field-row">
|
||||
<label class="field">
|
||||
<span>Auflösung</span>
|
||||
<select id="record-resolution">
|
||||
<option value="native">Nativ (Fenstergröße)</option>
|
||||
<option value="1080">1080p</option>
|
||||
<option value="720">720p</option>
|
||||
<option value="480">480p</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Qualität</span>
|
||||
<select id="record-quality">
|
||||
<option value="low">Niedrig (kleinere Datei)</option>
|
||||
<option value="medium" selected>Mittel</option>
|
||||
<option value="high">Hoch (größere Datei)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Bildrate</span>
|
||||
<select id="record-fps">
|
||||
<option value="30" selected>30 fps</option>
|
||||
<option value="60">60 fps (größere Datei)</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<p id="record-video-hint" class="hint hidden">Gilt erst für die nächste Aufnahme – während einer laufenden Aufnahme nicht änderbar.</p>
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<div class="spacer"></div>
|
||||
<button class="btn btn-primary" data-close>Fertig</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===================== Dialog: Audioquellen der Aufzeichnung ===================== -->
|
||||
<div id="modal-record-audio" class="modal hidden">
|
||||
<div class="modal-card modal-sm">
|
||||
<div class="modal-head">
|
||||
<h3>Audioquellen wählen</h3>
|
||||
<button class="modal-close" data-close>✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="hint">Mehrfachauswahl möglich – alle angehakten Quellen werden zu einer Tonspur gemischt, Lautstärke je Quelle einstellbar.</p>
|
||||
<div class="record-source-row">
|
||||
<label class="checkbox-field">
|
||||
<input id="record-src-system" type="checkbox" checked /> <span>Systemton</span>
|
||||
</label>
|
||||
<input id="record-vol-system" class="record-vol" type="range" min="0" max="100" value="100" title="Lautstärke" />
|
||||
</div>
|
||||
<div class="record-source-row">
|
||||
<label class="checkbox-field">
|
||||
<input id="record-src-game" type="checkbox" disabled /> <span>Nur Minecraft (isoliert)</span>
|
||||
</label>
|
||||
<input id="record-vol-game" class="record-vol" type="range" min="0" max="100" value="100" title="Lautstärke" />
|
||||
</div>
|
||||
<p id="record-src-game-hint" class="hint hidden">Kein laufender Minecraft-Prozess erkannt – bitte über den Launcher starten.</p>
|
||||
<div id="record-mic-list" class="record-mic-list"></div>
|
||||
<p class="hint" style="margin-top:10px">Systemton ist immer der komplette Mix (Spiel, Discord, Musik, Benachrichtigungen usw.). "Nur Minecraft" nimmt ausschließlich den Ton des Spiels auf (z. B. wenn nebenbei Musik läuft, die nicht mit rein soll) – beides lässt sich auch kombiniert mit einem Mikrofon anhaken.</p>
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<div class="spacer"></div>
|
||||
<button class="btn btn-primary" data-close>Fertig</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===================== Dialog: Screenshots ===================== -->
|
||||
<div id="modal-screens" class="modal hidden">
|
||||
<div class="modal-card modal-lg">
|
||||
@@ -760,13 +1026,30 @@
|
||||
<button class="modal-close" data-close>✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="screens-toolbar">
|
||||
<button id="screens-select-all" class="btn btn-mini">Alle auswählen</button>
|
||||
<button id="screens-select-none" class="btn btn-mini">Auswahl aufheben</button>
|
||||
<button id="screens-delete-sel" class="btn btn-danger btn-mini" disabled>Auswahl löschen</button>
|
||||
<span id="screens-sel-hint" class="hint">Klick auf Vorschaubild zum Auswählen</span>
|
||||
<div class="mods-tabs">
|
||||
<button id="screens-tab-photos" class="mtab on">Bildschirmfotos</button>
|
||||
<button id="screens-tab-records" class="mtab">Aufzeichnungen</button>
|
||||
</div>
|
||||
|
||||
<div id="screens-pane-photos">
|
||||
<div class="screens-toolbar">
|
||||
<button id="screens-select-all" class="btn btn-mini">Alle auswählen</button>
|
||||
<button id="screens-select-none" class="btn btn-mini">Auswahl aufheben</button>
|
||||
<button id="screens-delete-sel" class="btn btn-danger btn-mini" disabled>Auswahl löschen</button>
|
||||
<span id="screens-sel-hint" class="hint">Klick auf Vorschaubild zum Auswählen</span>
|
||||
</div>
|
||||
<div id="screens-grid" class="screens-grid"></div>
|
||||
</div>
|
||||
|
||||
<div id="screens-pane-records" class="hidden">
|
||||
<div class="screens-toolbar">
|
||||
<button id="records-select-all" class="btn btn-mini">Alle auswählen</button>
|
||||
<button id="records-select-none" class="btn btn-mini">Auswahl aufheben</button>
|
||||
<button id="records-delete-sel" class="btn btn-danger btn-mini" disabled>Auswahl löschen</button>
|
||||
<span id="records-sel-hint" class="hint">Klick auf Vorschaubild zum Auswählen, Doppelklick zum Abspielen</span>
|
||||
</div>
|
||||
<div id="records-grid" class="screens-grid"></div>
|
||||
</div>
|
||||
<div id="screens-grid" class="screens-grid"></div>
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<button id="screens-open" class="btn" data-ic="folder">Ordner</button>
|
||||
@@ -776,6 +1059,96 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===================== Dialog: Instanz teilen ===================== -->
|
||||
<div id="modal-share" class="modal hidden">
|
||||
<div class="modal-card modal-sm">
|
||||
<div class="modal-head">
|
||||
<h3 id="share-title">Instanz teilen</h3>
|
||||
<button class="modal-close" data-close>✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p id="share-status" class="hint">Code wird erstellt …</p>
|
||||
<div id="share-result" class="hidden">
|
||||
<p class="hint" style="margin-top:0">Gib diesen Code an deine Freunde weiter. Sie lösen ihn unter <b>Importieren → Per Code</b> ein.</p>
|
||||
<div class="share-code-box">
|
||||
<span id="share-code" class="share-code">XXXX-XXXX</span>
|
||||
<button id="share-copy" class="btn btn-mini">Kopieren</button>
|
||||
</div>
|
||||
<p id="share-info" class="hint"></p>
|
||||
<p id="share-warn" class="hint share-warn hidden"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<div class="spacer"></div>
|
||||
<button class="btn" data-close>Schließen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===================== Dialog: Spielzeit-Statistik ===================== -->
|
||||
<div id="modal-stats" class="modal hidden">
|
||||
<div class="modal-card">
|
||||
<div class="modal-head">
|
||||
<h3>Spielzeit-Statistik</h3>
|
||||
<button class="modal-close" data-close>✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="stats-tiles" class="stats-tiles"></div>
|
||||
<div class="friends-section-title">Spielzeit pro Instanz</div>
|
||||
<div id="stats-list" class="stats-list" role="list"></div>
|
||||
<p id="stats-unplayed" class="hint"></p>
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<div class="spacer"></div>
|
||||
<button class="btn" data-close>Schließen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===================== Dialog: Freunde ===================== -->
|
||||
<div id="modal-friends" class="modal hidden">
|
||||
<div class="modal-card modal-sm">
|
||||
<div class="modal-head">
|
||||
<h3>Freunde</h3>
|
||||
<button class="modal-close" data-close>✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p id="friends-disabled-hint" class="hint hidden">Das Freundesystem ist ausgeschaltet – aktiviere es in den Einstellungen, um Freunde zu sehen oder hinzuzufügen.</p>
|
||||
<div id="friends-content">
|
||||
<div class="field-row">
|
||||
<input id="friends-add-input" type="text" placeholder="Minecraft-Name…" maxlength="16" />
|
||||
<button id="friends-add-btn" class="btn btn-mini">Anfrage senden</button>
|
||||
</div>
|
||||
<div id="friends-requests-box"></div>
|
||||
<div id="friends-list-box"></div>
|
||||
</div>
|
||||
<!-- Unterhaltung mit einem Freund -->
|
||||
<div id="friends-chat" class="friends-chat hidden">
|
||||
<div class="chat-head">
|
||||
<button id="chat-back" class="btn btn-mini" title="Zurück zur Freundesliste">← Zurück</button>
|
||||
<img id="chat-avatar" class="friend-avatar" alt="" />
|
||||
<b id="chat-name"></b>
|
||||
</div>
|
||||
<div id="chat-verlauf" class="chat-verlauf"></div>
|
||||
<div class="chat-schnell">
|
||||
<button class="btn btn-mini chat-vorlage">Bin in 10 Min. online</button>
|
||||
<button class="btn btn-mini chat-vorlage">Komm online!</button>
|
||||
<button class="btn btn-mini chat-vorlage">Bin gleich zurück</button>
|
||||
<button class="btn btn-mini chat-vorlage">Gute Nacht 👋</button>
|
||||
</div>
|
||||
<div class="path-row">
|
||||
<input id="chat-text" type="text" maxlength="200" placeholder="Nachricht …" autocomplete="off" />
|
||||
<button id="chat-send" class="btn btn-primary">Senden</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<div class="spacer"></div>
|
||||
<button class="btn" data-close>Schließen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===================== Dialog: Modpack aktualisieren ===================== -->
|
||||
<div id="modal-modpack" class="modal hidden">
|
||||
<div class="modal-card modal-sm">
|
||||
@@ -808,7 +1181,7 @@
|
||||
<div class="field-row" style="margin-top:12px">
|
||||
<label class="field"><span>Name</span><input id="srv-name" type="text" placeholder="z. B. Mein Server" /></label>
|
||||
<label class="field"><span>Adresse</span><input id="srv-ip" type="text" placeholder="mc.example.de oder 192.168.1.50:25566" /></label>
|
||||
<button id="srv-add" class="btn btn-primary" style="align-self:flex-end">Hinzufügen</button>
|
||||
<div class="field" style="flex:0 0 auto"><span> </span><button id="srv-add" class="btn btn-primary">Hinzufügen</button></div>
|
||||
</div>
|
||||
<div id="srv-list" class="mods-list" style="margin-top:12px"></div>
|
||||
</div>
|
||||
@@ -851,19 +1224,19 @@
|
||||
<div class="skin-spalte">
|
||||
<div class="skin-label">Aktuell</div>
|
||||
<div class="skin-bild">
|
||||
<img id="skin-aktuell" alt="" class="hidden" />
|
||||
<canvas id="skin-aktuell" class="hidden"></canvas>
|
||||
<span id="skin-aktuell-leer" class="hint">wird geladen …</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="skin-spalte">
|
||||
<div class="skin-label">Neu</div>
|
||||
<div class="skin-bild">
|
||||
<img id="skin-neu" alt="" class="hidden" />
|
||||
<canvas id="skin-neu" class="hidden"></canvas>
|
||||
<span id="skin-neu-leer" class="hint">noch keine Datei gewählt</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<small class="hint">Die Vorschau zeigt die Figur so, wie sie im Spiel aussieht. Die Armform wirkt sich auf die Darstellung aus.</small>
|
||||
<small class="hint">Mit der Maus ziehen dreht die Figur, Mausrad zoomt. Die Armform wirkt sich auf die Darstellung aus.</small>
|
||||
<div class="path-row" style="margin-top:12px">
|
||||
<input id="skin-pfad" type="text" readonly placeholder="PNG mit 64×64 Pixeln" />
|
||||
<button id="skin-pick" class="btn">Datei wählen…</button>
|
||||
@@ -879,6 +1252,7 @@
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<button id="skin-reset" class="btn btn-danger">Auf Standard zurücksetzen</button>
|
||||
<button id="skin-editor" class="btn" title="Skin selbst pixeln – mit Grundschicht und 2. Schicht">Im Editor bearbeiten…</button>
|
||||
<div class="spacer"></div>
|
||||
<button class="btn" data-close>Schließen</button>
|
||||
<button id="skin-apply" class="btn btn-primary" disabled>Skin setzen</button>
|
||||
@@ -886,6 +1260,86 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===================== Dialog: Skin-Editor ===================== -->
|
||||
<div id="modal-skineditor" class="modal hidden">
|
||||
<div class="modal-card se-karte">
|
||||
<div class="modal-head">
|
||||
<h3>Skin-Editor</h3>
|
||||
<div class="se-modus">
|
||||
<button class="se-modus-btn on" data-se-modus="2d" title="Auf der aufgeklappten Vorlage malen">2D-Vorlage</button>
|
||||
<button class="se-modus-btn" data-se-modus="3d" title="Direkt auf der drehbaren Figur malen">3D-Figur</button>
|
||||
</div>
|
||||
<button class="modal-close" data-close>✕</button>
|
||||
</div>
|
||||
<div class="modal-body se-body">
|
||||
<aside class="se-werkzeuge">
|
||||
<div class="se-gruppe">
|
||||
<div class="se-titel">Werkzeug</div>
|
||||
<div class="se-tools">
|
||||
<button class="se-tool on" data-se-tool="stift" title="Stift (B)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20h4L18 10l-4-4L4 16v4zM14 6l4 4"/></svg><span>Stift</span></button>
|
||||
<button class="se-tool" data-se-tool="radierer" title="Radierer (E)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 20h12M5.5 14.5l8-8a2 2 0 012.8 0l2.2 2.2a2 2 0 010 2.8L11 19H8.5l-3-3a1.1 1.1 0 010-1.5zM9 11l5 5"/></svg><span>Radierer</span></button>
|
||||
<button class="se-tool" data-se-tool="pipette" title="Pipette (I) – auch Alt+Klick"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 7l3 3M5 19l1-4 8.5-8.5a2.1 2.1 0 013 3L9 18l-4 1zM15.5 5.5l1.5-1.5a2.1 2.1 0 013 3l-1.5 1.5"/></svg><span>Pipette</span></button>
|
||||
<button class="se-tool" data-se-tool="fuellen" title="Füllen (G) – färbt gleichfarbige Pixel einer Seite"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 11l7-7 7 7-7 7zM5 11h14M20 16s1.5 2 1.5 3a1.5 1.5 0 01-3 0c0-1 1.5-3 1.5-3z"/></svg><span>Füllen</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="se-gruppe">
|
||||
<div class="se-titel">Farbe</div>
|
||||
<div class="se-farbzeile">
|
||||
<input id="se-farbe" type="color" value="#3a6ea5" title="Farbe wählen" />
|
||||
<input id="se-hex" type="text" maxlength="7" spellcheck="false" value="#3a6ea5" />
|
||||
</div>
|
||||
<div id="se-zuletzt" class="se-zuletzt"></div>
|
||||
</div>
|
||||
<div class="se-gruppe">
|
||||
<div class="se-titel">Ebene</div>
|
||||
<div class="se-ebenen">
|
||||
<button class="se-ebene on" data-se-schicht="0" title="Die eigentliche Figur (L wechselt)">Grundschicht</button>
|
||||
<button class="se-ebene" data-se-schicht="1" title="Hülle über der Figur – Haare, Jacke, Ärmel … (L wechselt)">2. Schicht</button>
|
||||
</div>
|
||||
<label class="se-check"><input id="se-aussen" type="checkbox" checked /> 2. Schicht zeigen</label>
|
||||
<label class="se-check"><input id="se-durch" type="checkbox" checked /> Grundschicht durchscheinen</label>
|
||||
<button id="se-leeren" class="btn btn-mini">2. Schicht leeren</button>
|
||||
</div>
|
||||
<div class="se-gruppe">
|
||||
<div class="se-titel">Optionen</div>
|
||||
<label class="se-check" title="Malt gleichzeitig auf der anderen Körperseite (M)"><input id="se-spiegeln" type="checkbox" /> Spiegeln (links ↔ rechts)</label>
|
||||
<label class="se-feld"><span>Armform</span>
|
||||
<select id="se-variante">
|
||||
<option value="classic">Klassisch (Steve)</option>
|
||||
<option value="slim">Schmal (Alex)</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="se-undo">
|
||||
<button id="se-undo" class="btn btn-mini" title="Rückgängig (Strg+Z)" disabled>↶ Rückgängig</button>
|
||||
<button id="se-redo" class="btn btn-mini" title="Wiederholen (Strg+Y)" disabled>↷ Wiederholen</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<section class="se-mitte">
|
||||
<div id="se-buehne" class="se-buehne">
|
||||
<div id="se-2d" class="se-ansicht se-2d"><canvas id="se-2d-canvas"></canvas></div>
|
||||
</div>
|
||||
<div id="se-info" class="se-info hint"></div>
|
||||
</section>
|
||||
<aside class="se-seite">
|
||||
<div id="se-mini-titel" class="se-titel">3D-Vorschau</div>
|
||||
<div id="se-mini" class="se-mini">
|
||||
<div id="se-3d" class="se-ansicht se-3d"><canvas id="se-3d-canvas"></canvas><span id="se-3d-leer" class="hint se-3d-leer">wird geladen …</span></div>
|
||||
</div>
|
||||
<p class="hint se-tipp">Beide Ansichten sind bemalbar. Die <b>2. Schicht</b> liegt wie eine Hülle über der Figur – lässt man Pixel darin frei, wirken Haare, Kapuzen oder Ärmel plastisch.</p>
|
||||
</aside>
|
||||
</div>
|
||||
<div class="modal-foot se-fuss">
|
||||
<button id="se-neu" class="btn" title="Mit einer leeren Vorlage beginnen">Neu</button>
|
||||
<button id="se-laden" class="btn" title="Den Skin deines angemeldeten Kontos laden">Aktuellen Skin laden</button>
|
||||
<button id="se-oeffnen" class="btn">PNG öffnen…</button>
|
||||
<button id="se-speichern" class="btn">Als PNG speichern…</button>
|
||||
<span id="se-status" class="hint se-status"></span>
|
||||
<button id="se-uebernehmen" class="btn btn-primary">Als Skin übernehmen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===================== Dialog: Logdateien ===================== -->
|
||||
<div id="modal-logs" class="modal hidden">
|
||||
<div class="modal-card modal-lg">
|
||||
@@ -921,8 +1375,9 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p id="crash-sub" class="hint">–</p>
|
||||
<div id="crash-hilfe" class="crash-hilfe"></div>
|
||||
<div id="crash-kern" class="crash-kern"></div>
|
||||
<details class="crash-details">
|
||||
<details id="crash-details" class="crash-details">
|
||||
<summary>Vollständigen Bericht anzeigen</summary>
|
||||
<pre id="crash-voll" class="crash-voll"></pre>
|
||||
</details>
|
||||
@@ -930,13 +1385,63 @@
|
||||
<div class="modal-foot">
|
||||
<button id="crash-folder" class="btn" data-ic="folder">Ordner öffnen</button>
|
||||
<button id="crash-console" class="btn">Konsole</button>
|
||||
<button id="crash-send" class="btn" title="Bericht und Mod-Liste an den Entwickler schicken">An M_Viper senden</button>
|
||||
<div class="spacer"></div>
|
||||
<button id="crash-retry" class="btn btn-primary hidden">Erneut starten</button>
|
||||
<button class="btn" data-close>Schließen</button>
|
||||
<button id="crash-copy" class="btn btn-primary">Bericht kopieren</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===================== Dialog: Fehlerbericht senden ===================== -->
|
||||
<div id="modal-report" class="modal hidden">
|
||||
<div class="modal-card modal-sm">
|
||||
<div class="modal-head">
|
||||
<h3>Fehlerbericht an M_Viper senden</h3>
|
||||
<button class="modal-close" data-close>✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="hint" style="margin-top:0">Damit hilfst du, Fehler in AeroMC zu finden und zu beheben. Gesendet wird:</p>
|
||||
<ul class="report-liste">
|
||||
<li>der Absturzbericht bzw. das Ende des Spielprotokolls</li>
|
||||
<li>die Liste deiner Mods dieser Instanz und ihre Versionen</li>
|
||||
<li>Minecraft-Version, Loader, Launcher-Version, Windows-Version und RAM</li>
|
||||
<li>was der Absturz-Assistent erkannt hat</li>
|
||||
</ul>
|
||||
<p class="hint">Dein Windows-Benutzername und Anmeldedaten werden vorher entfernt. Ohne Häkchen unten wird anonym gesendet – dann sind auch dein Minecraft-Name und deine UUID unkenntlich gemacht.</p>
|
||||
<label class="field">
|
||||
<span>Was hast du gemacht, als es passiert ist? <small class="opt">(optional)</small></span>
|
||||
<textarea id="report-kommentar" rows="3" maxlength="2000" placeholder="z. B. „Beim Betreten des Nethers abgestürzt“"></textarea>
|
||||
</label>
|
||||
<label class="checkbox-field">
|
||||
<input id="report-name" type="checkbox" /> <span>Meinen Minecraft-Namen mitsenden (für Rückfragen)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<div class="spacer"></div>
|
||||
<button class="btn" data-close>Abbrechen</button>
|
||||
<button id="report-do" class="btn btn-primary">Senden</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===================== Dialog: Lizenz ===================== -->
|
||||
<div id="modal-license" class="modal hidden">
|
||||
<div class="modal-card modal-lg">
|
||||
<div class="modal-head">
|
||||
<h3>Lizenz</h3>
|
||||
<button class="modal-close" data-close>✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<pre id="license-text" class="crash-voll">Lade …</pre>
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<button class="btn" data-close>Schließen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===================== Dialog: Update ===================== -->
|
||||
<div id="modal-update" class="modal hidden">
|
||||
<div class="modal-card modal-sm">
|
||||
@@ -979,7 +1484,10 @@
|
||||
<button id="import-tab-modpack" class="mtab" data-kind="modpack">Modpack</button>
|
||||
<button id="import-tab-mc" class="mtab" data-kind="mc">Minecraft Launcher</button>
|
||||
<button id="import-tab-cf" class="mtab" data-kind="cf">CurseForge</button>
|
||||
<button id="import-tab-mr" class="mtab" data-kind="mr">Modrinth App</button>
|
||||
<button id="import-tab-mmc" class="mtab" data-kind="mmc">MultiMC / Prism</button>
|
||||
<button id="import-tab-technic" class="mtab" data-kind="technic">Technic</button>
|
||||
<button id="import-tab-code" class="mtab" data-kind="code">Per Code</button>
|
||||
</div>
|
||||
|
||||
<div id="import-pane-zip" class="import-pane">
|
||||
@@ -989,11 +1497,18 @@
|
||||
|
||||
<div id="import-pane-modpack" class="import-pane hidden">
|
||||
<p class="import-copy">Modpack aus dem Katalog installieren oder als Datei importieren.</p>
|
||||
<p class="hint">Suche läuft über Modrinth. Datei-Import unterstützt zusätzlich CurseForge-ZIP (manifest.json) – dafür ist ein API-Key oder eine Weiterleitung in den Netzwerk-Einstellungen nötig.</p>
|
||||
<p class="hint">Katalog-Suche läuft über Modrinth oder CurseForge. Für CurseForge wird ein eigener API-Key in den Netzwerk-Einstellungen gebraucht (die Weiterleitung reicht nur für den Datei-Import). Datei-Import unterstützt zusätzlich CurseForge-ZIP (manifest.json).</p>
|
||||
|
||||
<div class="field-row" style="margin-top:12px;align-items:flex-end">
|
||||
<label class="field" style="width:150px">
|
||||
<span>Quelle</span>
|
||||
<select id="mp-source">
|
||||
<option value="modrinth">Modrinth</option>
|
||||
<option value="curseforge">CurseForge</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field" style="flex:1">
|
||||
<span>Suche (Modrinth)</span>
|
||||
<span>Suche</span>
|
||||
<input id="mp-query" type="text" placeholder="z. B. Fabulously Optimized, Create …" autocomplete="off" />
|
||||
</label>
|
||||
<label class="field" style="width:140px">
|
||||
@@ -1066,6 +1581,31 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="import-pane-mr" class="import-pane hidden">
|
||||
<label class="field">
|
||||
<span>Datenordner der Modrinth App</span>
|
||||
<div class="path-row">
|
||||
<input id="mr-path" type="text" placeholder="leer lassen = Standardort (%APPDATA%\ModrinthApp)" />
|
||||
<button id="mr-pick" class="btn">Wählen…</button>
|
||||
<button id="mr-scan" class="btn btn-primary">Suchen</button>
|
||||
</div>
|
||||
<small id="mr-status" class="hint">Leer lassen nutzt automatisch den Standardort der Modrinth App.</small>
|
||||
</label>
|
||||
|
||||
<div id="mr-result" class="hidden">
|
||||
<div class="mmc-bar">
|
||||
<label class="checkbox-field" style="margin:0">
|
||||
<input id="mr-all" type="checkbox" checked /> <span>Alle auswählen</span>
|
||||
</label>
|
||||
<label class="checkbox-field" style="margin:0">
|
||||
<input id="mr-copy" type="checkbox" checked /> <span>Spieldaten mitkopieren (Welten, Mods, Konfiguration)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="mr-list" class="mods-list"></div>
|
||||
<p class="hint" style="margin-top:8px">Übernommen werden Name, Gruppe, Symbol, Minecraft-Version, Loader, Arbeitsspeicher, Java-Einstellungen und die Spielzeit. Modrinth-Modpacks bleiben mit ihrem Paket verknüpft und lassen sich in AeroMC weiter aktualisieren.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="import-pane-mmc" class="import-pane hidden">
|
||||
<label class="field">
|
||||
<span>MultiMC- oder Prism-Ordner</span>
|
||||
@@ -1089,18 +1629,76 @@
|
||||
<div id="mmc-list" class="mods-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="import-pane-code" class="import-pane hidden">
|
||||
<p class="import-copy">Ein Freund hat dir eine Instanz geteilt? Gib hier seinen Code ein – AeroMC legt die Instanz an und lädt alle Mods, Ressourcenpakete und Shader in denselben Versionen selbst von Modrinth bzw. CurseForge.</p>
|
||||
<label class="field">
|
||||
<span>Code</span>
|
||||
<div class="path-row">
|
||||
<input id="code-input" class="share-code-input" type="text" placeholder="z. B. K7QD-M2XP" maxlength="12" autocomplete="off" spellcheck="false" />
|
||||
<button id="code-check" class="btn btn-primary">Prüfen</button>
|
||||
</div>
|
||||
</label>
|
||||
<div id="code-status" class="hint" style="margin-top:8px"></div>
|
||||
<div id="code-preview" class="share-preview hidden"></div>
|
||||
</div>
|
||||
|
||||
<div id="import-pane-technic" class="import-pane hidden">
|
||||
<p class="import-copy">Modpack direkt von der Technic-Plattform installieren oder eine bereits vorhandene Technic-Installation importieren.</p>
|
||||
|
||||
<div class="field-row" style="align-items:flex-end">
|
||||
<label class="field" style="flex:1">
|
||||
<span>Technic-Katalog durchsuchen</span>
|
||||
<input id="tp-query" type="text" placeholder="z. B. Hexxit, Tekxit …" autocomplete="off" />
|
||||
</label>
|
||||
<button id="tp-search-btn" class="btn btn-primary">Suchen</button>
|
||||
</div>
|
||||
<div id="tp-status" class="hint" style="margin-top:8px">Gib einen Suchbegriff ein oder lasse das Feld leer für beliebte Packs.</div>
|
||||
<div id="tp-results" class="mods-list" style="margin-top:10px;max-height:220px;overflow:auto"></div>
|
||||
|
||||
<div class="modal-sep" style="margin:16px 0"></div>
|
||||
|
||||
<label class="field">
|
||||
<span>Oder: vorhandenen Technic-Ordner importieren</span>
|
||||
<div class="path-row">
|
||||
<input id="technic-path" type="text" placeholder="z. B. C:\Users\DeinName\AppData\Roaming\.technic" />
|
||||
<button id="technic-pick" class="btn">Wählen…</button>
|
||||
<button id="technic-scan" class="btn btn-primary">Suchen</button>
|
||||
</div>
|
||||
<small id="technic-status" class="hint">Leer lassen nutzt automatisch den Standardpfad unter Windows (%APPDATA%\.technic).</small>
|
||||
</label>
|
||||
|
||||
<div id="technic-result" class="hidden">
|
||||
<div class="mmc-bar">
|
||||
<label class="checkbox-field" style="margin:0">
|
||||
<input id="technic-all" type="checkbox" checked /> <span>Alle auswählen</span>
|
||||
</label>
|
||||
<label class="checkbox-field" style="margin:0">
|
||||
<input id="technic-copy" type="checkbox" checked /> <span>Spieldaten mitkopieren (Mods, Welten, Konfiguration)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="technic-list" class="mods-list"></div>
|
||||
<p class="hint" style="margin-top:8px">Minecraft-Version und Mod-Loader werden nach bestem Wissen aus dem Pack erkannt – bitte nach dem Import kurz prüfen, falls ein Pack ungewöhnlich aufgebaut ist.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<div id="mc-progress" class="hint hidden"></div>
|
||||
<div id="mmc-progress" class="hint hidden"></div>
|
||||
<div id="cf-progress" class="hint hidden"></div>
|
||||
<div id="mr-progress" class="hint hidden"></div>
|
||||
<div id="technic-progress" class="hint hidden"></div>
|
||||
<div id="code-progress" class="hint hidden"></div>
|
||||
<div class="spacer"></div>
|
||||
<button class="btn" data-close>Abbrechen</button>
|
||||
<button id="import-zip-do" class="btn btn-primary">ZIP wählen…</button>
|
||||
<button id="import-modpack-do" class="btn btn-primary hidden">Modpack wählen…</button>
|
||||
<button id="mc-do" class="btn btn-primary hidden" disabled>Übernehmen</button>
|
||||
<button id="cf-do" class="btn btn-primary hidden" disabled>Übernehmen</button>
|
||||
<button id="mr-do" class="btn btn-primary hidden" disabled>Übernehmen</button>
|
||||
<button id="mmc-do" class="btn btn-primary hidden" disabled>Übernehmen</button>
|
||||
<button id="technic-do" class="btn btn-primary hidden" disabled>Übernehmen</button>
|
||||
<button id="code-do" class="btn btn-primary hidden" disabled>Instanz anlegen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1149,6 +1747,29 @@
|
||||
</div>
|
||||
|
||||
<!-- ===================== Kontextmenü (Rechtsklick auf Instanz) ===================== -->
|
||||
<!-- Auswahl-Dialog: z. B. Ziel-Instanz, Freund oder Schnellstart-Taste (siehe waehleAus) -->
|
||||
<div id="modal-wahl" class="modal hidden">
|
||||
<div class="modal-card modal-sm">
|
||||
<div class="modal-head">
|
||||
<h3 id="wahl-titel">Auswählen</h3>
|
||||
<button class="modal-close" data-close>✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p id="wahl-text" class="hint" style="margin-top:0"></p>
|
||||
<div id="wahl-liste" class="wahl-liste"></div>
|
||||
<label id="wahl-eingabe-feld" class="field hidden" style="margin:12px 0 0">
|
||||
<span id="wahl-eingabe-label">Text</span>
|
||||
<input id="wahl-eingabe" type="text" maxlength="200" autocomplete="off" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<div class="spacer"></div>
|
||||
<button class="btn" data-close>Abbrechen</button>
|
||||
<button id="wahl-ok" class="btn btn-primary" disabled>OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="ctx-menu" class="ctx hidden">
|
||||
<button class="ctx-item" data-act="p-edit">Symbol ändern</button>
|
||||
<button class="ctx-item" data-act="p-edit">Umbenennen</button>
|
||||
@@ -1156,12 +1777,14 @@
|
||||
<div class="ctx-sep"></div>
|
||||
<button class="ctx-item ctx-strong" data-act="p-play">Starten</button>
|
||||
<button class="ctx-item" data-act="p-offline">Offline starten</button>
|
||||
<button class="ctx-item" data-act="ctx-schnellstart">Schnellstart-Taste …</button>
|
||||
<div class="ctx-sep"></div>
|
||||
<button class="ctx-item" data-act="p-edit">Instanz bearbeiten</button>
|
||||
<button class="ctx-item" data-act="p-notes">Notizen bearbeiten</button>
|
||||
<button class="ctx-item" data-act="p-mods">Mods anzeigen</button>
|
||||
<button class="ctx-item" data-act="p-worlds">Welten verwalten</button>
|
||||
<button class="ctx-item" data-act="p-screens">Bildschirmfotos verwalten</button>
|
||||
<button class="ctx-item" data-act="p-record">Spiel aufzeichnen</button>
|
||||
<button class="ctx-item" data-act="p-logs">Logdateien ansehen</button>
|
||||
<button class="ctx-item" data-act="p-servers">Server verwalten</button>
|
||||
<div class="ctx-sep"></div>
|
||||
@@ -1181,5 +1804,6 @@
|
||||
<canvas id="skin-canvas" width="420" height="700" style="display:none"></canvas>
|
||||
<script src="vendor/skinview3d.js"></script>
|
||||
<script src="renderer.js"></script>
|
||||
<script src="skineditor.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -21,6 +21,7 @@ const { spawn, spawnSync } = require('child_process');
|
||||
const services = require('./services');
|
||||
const loaders = require('./loaders');
|
||||
const forge = require('./forge');
|
||||
const processwatchdog = require('./processwatchdog');
|
||||
|
||||
const UA = 'AeroMC-Launcher/0.2';
|
||||
const RESOURCES = 'https://resources.download.minecraft.net';
|
||||
@@ -253,20 +254,28 @@ async function prepareAssets(vj, sharedDir, onProgress) {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function pickJava(vj, preferredPath) {
|
||||
const required = (vj.javaVersion && vj.javaVersion.majorVersion) || 8;
|
||||
let preferred = null;
|
||||
if (preferredPath && preferredPath.trim()) {
|
||||
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();
|
||||
// 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 exact = x64.find((j) => j.major === required);
|
||||
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];
|
||||
if (higher) return { path: higher.path, major: higher.major };
|
||||
const any = jvms[0];
|
||||
return any ? { path: any.path, major: any.major, mismatch: required } : null;
|
||||
const fallback = preferred || x64[0] || jvms[0];
|
||||
return fallback ? {
|
||||
path: fallback.path,
|
||||
major: fallback.major,
|
||||
forced: !!fallback.forced,
|
||||
mismatch: required,
|
||||
} : null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -348,6 +357,13 @@ function buildArgs(vj, ctx) {
|
||||
* auth: null (kein Login) ODER { name, uuid, accessToken, userType }
|
||||
* 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) {
|
||||
const emit = (phase, extra) => onProgress && onProgress(Object.assign({ phase }, extra));
|
||||
|
||||
@@ -387,6 +403,14 @@ async function prepareAndLaunch(opts, onProgress) {
|
||||
}
|
||||
}
|
||||
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
|
||||
let effectiveVj = vj;
|
||||
@@ -447,6 +471,37 @@ async function prepareAndLaunch(opts, onProgress) {
|
||||
classpath.unshift(path.join(libDir, rel.replace(/\//g, path.sep)));
|
||||
}
|
||||
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);
|
||||
@@ -478,9 +533,17 @@ async function prepareAndLaunch(opts, onProgress) {
|
||||
// Fenstergröße bzw. maximiert starten (Minecraft-Argumente)
|
||||
if (opts.mcMaximized) args.push('--fullscreen');
|
||||
else if (opts.mcWidth && opts.mcHeight) args.push('--width', String(opts.mcWidth), '--height', String(opts.mcHeight));
|
||||
// Ab 1.20: direkt einem Server beitreten (Quick Play)
|
||||
// Direkt einem Server beitreten: ab 1.20 per Quick Play, davor über die
|
||||
// älteren Startparameter --server/--port (z. B. für "Beitreten" bei Freunden)
|
||||
if (opts.quickPlayServer) {
|
||||
args.push('--quickPlayMultiplayer', String(opts.quickPlayServer));
|
||||
const adresse = String(opts.quickPlayServer).trim();
|
||||
if (vorQuickPlay(opts.versionId)) {
|
||||
const m = /^(.*?)(?::(\d+))?$/.exec(adresse);
|
||||
args.push('--server', m[1]);
|
||||
if (m[2]) args.push('--port', m[2]);
|
||||
} else {
|
||||
args.push('--quickPlayMultiplayer', adresse);
|
||||
}
|
||||
}
|
||||
|
||||
ensureDir(opts.gameDir);
|
||||
@@ -503,14 +566,33 @@ async function prepareAndLaunch(opts, onProgress) {
|
||||
spawnArgs = [...w.slice(1), java.path, ...args];
|
||||
}
|
||||
const child = spawn(cmd, spawnArgs, { cwd: opts.gameDir, detached: false });
|
||||
child.stdout.on('data', (d) => emit('log', { line: d.toString() }));
|
||||
child.stderr.on('data', (d) => emit('log', { line: d.toString() }));
|
||||
const shutdownWatchdog = opts.wrapperCommand && opts.wrapperCommand.trim()
|
||||
? null
|
||||
: processwatchdog.createProcessWatchdog({
|
||||
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) => {
|
||||
const meldeEnde = (code, cleanShutdown) => {
|
||||
if (beendetGemeldet) return;
|
||||
beendetGemeldet = true;
|
||||
emit('exit', { code });
|
||||
emit('exit', { code, cleanShutdown: !!cleanShutdown });
|
||||
// Post-Exit-Befehl
|
||||
if (opts.postExitCommand && opts.postExitCommand.trim()) {
|
||||
emit('log', { line: '[Post-Exit] ' + opts.postExitCommand + '\n' });
|
||||
@@ -521,12 +603,19 @@ async function prepareAndLaunch(opts, onProgress) {
|
||||
} catch { /* egal */ }
|
||||
}
|
||||
};
|
||||
child.on('exit', (code) => meldeEnde(code));
|
||||
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);
|
||||
meldeEnde(-1, false);
|
||||
});
|
||||
|
||||
return { ok: true, pid: child.pid, java };
|
||||
|
||||
@@ -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 };
|
||||
@@ -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,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);
|
||||
@@ -7,6 +7,15 @@
|
||||
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
// Debug-Hilfe: JS-Fehler im Renderer landen im Terminal des Hauptprozesses,
|
||||
// damit man sie auch ohne offene DevTools sieht
|
||||
window.addEventListener('error', (e) => {
|
||||
ipcRenderer.send('debug:rendererError', (e.error && e.error.stack) || e.message);
|
||||
});
|
||||
window.addEventListener('unhandledrejection', (e) => {
|
||||
ipcRenderer.send('debug:rendererError', 'unhandledrejection: ' + ((e.reason && e.reason.stack) || e.reason));
|
||||
});
|
||||
|
||||
contextBridge.exposeInMainWorld('api', {
|
||||
// Einstellungen
|
||||
getSettings: () => ipcRenderer.invoke('settings:get'),
|
||||
@@ -19,6 +28,9 @@ contextBridge.exposeInMainWorld('api', {
|
||||
// Minecraft-Versionen (Mojang)
|
||||
mcVersions: () => ipcRenderer.invoke('mc:versions'),
|
||||
loaderVersions: (loader, mcVersion) => ipcRenderer.invoke('loaders:versions', loader, mcVersion),
|
||||
optifineFind: (mcVersion) => ipcRenderer.invoke('optifine:find', mcVersion),
|
||||
optifineSetup: (id) => ipcRenderer.invoke('optifine:setup', id),
|
||||
optifineConfirm: (id) => ipcRenderer.invoke('optifine:confirm', id),
|
||||
|
||||
// Mods (Modrinth)
|
||||
modSearch: (opts) => ipcRenderer.invoke('mods:search', opts),
|
||||
@@ -27,10 +39,23 @@ contextBridge.exposeInMainWorld('api', {
|
||||
modList: (id, art) => ipcRenderer.invoke('mods:list', id, art),
|
||||
modInstall: (id, project, art) => ipcRenderer.invoke('mods:install', id, project, art),
|
||||
modRemove: (id, filename, art) => ipcRenderer.invoke('mods:remove', id, filename, art),
|
||||
modDependents: (id, filename, art) => ipcRenderer.invoke('mods:dependents', id, filename, art),
|
||||
modAddFile: (id, art) => ipcRenderer.invoke('mods:addFile', id, art),
|
||||
modSetEnabled: (id, filename, enabled, art) => ipcRenderer.invoke('mods:setEnabled', id, filename, enabled, art),
|
||||
systemMemory: () => ipcRenderer.invoke('system:memory'),
|
||||
checkMemory: (minMb, maxMb) => ipcRenderer.invoke('system:checkMemory', minMb, maxMb),
|
||||
crashLatest: (id, seit) => ipcRenderer.invoke('crash:latest', id, seit),
|
||||
crashAnalyze: (id, seit) => ipcRenderer.invoke('crash:analyze', id, seit),
|
||||
crashFix: (id, aktion) => ipcRenderer.invoke('crash:fix', id, aktion),
|
||||
crashRelaunch: (id) => ipcRenderer.invoke('crash:relaunch', id),
|
||||
shareCreate: (id) => ipcRenderer.invoke('share:create', id),
|
||||
shareFetch: (code) => ipcRenderer.invoke('share:fetch', code),
|
||||
shareInstall: (code) => ipcRenderer.invoke('share:install', code),
|
||||
onShareProgress: (cb) => {
|
||||
const l = (_e, p) => cb(p);
|
||||
ipcRenderer.on('share:progress', l);
|
||||
return () => ipcRenderer.removeListener('share:progress', l);
|
||||
},
|
||||
onAutoBackup: (cb) => {
|
||||
const l = (_e, d) => cb(d);
|
||||
ipcRenderer.on('backup:auto', l);
|
||||
@@ -42,6 +67,7 @@ contextBridge.exposeInMainWorld('api', {
|
||||
serversRemove: (id, ip) => ipcRenderer.invoke('servers:remove', id, ip),
|
||||
serversRename: (id, ip, neuerName) => ipcRenderer.invoke('servers:rename', id, ip, neuerName),
|
||||
serversCopyTo: (zielIds, eintrag) => ipcRenderer.invoke('servers:copyTo', zielIds, eintrag),
|
||||
serversPing: (ip) => ipcRenderer.invoke('servers:ping', ip),
|
||||
logsList: (id) => ipcRenderer.invoke('logs:list', id),
|
||||
logsRead: (id, name) => ipcRenderer.invoke('logs:read', id, name),
|
||||
logsOpenFolder: (id) => ipcRenderer.invoke('logs:openFolder', id),
|
||||
@@ -57,6 +83,7 @@ contextBridge.exposeInMainWorld('api', {
|
||||
listInstances: () => ipcRenderer.invoke('instances:list'),
|
||||
getInstance: (id) => ipcRenderer.invoke('instances:get', id),
|
||||
createInstance: (data) => ipcRenderer.invoke('instances:create', data),
|
||||
installPerformancePack: (id) => ipcRenderer.invoke('instances:installPerformancePack', id),
|
||||
updateInstance: (id, patch) => ipcRenderer.invoke('instances:update', id, patch),
|
||||
deleteInstance: (id) => ipcRenderer.invoke('instances:delete', id),
|
||||
duplicateInstance: (id) => ipcRenderer.invoke('instances:duplicate', id),
|
||||
@@ -68,6 +95,9 @@ contextBridge.exposeInMainWorld('api', {
|
||||
resourcepacks: (id) => ipcRenderer.invoke('instance:resourcepacks', id),
|
||||
datapacks: (id) => ipcRenderer.invoke('instance:datapacks', id),
|
||||
deleteContent: (id, sub, name) => ipcRenderer.invoke('instance:deleteContent', id, sub, name),
|
||||
backupWorld: (id, worldName) => ipcRenderer.invoke('world:backup', id, worldName),
|
||||
worldCopyTo: (id, worldName, zielId) => ipcRenderer.invoke('world:copyTo', id, worldName, zielId),
|
||||
friendsSendScreenshot: (otherUuid, instanceId, datei, text) => ipcRenderer.invoke('friends:sendScreenshot', otherUuid, instanceId, datei, text),
|
||||
openSubdir: (id, sub) => ipcRenderer.invoke('instance:openSubdir', id, sub),
|
||||
screenshots: (id) => ipcRenderer.invoke('instance:screenshots', id),
|
||||
screenshotData: (id, name) => ipcRenderer.invoke('instance:screenshotData', id, name),
|
||||
@@ -83,16 +113,24 @@ contextBridge.exposeInMainWorld('api', {
|
||||
return () => ipcRenderer.removeListener('backup:progress', l);
|
||||
},
|
||||
importInstance: () => ipcRenderer.invoke('instances:import'),
|
||||
createShortcut: (id) => ipcRenderer.invoke('instances:shortcut', id),
|
||||
// Start über eine Desktop-Verknüpfung ("--launch <id>")
|
||||
createShortcut: (id, customName) => ipcRenderer.invoke('instances:shortcut', id, customName),
|
||||
serversShortcut: (id, ip, name, customName) => ipcRenderer.invoke('servers:shortcut', id, ip, name, customName),
|
||||
// Start über eine Desktop-Verknüpfung ("--launch=<id>", optional "--server=<ip>")
|
||||
onLaunchRequest: (cb) => {
|
||||
const l = (_e, id) => cb(id);
|
||||
const l = (_e, payload) => cb(payload);
|
||||
ipcRenderer.on('instance:launch-request', l);
|
||||
return () => ipcRenderer.removeListener('instance:launch-request', l);
|
||||
},
|
||||
// Signalisiert dem Hauptprozess, dass onLaunchRequest jetzt registriert ist –
|
||||
// verhindert, dass ein Start-Wunsch aus einer Verknüpfung verloren geht,
|
||||
// weil er ankommt, bevor die Oberfläche fertig eingerichtet ist
|
||||
notifyReady: () => ipcRenderer.send('renderer:ready'),
|
||||
importModpack: () => ipcRenderer.invoke('modpack:import'),
|
||||
// Datei per Drag&Drop aufs Fenster gezogen (.jar/.mrpack/.zip)
|
||||
filesDrop: (filePath, targetId, targetArt) => ipcRenderer.invoke('files:drop', filePath, targetId, targetArt),
|
||||
modpackSearch: (opts) => ipcRenderer.invoke('modpack:search', opts),
|
||||
modpackInstallFromModrinth: (project) => ipcRenderer.invoke('modpack:installFromModrinth', project),
|
||||
modpackInstallFromCurseForge: (project) => ipcRenderer.invoke('modpack:installFromCurseForge', project),
|
||||
modpackCheck: (id) => ipcRenderer.invoke('modpack:check', id),
|
||||
modpackUpdate: (id, versionId) => ipcRenderer.invoke('modpack:update', id, versionId),
|
||||
onLaunchProgress: (cb) => {
|
||||
@@ -116,6 +154,11 @@ contextBridge.exposeInMainWorld('api', {
|
||||
skinChoose: () => ipcRenderer.invoke('skin:choose'),
|
||||
skinApply: (pfad, variante) => ipcRenderer.invoke('skin:apply', pfad, variante),
|
||||
skinReset: () => ipcRenderer.invoke('skin:reset'),
|
||||
skinCurrentData: () => ipcRenderer.invoke('skin:currentData'),
|
||||
skinModelFor: (uuid) => ipcRenderer.invoke('skin:modelFor', uuid),
|
||||
skinReadData: (pfad) => ipcRenderer.invoke('skin:readData', pfad),
|
||||
skinSaveData: (base64) => ipcRenderer.invoke('skin:saveData', base64),
|
||||
skinApplyData: (base64, variante) => ipcRenderer.invoke('skin:applyData', base64, variante),
|
||||
authRemove: (id) => ipcRenderer.invoke('auth:remove', id),
|
||||
authLogin: () => ipcRenderer.invoke('auth:login'),
|
||||
authLogout: () => ipcRenderer.invoke('auth:logout'),
|
||||
@@ -154,6 +197,14 @@ contextBridge.exposeInMainWorld('api', {
|
||||
return () => ipcRenderer.removeListener('mmc:progress', l);
|
||||
},
|
||||
cfPickFolder: () => ipcRenderer.invoke('cf:pickFolder'),
|
||||
mrPickFolder: () => ipcRenderer.invoke('mr:pickFolder'),
|
||||
mrScan: (folder) => ipcRenderer.invoke('mr:scan', folder),
|
||||
mrImport: (entries, copyData) => ipcRenderer.invoke('mr:import', entries, copyData),
|
||||
onMrProgress: (cb) => {
|
||||
const l = (_e, p) => cb(p);
|
||||
ipcRenderer.on('mr:progress', l);
|
||||
return () => ipcRenderer.removeListener('mr:progress', l);
|
||||
},
|
||||
cfScan: (folder) => ipcRenderer.invoke('cf:scan', folder),
|
||||
cfImport: (entries, copyData) => ipcRenderer.invoke('cf:import', entries, copyData),
|
||||
onCfProgress: (cb) => {
|
||||
@@ -169,6 +220,21 @@ contextBridge.exposeInMainWorld('api', {
|
||||
ipcRenderer.on('mc:progress', l);
|
||||
return () => ipcRenderer.removeListener('mc:progress', l);
|
||||
},
|
||||
technicPickFolder: () => ipcRenderer.invoke('technic:pickFolder'),
|
||||
technicScan: (folder) => ipcRenderer.invoke('technic:scan', folder),
|
||||
technicImport: (entries, copyData) => ipcRenderer.invoke('technic:import', entries, copyData),
|
||||
onTechnicProgress: (cb) => {
|
||||
const l = (_e, d) => cb(d);
|
||||
ipcRenderer.on('technic:progress', l);
|
||||
return () => ipcRenderer.removeListener('technic:progress', l);
|
||||
},
|
||||
technicPlatformSearch: (query) => ipcRenderer.invoke('technicPlatform:search', query),
|
||||
technicPlatformInstall: (slug) => ipcRenderer.invoke('technicPlatform:install', slug),
|
||||
onTechnicPlatformProgress: (cb) => {
|
||||
const l = (_e, d) => cb(d);
|
||||
ipcRenderer.on('technicPlatform:progress', l);
|
||||
return () => ipcRenderer.removeListener('technicPlatform:progress', l);
|
||||
},
|
||||
|
||||
// Bedrock (Windows-Store-Ausgabe)
|
||||
bedrockDetect: () => ipcRenderer.invoke('bedrock:detect'),
|
||||
@@ -186,12 +252,21 @@ contextBridge.exposeInMainWorld('api', {
|
||||
|
||||
// Updates
|
||||
updateCheck: () => ipcRenderer.invoke('update:check'),
|
||||
updateLastCheck: () => ipcRenderer.invoke('update:lastCheck'),
|
||||
updateInstall: (asset) => ipcRenderer.invoke('update:install', asset),
|
||||
onUpdateAvailable: (cb) => {
|
||||
const l = (_e, d) => cb(d);
|
||||
ipcRenderer.on('update:available', l);
|
||||
return () => ipcRenderer.removeListener('update:available', l);
|
||||
},
|
||||
// Ergebnis des automatischen Checks beim Start – auch wenn kein Update
|
||||
// verfügbar ist (im Unterschied zu onUpdateAvailable, das nur bei
|
||||
// verfügbarem Update feuert)
|
||||
onUpdateChecked: (cb) => {
|
||||
const l = (_e, d) => cb(d);
|
||||
ipcRenderer.on('update:checked', l);
|
||||
return () => ipcRenderer.removeListener('update:checked', l);
|
||||
},
|
||||
onUpdateProgress: (cb) => {
|
||||
const l = (_e, d) => cb(d);
|
||||
ipcRenderer.on('update:progress', l);
|
||||
@@ -202,4 +277,112 @@ contextBridge.exposeInMainWorld('api', {
|
||||
pickFolder: (title) => ipcRenderer.invoke('dialog:pickFolder', title),
|
||||
pickJava: () => ipcRenderer.invoke('dialog:pickJava'),
|
||||
appInfo: () => ipcRenderer.invoke('app:info'),
|
||||
appLicense: () => ipcRenderer.invoke('app:license'),
|
||||
|
||||
// Einstellungen/Konsole als eigenes, frei verschiebbares Fenster
|
||||
popoutOpen: (modalId, params) => ipcRenderer.send('popout:open', modalId, params),
|
||||
popoutHide: () => ipcRenderer.send('popout:hide'),
|
||||
setTitleBarDesign: (design) => ipcRenderer.send('window:titleBar', design),
|
||||
onPopoutReopen: (cb) => {
|
||||
const l = (_e, params) => cb(params);
|
||||
ipcRenderer.on('popout:reopen', l);
|
||||
return () => ipcRenderer.removeListener('popout:reopen', l);
|
||||
},
|
||||
onSettingsChanged: (cb) => {
|
||||
const l = (_e, s) => cb(s);
|
||||
ipcRenderer.on('settings:changed', l);
|
||||
return () => ipcRenderer.removeListener('settings:changed', l);
|
||||
},
|
||||
onInstancesChanged: (cb) => {
|
||||
const l = () => cb();
|
||||
ipcRenderer.on('instances:changed', l);
|
||||
return () => ipcRenderer.removeListener('instances:changed', l);
|
||||
},
|
||||
onSkinChanged: (cb) => {
|
||||
const l = (_e, info) => cb(info || {});
|
||||
ipcRenderer.on('skin:changed', l);
|
||||
return () => ipcRenderer.removeListener('skin:changed', l);
|
||||
},
|
||||
updateGetPending: () => ipcRenderer.invoke('update:getPending'),
|
||||
|
||||
// Spielaufzeichnung
|
||||
recordingListWindows: () => ipcRenderer.invoke('recording:listWindows'),
|
||||
recordingGetAudioSourceId: () => ipcRenderer.invoke('recording:getAudioSourceId'),
|
||||
recordingSave: (instanceName, buffer, serverLabel, fps) => ipcRenderer.invoke('recording:save', instanceName, buffer, serverLabel, fps),
|
||||
recordingOpenFolder: (instanceName) => ipcRenderer.invoke('recording:openFolder', instanceName),
|
||||
recordingList: (instanceName) => ipcRenderer.invoke('recording:list', instanceName),
|
||||
recordingThumbnail: (instanceName, dateiname) => ipcRenderer.invoke('recording:thumbnail', instanceName, dateiname),
|
||||
recordingDelete: (instanceName, dateiname) => ipcRenderer.invoke('recording:delete', instanceName, dateiname),
|
||||
recordingOpen: (instanceName, dateiname) => ipcRenderer.invoke('recording:open', instanceName, dateiname),
|
||||
|
||||
// Freundesystem
|
||||
friendsEnabled: () => ipcRenderer.invoke('friends:enabled'),
|
||||
friendsList: () => ipcRenderer.invoke('friends:list'),
|
||||
friendsRequests: () => ipcRenderer.invoke('friends:requests'),
|
||||
friendsAdd: (targetUsername) => ipcRenderer.invoke('friends:add', targetUsername),
|
||||
friendsRespond: (otherUuid, accept) => ipcRenderer.invoke('friends:respond', otherUuid, accept),
|
||||
friendsRemove: (otherUuid) => ipcRenderer.invoke('friends:remove', otherUuid),
|
||||
friendsSentRequests: () => ipcRenderer.invoke('friends:sentRequests'),
|
||||
friendsCancel: (otherUuid) => ipcRenderer.invoke('friends:cancel', otherUuid),
|
||||
friendsSetNickname: (uuid, nickname) => ipcRenderer.invoke('friends:setNickname', uuid, nickname),
|
||||
friendsRequestCount: () => ipcRenderer.invoke('friends:requestCount'),
|
||||
friendsMessages: (otherUuid) => ipcRenderer.invoke('friends:messages', otherUuid),
|
||||
friendsSendMessage: (otherUuid, text) => ipcRenderer.invoke('friends:sendMessage', otherUuid, text),
|
||||
friendsUnreadByFriend: () => ipcRenderer.invoke('friends:unreadByFriend'),
|
||||
onFriendsMessage: (cb) => {
|
||||
const l = (_e, n) => cb(n);
|
||||
ipcRenderer.on('friends:message', l);
|
||||
return () => ipcRenderer.removeListener('friends:message', l);
|
||||
},
|
||||
crashReport: (id, seit, wahl) => ipcRenderer.invoke('crash:report', id, seit, wahl),
|
||||
clipSetActive: (active, instanceId) => ipcRenderer.invoke('clip:setActive', active, instanceId),
|
||||
clipSave: (data) => ipcRenderer.invoke('clip:save', data),
|
||||
onClipHotkey: (cb) => {
|
||||
const l = () => cb();
|
||||
ipcRenderer.on('clip:hotkey', l);
|
||||
return () => ipcRenderer.removeListener('clip:hotkey', l);
|
||||
},
|
||||
onUiToast: (cb) => {
|
||||
const l = (_e, t) => cb(t);
|
||||
ipcRenderer.on('ui:toast', l);
|
||||
return () => ipcRenderer.removeListener('ui:toast', l);
|
||||
},
|
||||
friendsJoin: (server, version) => ipcRenderer.invoke('friends:join', server, version),
|
||||
friendsJoinNew: (server, version) => ipcRenderer.invoke('friends:joinNew', server, version),
|
||||
friendsMyServer: () => ipcRenderer.invoke('friends:myServer'),
|
||||
friendsInvite: (targetUuid) => ipcRenderer.invoke('friends:invite', targetUuid),
|
||||
friendsInvites: () => ipcRenderer.invoke('friends:invites'),
|
||||
friendsDismissInvite: (id) => ipcRenderer.invoke('friends:dismissInvite', id),
|
||||
onFriendsInvite: (cb) => {
|
||||
const l = (_e, einladung) => cb(einladung);
|
||||
ipcRenderer.on('friends:invite', l);
|
||||
return () => ipcRenderer.removeListener('friends:invite', l);
|
||||
},
|
||||
onFriendsRequestCount: (cb) => {
|
||||
const l = (_e, zahl) => cb(zahl);
|
||||
ipcRenderer.on('friends:requestCount', l);
|
||||
return () => ipcRenderer.removeListener('friends:requestCount', l);
|
||||
},
|
||||
recordingGetServer: (instanceId) => ipcRenderer.invoke('recording:getServer', instanceId),
|
||||
onRecordingServerDetected: (cb) => {
|
||||
const l = (_e, payload) => cb(payload);
|
||||
ipcRenderer.on('recording:serverDetected', l);
|
||||
return () => ipcRenderer.removeListener('recording:serverDetected', l);
|
||||
},
|
||||
recordingGetPid: (instanceId) => ipcRenderer.invoke('recording:getPid', instanceId),
|
||||
recordingSetCaptureTarget: (sourceId) => ipcRenderer.invoke('recording:setCaptureTarget', sourceId),
|
||||
recordingGetWindowBounds: (instanceId, fensterTitel) => ipcRenderer.invoke('recording:getWindowBounds', instanceId, fensterTitel),
|
||||
recordingStartGameAudio: (pid) => ipcRenderer.invoke('recording:startGameAudio', pid),
|
||||
recordingStopGameAudio: () => ipcRenderer.invoke('recording:stopGameAudio'),
|
||||
onGameAudioChunk: (cb) => {
|
||||
const l = (_e, chunk) => cb(chunk);
|
||||
ipcRenderer.on('recording:gameAudioChunk', l);
|
||||
return () => ipcRenderer.removeListener('recording:gameAudioChunk', l);
|
||||
},
|
||||
recordingSetGuard: (active) => ipcRenderer.send('recording:guard', active),
|
||||
onRecordCloseAttempt: (cb) => {
|
||||
const l = () => cb();
|
||||
ipcRenderer.on('record:closeAttempt', l);
|
||||
return () => ipcRenderer.removeListener('record:closeAttempt', l);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
'use strict';
|
||||
|
||||
const DEFAULT_GRACE_MS = 15000;
|
||||
|
||||
const CLEAN_SHUTDOWN = /\[(?:Render thread|Client thread)\/INFO\](?:\s+\([^)]*\))?:\s*Stopping!\s*$/;
|
||||
const FATAL_OUTPUT = [
|
||||
/---- Minecraft Crash Report ----/i,
|
||||
/\b(?:Encountered an unexpected exception|Unreported exception thrown|Reported exception thrown)\b/i,
|
||||
/\b(?:This crash report has been saved to|Saving crash report to)\b/i,
|
||||
/\bA fatal error has been detected by the Java Runtime Environment\b/i,
|
||||
/Exception in thread "(?:Render thread|Client thread|main)"/i,
|
||||
/\[(?:Render thread|Client thread|main)\/FATAL\]/i,
|
||||
];
|
||||
|
||||
function createProcessWatchdog(options) {
|
||||
const graceMs = options.graceMs || DEFAULT_GRACE_MS;
|
||||
const schedule = options.setTimer || setTimeout;
|
||||
const cancel = options.clearTimer || clearTimeout;
|
||||
const buffers = { stdout: '', stderr: '' };
|
||||
let timer = null;
|
||||
let armed = false;
|
||||
let crashed = false;
|
||||
let ended = false;
|
||||
let forced = false;
|
||||
// bleibt true, sobald einmal ein regulärer Shutdown ("Stopping!") gesehen
|
||||
// wurde – im Gegensatz zu `armed` wird das NICHT durch markCrash()
|
||||
// zurückgesetzt, damit ein später vom internen Watchdog erzwungener Halt
|
||||
// weiterhin als "war ein regulärer Shutdown" erkennbar bleibt
|
||||
let sawCleanShutdown = false;
|
||||
|
||||
function clear() {
|
||||
if (timer !== null) cancel(timer);
|
||||
timer = null;
|
||||
armed = false;
|
||||
}
|
||||
|
||||
function markCrash() {
|
||||
crashed = true;
|
||||
clear();
|
||||
}
|
||||
|
||||
function scheduleKill() {
|
||||
if (timer !== null) cancel(timer);
|
||||
timer = schedule(() => {
|
||||
timer = null;
|
||||
if (ended || crashed || !armed || !options.isRunning()) return;
|
||||
try {
|
||||
forced = options.kill() === true;
|
||||
} catch {
|
||||
forced = false;
|
||||
}
|
||||
if (forced && options.onForce) options.onForce(graceMs);
|
||||
else if (!forced && options.isRunning() && options.onError) options.onError();
|
||||
}, graceMs);
|
||||
if (timer && typeof timer.unref === 'function') timer.unref();
|
||||
}
|
||||
|
||||
function inspectLine(line) {
|
||||
const isCleanShutdown = CLEAN_SHUTDOWN.test(line);
|
||||
if (isCleanShutdown) sawCleanShutdown = true;
|
||||
if (FATAL_OUTPUT.some((pattern) => pattern.test(line))) {
|
||||
markCrash();
|
||||
return;
|
||||
}
|
||||
if (ended || crashed || armed || !isCleanShutdown) return;
|
||||
|
||||
armed = true;
|
||||
scheduleKill();
|
||||
}
|
||||
|
||||
function feed(chunk, stream) {
|
||||
if (ended) return;
|
||||
const key = stream === 'stderr' ? 'stderr' : 'stdout';
|
||||
const text = buffers[key] + String(chunk || '');
|
||||
const lines = text.split(/\r?\n/);
|
||||
buffers[key] = lines.pop() || '';
|
||||
for (const line of lines) inspectLine(line);
|
||||
if (armed && !crashed && !ended && lines.length) scheduleKill();
|
||||
}
|
||||
|
||||
function end() {
|
||||
ended = true;
|
||||
clear();
|
||||
}
|
||||
|
||||
return {
|
||||
feed,
|
||||
end,
|
||||
get armed() { return armed; },
|
||||
get crashed() { return crashed; },
|
||||
get forced() { return forced; },
|
||||
get sawCleanShutdown() { return sawCleanShutdown; },
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_GRACE_MS,
|
||||
CLEAN_SHUTDOWN,
|
||||
FATAL_OUTPUT,
|
||||
createProcessWatchdog,
|
||||
};
|
||||
@@ -236,6 +236,7 @@ const INHALTSARTEN = {
|
||||
mod: { projectType: 'mod', ordner: 'mods', label: 'Mods', loaderNoetig: true },
|
||||
resourcepack: { projectType: 'resourcepack', ordner: 'resourcepacks', label: 'Ressourcenpakete', loaderNoetig: false },
|
||||
shader: { projectType: 'shader', ordner: 'shaderpacks', label: 'Shader', loaderNoetig: false },
|
||||
datapack: { projectType: 'datapack', ordner: 'datapacks', label: 'Datenpakete', loaderNoetig: false },
|
||||
modpack: { projectType: 'modpack', ordner: null, label: 'Modpacks', loaderNoetig: false },
|
||||
};
|
||||
|
||||
@@ -282,8 +283,30 @@ async function modrinthSearch({ query = '', gameVersion = '', loader = '', categ
|
||||
}
|
||||
|
||||
// findet die passende Datei-Version für Spielversion + Loader
|
||||
async function modrinthResolveFile({ projectId, gameVersion, loader, art = 'mod' }) {
|
||||
async function modrinthResolveFile({ projectId, gameVersion, loader, art = 'mod', versionId = '' }) {
|
||||
const typ = inhaltsArt(art);
|
||||
// bestimmte Version gewünscht (z. B. geteilte Instanz) – sonst neueste passende
|
||||
if (versionId) {
|
||||
try {
|
||||
const res = await fetch(MODRINTH + '/version/' + encodeURIComponent(versionId), { headers: { 'User-Agent': UA } });
|
||||
if (res.ok) {
|
||||
const v = await res.json();
|
||||
const passt = !gameVersion || (v.game_versions || []).includes(gameVersion);
|
||||
const file = (v.files || []).find((f) => f.primary) || (v.files || [])[0];
|
||||
if (passt && file && v.project_id === projectId) {
|
||||
return {
|
||||
versionId: v.id,
|
||||
versionNumber: v.version_number,
|
||||
filename: file.filename,
|
||||
url: file.url,
|
||||
sha1: file.hashes && file.hashes.sha1,
|
||||
size: file.size,
|
||||
dependencies: v.dependencies || [],
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch { /* dann eben die neueste passende */ }
|
||||
}
|
||||
const url = new URL(MODRINTH + '/project/' + projectId + '/version');
|
||||
if (gameVersion) url.searchParams.set('game_versions', JSON.stringify([gameVersion]));
|
||||
// Nur Mods werden nach Loader eingeordnet; bei Ressourcenpaketen und
|
||||
@@ -350,9 +373,9 @@ async function downloadFile(url, destPath, extraHeaders) {
|
||||
}
|
||||
|
||||
// lädt eine Mod in <modsDir>; gibt Metadaten für die Instanz zurück
|
||||
async function modrinthInstall({ projectId, title, gameVersion, loader, modsDir, art = 'mod' }) {
|
||||
async function modrinthInstall({ projectId, title, gameVersion, loader, modsDir, art = 'mod', versionId = '' }) {
|
||||
const typ = inhaltsArt(art);
|
||||
const file = await modrinthResolveFile({ projectId, gameVersion, loader, art });
|
||||
const file = await modrinthResolveFile({ projectId, gameVersion, loader, art, versionId });
|
||||
if (!file) {
|
||||
return { ok: false, reason: 'no-match',
|
||||
message: typ.loaderNoetig
|
||||
@@ -515,7 +538,9 @@ function forgeCdnUrl(fileId, fileName) {
|
||||
}
|
||||
|
||||
const CF_LOADER = { forge: 1, fabric: 4, quilt: 5, neoforge: 6 };
|
||||
const CF_CLASS = { mod: 6, resourcepack: 12, shader: 6552 };
|
||||
// classId je Inhaltsart bei gameId 432 (Minecraft). modpack=4471 ist die auf
|
||||
// CurseForge selbst verwendete Kategorie-ID (siehe curseforge.com/minecraft/modpacks).
|
||||
const CF_CLASS = { mod: 6, resourcepack: 12, shader: 6552, modpack: 4471 };
|
||||
|
||||
function cfLoaderType(loader) {
|
||||
return CF_LOADER[String(loader || '').toLowerCase()] || 0;
|
||||
@@ -598,7 +623,12 @@ async function curseforgeSearch(zugang, {
|
||||
+ 'Die Weiterleitung reicht für Downloads, nicht für den Katalog.',
|
||||
};
|
||||
}
|
||||
const classId = CF_CLASS[art] || CF_CLASS.mod;
|
||||
if (!CF_CLASS[art]) {
|
||||
// Kein zugeordneter CurseForge-Klassencode für diese Inhaltsart -> lieber gar
|
||||
// keine Treffer als falsch einsortierte (z. B. Mods unter "Datenpakete").
|
||||
return { total: 0, hits: [], error: 'CurseForge unterstützt diese Inhaltsart hier nicht – bitte Modrinth nutzen.' };
|
||||
}
|
||||
const classId = CF_CLASS[art];
|
||||
const url = new URL(CURSEFORGE + '/mods/search');
|
||||
url.searchParams.set('gameId', '432');
|
||||
url.searchParams.set('classId', String(classId));
|
||||
@@ -713,6 +743,12 @@ async function curseforgePickFile(zugang, modId, gameVersion, loader, art) {
|
||||
};
|
||||
}
|
||||
|
||||
// Beste Modpack-Datei eines CurseForge-Projekts auflösen (für den Katalog-Import,
|
||||
// analog zu modrinthResolveModpack).
|
||||
async function curseforgeResolveModpackFile(zugang, modId, gameVersion) {
|
||||
return curseforgePickFile(zugang, modId, gameVersion, '', 'modpack');
|
||||
}
|
||||
|
||||
async function curseforgeInstall({ zugang, projectId, title, gameVersion, loader, modsDir, art = 'mod' }) {
|
||||
if (!zugang) return { ok: false, message: 'Kein CurseForge-Zugang eingerichtet.' };
|
||||
const rawId = String(projectId || '').replace(/^cf-/, '');
|
||||
@@ -814,4 +850,5 @@ module.exports = {
|
||||
modrinthSearch, modrinthResolveFile, modrinthResolveModpack, modrinthInstall, modrinthProjectTitle,
|
||||
modrinthFindUpdates, downloadFile, downloadBuffer, inhaltsArt, INHALTSARTEN,
|
||||
curseforgeResolveFiles, curseforgeSearch, curseforgeInstall, curseforgeTestKey, forgeCdnUrl, curseforgeZugang,
|
||||
curseforgeResolveModpackFile,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,983 @@
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* skineditor.js – Pixel-Editor für Minecraft-Skins
|
||||
* -------------------------------------------------
|
||||
* Ein Skin ist eine einzige 64×64-Textur. Jedes Körperteil hat darin zwei
|
||||
* Bereiche: die Grundschicht (liegt direkt auf der Figur) und die 2. Schicht
|
||||
* (eine etwas größere Hülle darüber – Haare, Jacke, Ärmel …). Die 2. Schicht
|
||||
* darf durchsichtig sein und lässt die Figur dadurch plastischer wirken.
|
||||
*
|
||||
* Gemalt wird wahlweise auf der aufgeklappten Vorlage (2D) oder direkt auf der
|
||||
* drehbaren Figur (3D). Beide Ansichten sind immer da – die gerade nicht
|
||||
* gewählte steht klein rechts und ist ebenfalls bemalbar.
|
||||
*
|
||||
* Läuft im Popup-Fenster "skineditor" und nutzt die Helfer aus renderer.js
|
||||
* (el, show, toast, askConfirm, POPOUT, SKIN_POSES).
|
||||
*/
|
||||
|
||||
(() => {
|
||||
const N = 64;
|
||||
|
||||
// Lage der Körperteile in der Textur. w/h/d = Breite/Höhe/Tiefe in Pixeln,
|
||||
// base/over = linke obere Ecke des Bereichs der Grund- bzw. 2. Schicht.
|
||||
const TEILE = [
|
||||
{ key: 'head', name: 'Kopf', w: 8, h: 8, d: 8, base: [0, 0], over: [32, 0], spiegel: 'head' },
|
||||
{ key: 'body', name: 'Körper', w: 8, h: 12, d: 4, base: [16, 16], over: [16, 32], spiegel: 'body' },
|
||||
{ key: 'rightArm', name: 'Rechter Arm', w: 4, h: 12, d: 4, base: [40, 16], over: [40, 32], spiegel: 'leftArm', arm: true },
|
||||
{ key: 'leftArm', name: 'Linker Arm', w: 4, h: 12, d: 4, base: [32, 48], over: [48, 48], spiegel: 'rightArm', arm: true },
|
||||
{ key: 'rightLeg', name: 'Rechtes Bein', w: 4, h: 12, d: 4, base: [0, 16], over: [0, 32], spiegel: 'leftLeg' },
|
||||
{ key: 'leftLeg', name: 'Linkes Bein', w: 4, h: 12, d: 4, base: [16, 48], over: [0, 48], spiegel: 'rightLeg' },
|
||||
];
|
||||
const TEIL = Object.fromEntries(TEILE.map((t) => [t.key, t]));
|
||||
const SEITE_NAME = { oben: 'oben', unten: 'unten', rechts: 'rechte Seite', vorne: 'vorne', links: 'linke Seite', hinten: 'hinten' };
|
||||
const SEITE_GEGENSTUECK = { rechts: 'links', links: 'rechts' };
|
||||
const SCHICHT_NAME = ['Grundschicht', '2. Schicht'];
|
||||
|
||||
// Rechtecke [x, y, breite, höhe] der sechs Seiten eines Teils (Minecraft-Aufteilung:
|
||||
// oben/unten in der ersten Reihe, darunter rechts | vorne | links | hinten)
|
||||
function seiten(teil, schicht, slim) {
|
||||
const w = teil.arm && slim ? 3 : teil.w;
|
||||
const { h, d } = teil;
|
||||
const [u, v] = schicht ? teil.over : teil.base;
|
||||
return {
|
||||
oben: [u + d, v, w, d],
|
||||
unten: [u + d + w, v, w, d],
|
||||
rechts: [u, v + d, d, h],
|
||||
vorne: [u + d, v + d, w, h],
|
||||
links: [u + d + w, v + d, d, h],
|
||||
hinten: [u + 2 * d + w, v + d, w, h],
|
||||
};
|
||||
}
|
||||
|
||||
// Für jede Armform: Liste aller Flächen und eine Pixel→Fläche-Karte
|
||||
const AUFTEILUNG = [false, true].map((slim) => {
|
||||
const flaechen = [];
|
||||
const karte = new Array(N * N).fill(null);
|
||||
for (const teil of TEILE) {
|
||||
for (const schicht of [0, 1]) {
|
||||
for (const [seite, r] of Object.entries(seiten(teil, schicht, slim))) {
|
||||
const f = { teil, schicht, seite, x: r[0], y: r[1], w: r[2], h: r[3] };
|
||||
flaechen.push(f);
|
||||
for (let yy = 0; yy < f.h; yy++) for (let xx = 0; xx < f.w; xx++) karte[(f.y + yy) * N + f.x + xx] = f;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { flaechen, karte };
|
||||
});
|
||||
|
||||
// ---------- Zustand ----------
|
||||
const ed = {
|
||||
daten: new Uint8ClampedArray(N * N * 4),
|
||||
slim: false,
|
||||
werkzeug: 'stift',
|
||||
vorPipette: 'stift',
|
||||
farbe: '#3a6ea5',
|
||||
schicht: 0,
|
||||
zeigeAussen: true,
|
||||
durchscheinen: true,
|
||||
spiegeln: false,
|
||||
modus: '2d',
|
||||
undo: [],
|
||||
redo: [],
|
||||
geaendert: false,
|
||||
hover: null,
|
||||
strich: null, // { snap, letzter: [x,y] | null, letzterBild: [cx,cy] | null, geaendert }
|
||||
viewer: null,
|
||||
bereit: false,
|
||||
zeichnenGeplant: false,
|
||||
};
|
||||
const textur = document.createElement('canvas');
|
||||
textur.width = textur.height = N;
|
||||
const texturCtx = textur.getContext('2d', { willReadFrequently: true });
|
||||
// Grundschicht als Schatten unter die 2. Schicht legen (Option "durchscheinen")
|
||||
const schatten = document.createElement('canvas');
|
||||
schatten.width = schatten.height = N;
|
||||
const schattenCtx = schatten.getContext('2d');
|
||||
// Hintergrund (Karomuster in den Flächen) je Armform einmal vorberechnet
|
||||
const hintergrund = [null, null];
|
||||
|
||||
const aufteilung = () => AUFTEILUNG[ed.slim ? 1 : 0];
|
||||
const flaecheBei = (x, y) => (x < 0 || y < 0 || x >= N || y >= N ? null : aufteilung().karte[y * N + x]);
|
||||
const dunkel = () => document.documentElement.getAttribute('data-theme') === 'dark';
|
||||
|
||||
function hexZuRgb(hex) {
|
||||
const m = /^#?([0-9a-f]{6})$/i.exec(hex || '');
|
||||
if (!m) return [0, 0, 0];
|
||||
const n = parseInt(m[1], 16);
|
||||
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
|
||||
}
|
||||
const rgbZuHex = (r, g, b) => '#' + [r, g, b].map((v) => v.toString(16).padStart(2, '0')).join('');
|
||||
|
||||
// Pixel auf der gespiegelten Körperseite (rechter ↔ linker Arm/Bein,
|
||||
// rechte ↔ linke Seite, waagerecht umgedreht)
|
||||
function spiegelPixel(x, y) {
|
||||
const f = flaecheBei(x, y);
|
||||
if (!f) return null;
|
||||
const partner = TEIL[f.teil.spiegel];
|
||||
const seite = SEITE_GEGENSTUECK[f.seite] || f.seite;
|
||||
const r = seiten(partner, f.schicht, ed.slim)[seite];
|
||||
const fx = x - f.x;
|
||||
const fy = y - f.y;
|
||||
return [r[0] + (r[2] - 1 - fx), r[1] + fy];
|
||||
}
|
||||
|
||||
// ---------- Daten ändern ----------
|
||||
function setzePixel(x, y, rgba) {
|
||||
const i = (y * N + x) * 4;
|
||||
const d = ed.daten;
|
||||
if (d[i] === rgba[0] && d[i + 1] === rgba[1] && d[i + 2] === rgba[2] && d[i + 3] === rgba[3]) return false;
|
||||
d[i] = rgba[0]; d[i + 1] = rgba[1]; d[i + 2] = rgba[2]; d[i + 3] = rgba[3];
|
||||
return true;
|
||||
}
|
||||
|
||||
function zielPixel(x, y) {
|
||||
const liste = [[x, y]];
|
||||
if (ed.spiegeln) {
|
||||
const s = spiegelPixel(x, y);
|
||||
if (s && (s[0] !== x || s[1] !== y)) liste.push(s);
|
||||
}
|
||||
return liste;
|
||||
}
|
||||
|
||||
function malFarbe() {
|
||||
if (ed.werkzeug === 'radierer') return [0, 0, 0, 0];
|
||||
return [...hexZuRgb(ed.farbe), 255];
|
||||
}
|
||||
|
||||
// Einen Pixel mit dem aktuellen Werkzeug bearbeiten (Stift/Radierer)
|
||||
function malePixel(x, y) {
|
||||
const f = flaecheBei(x, y);
|
||||
if (!f || f.schicht !== ed.schicht) return false;
|
||||
let geaendert = false;
|
||||
const rgba = malFarbe();
|
||||
for (const [px, py] of zielPixel(x, y)) if (setzePixel(px, py, rgba)) geaendert = true;
|
||||
return geaendert;
|
||||
}
|
||||
|
||||
// Füllen: zusammenhängende gleichfarbige Pixel innerhalb derselben Fläche
|
||||
function fuelle(x, y) {
|
||||
const f = flaecheBei(x, y);
|
||||
if (!f || f.schicht !== ed.schicht) return false;
|
||||
const d = ed.daten;
|
||||
const i0 = (y * N + x) * 4;
|
||||
const alt = [d[i0], d[i0 + 1], d[i0 + 2], d[i0 + 3]];
|
||||
const neu = malFarbe();
|
||||
if (alt.every((v, k) => v === neu[k])) return false;
|
||||
const gleich = (px, py) => {
|
||||
const i = (py * N + px) * 4;
|
||||
return d[i] === alt[0] && d[i + 1] === alt[1] && d[i + 2] === alt[2] && d[i + 3] === alt[3];
|
||||
};
|
||||
const gesehen = new Set();
|
||||
const stapel = [[x, y]];
|
||||
const treffer = [];
|
||||
while (stapel.length) {
|
||||
const [px, py] = stapel.pop();
|
||||
const k = py * N + px;
|
||||
if (gesehen.has(k)) continue;
|
||||
if (px < f.x || py < f.y || px >= f.x + f.w || py >= f.y + f.h) continue;
|
||||
gesehen.add(k);
|
||||
if (!gleich(px, py)) continue;
|
||||
treffer.push([px, py]);
|
||||
stapel.push([px + 1, py], [px - 1, py], [px, py + 1], [px, py - 1]);
|
||||
}
|
||||
let geaendert = false;
|
||||
for (const [px, py] of treffer) for (const [zx, zy] of zielPixel(px, py)) if (setzePixel(zx, zy, neu)) geaendert = true;
|
||||
return geaendert;
|
||||
}
|
||||
|
||||
function nimmFarbe(x, y) {
|
||||
const i = (y * N + x) * 4;
|
||||
const d = ed.daten;
|
||||
if (d[i + 3] === 0) return false;
|
||||
setzeFarbe(rgbZuHex(d[i], d[i + 1], d[i + 2]));
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------- Striche, Rückgängig ----------
|
||||
function beginneStrich() {
|
||||
ed.strich = { snap: ed.daten.slice(), letzter: null, letzterBild: null, geaendert: false };
|
||||
}
|
||||
|
||||
function beendeStrich() {
|
||||
const s = ed.strich;
|
||||
ed.strich = null;
|
||||
if (!s || !s.geaendert) return;
|
||||
ed.undo.push(s.snap);
|
||||
if (ed.undo.length > 100) ed.undo.shift();
|
||||
ed.redo = [];
|
||||
ed.geaendert = true;
|
||||
if (ed.werkzeug !== 'radierer') merkeFarbe(ed.farbe);
|
||||
aktualisiereKnoepfe();
|
||||
}
|
||||
|
||||
// Werkzeug auf einen Texturpixel anwenden (während eines Strichs)
|
||||
function wendeAn(x, y) {
|
||||
const s = ed.strich;
|
||||
if (!s) return;
|
||||
if (ed.werkzeug === 'pipette') {
|
||||
nimmFarbe(x, y);
|
||||
return;
|
||||
}
|
||||
if (ed.werkzeug === 'fuellen') {
|
||||
if (s.letzter) return; // ein Klick = eine Füllung
|
||||
s.letzter = [x, y];
|
||||
if (fuelle(x, y)) { s.geaendert = true; texturGeaendert(); }
|
||||
return;
|
||||
}
|
||||
let geaendert = false;
|
||||
// Lücken zwischen zwei Mausereignissen schließen (nur innerhalb der Vorlage
|
||||
// sinnvoll, auf der Figur liegen benachbarte Pixel nicht nebeneinander)
|
||||
const punkte = s.letzter && s.zweiD ? linie(s.letzter[0], s.letzter[1], x, y) : [[x, y]];
|
||||
for (const [px, py] of punkte) if (malePixel(px, py)) geaendert = true;
|
||||
s.letzter = [x, y];
|
||||
if (geaendert) { s.geaendert = true; texturGeaendert(); }
|
||||
}
|
||||
|
||||
function linie(x0, y0, x1, y1) {
|
||||
const punkte = [];
|
||||
const dx = Math.abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
|
||||
const dy = -Math.abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
|
||||
let err = dx + dy;
|
||||
for (;;) {
|
||||
punkte.push([x0, y0]);
|
||||
if (x0 === x1 && y0 === y1) break;
|
||||
const e2 = 2 * err;
|
||||
if (e2 >= dy) { err += dy; x0 += sx; }
|
||||
if (e2 <= dx) { err += dx; y0 += sy; }
|
||||
}
|
||||
return punkte;
|
||||
}
|
||||
|
||||
function rueckgaengig() {
|
||||
if (!ed.undo.length) return;
|
||||
ed.redo.push(ed.daten.slice());
|
||||
ed.daten = ed.undo.pop();
|
||||
ed.geaendert = true;
|
||||
texturGeaendert();
|
||||
aktualisiereKnoepfe();
|
||||
}
|
||||
|
||||
function wiederholen() {
|
||||
if (!ed.redo.length) return;
|
||||
ed.undo.push(ed.daten.slice());
|
||||
ed.daten = ed.redo.pop();
|
||||
ed.geaendert = true;
|
||||
texturGeaendert();
|
||||
aktualisiereKnoepfe();
|
||||
}
|
||||
|
||||
// ganze Änderung (Laden, Leeren …) als ein Rückgängig-Schritt
|
||||
function aenderung(fn) {
|
||||
const snap = ed.daten.slice();
|
||||
fn();
|
||||
if (snap.every((v, i) => v === ed.daten[i])) return false;
|
||||
ed.undo.push(snap);
|
||||
if (ed.undo.length > 100) ed.undo.shift();
|
||||
ed.redo = [];
|
||||
ed.geaendert = true;
|
||||
texturGeaendert();
|
||||
aktualisiereKnoepfe();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------- Farben ----------
|
||||
const FARB_SCHLUESSEL = 'aeromc-skin-farben';
|
||||
const STANDARD_FARBEN = ['#000000', '#ffffff', '#c69c6d', '#a87b4f', '#6b4a2f', '#3b2416', '#d32f2f', '#f9a825',
|
||||
'#43a047', '#1e88e5', '#3949ab', '#8e24aa', '#00897b', '#757575', '#424242', '#ff8a65'];
|
||||
let zuletzt = [];
|
||||
try { zuletzt = JSON.parse(localStorage.getItem(FARB_SCHLUESSEL) || '[]'); } catch { zuletzt = []; }
|
||||
if (!Array.isArray(zuletzt) || !zuletzt.length) zuletzt = STANDARD_FARBEN.slice();
|
||||
|
||||
function merkeFarbe(hex) {
|
||||
zuletzt = [hex, ...zuletzt.filter((f) => f !== hex)].slice(0, 16);
|
||||
try { localStorage.setItem(FARB_SCHLUESSEL, JSON.stringify(zuletzt)); } catch { /* egal */ }
|
||||
zeigeZuletzt();
|
||||
}
|
||||
|
||||
function zeigeZuletzt() {
|
||||
const box = el('se-zuletzt');
|
||||
box.innerHTML = '';
|
||||
for (const f of zuletzt) {
|
||||
const b = document.createElement('button');
|
||||
b.className = 'se-farbfeld' + (f === ed.farbe ? ' on' : '');
|
||||
b.style.background = f;
|
||||
b.title = f;
|
||||
b.addEventListener('click', () => setzeFarbe(f));
|
||||
box.appendChild(b);
|
||||
}
|
||||
}
|
||||
|
||||
function setzeFarbe(hex) {
|
||||
ed.farbe = hex.toLowerCase();
|
||||
el('se-farbe').value = ed.farbe;
|
||||
el('se-hex').value = ed.farbe;
|
||||
// nach dem Aufnehmen zurück zum vorherigen Werkzeug; mit dem Radierer
|
||||
// ergibt eine neue Farbe keinen Sinn -> Stift
|
||||
if (ed.werkzeug === 'pipette') waehleWerkzeug(ed.vorPipette === 'radierer' ? 'stift' : ed.vorPipette);
|
||||
else if (ed.werkzeug === 'radierer') waehleWerkzeug('stift');
|
||||
zeigeZuletzt();
|
||||
}
|
||||
|
||||
// ---------- Bedienelemente ----------
|
||||
function waehleWerkzeug(w) {
|
||||
if (w === 'radierer' && ed.schicht === 0) w = 'stift';
|
||||
if (w === 'pipette' && ed.werkzeug !== 'pipette') ed.vorPipette = ed.werkzeug;
|
||||
ed.werkzeug = w;
|
||||
document.querySelectorAll('[data-se-tool]').forEach((b) => b.classList.toggle('on', b.dataset.seTool === w));
|
||||
for (const h of [el('se-2d'), el('se-3d')]) h.dataset.werkzeug = w;
|
||||
}
|
||||
|
||||
function waehleSchicht(s) {
|
||||
ed.schicht = s;
|
||||
document.querySelectorAll('[data-se-schicht]').forEach((b) => b.classList.toggle('on', Number(b.dataset.seSchicht) === s));
|
||||
// die 2. Schicht muss sichtbar sein, wenn man darauf malen will
|
||||
if (s === 1 && !ed.zeigeAussen) { ed.zeigeAussen = true; el('se-aussen').checked = true; }
|
||||
const radierer = document.querySelector('[data-se-tool="radierer"]');
|
||||
radierer.disabled = s === 0;
|
||||
radierer.title = s === 0
|
||||
? 'Radierer – nur in der 2. Schicht (die Grundschicht darf keine Löcher haben)'
|
||||
: 'Radierer (E) – macht Pixel durchsichtig';
|
||||
if (s === 0 && ed.werkzeug === 'radierer') waehleWerkzeug('stift');
|
||||
el('se-durch').disabled = s === 0;
|
||||
ed.hover = null;
|
||||
texturGeaendert();
|
||||
}
|
||||
|
||||
function waehleModus(m) {
|
||||
ed.modus = m;
|
||||
document.querySelectorAll('[data-se-modus]').forEach((b) => b.classList.toggle('on', b.dataset.seModus === m));
|
||||
const gross = el('se-buehne');
|
||||
const klein = el('se-mini');
|
||||
gross.appendChild(m === '2d' ? el('se-2d') : el('se-3d'));
|
||||
klein.appendChild(m === '2d' ? el('se-3d') : el('se-2d'));
|
||||
el('se-mini-titel').textContent = m === '2d' ? '3D-Vorschau' : 'Vorlage';
|
||||
zeigeInfo();
|
||||
passeGroessenAn();
|
||||
}
|
||||
|
||||
function aktualisiereKnoepfe() {
|
||||
el('se-undo').disabled = !ed.undo.length;
|
||||
el('se-redo').disabled = !ed.redo.length;
|
||||
}
|
||||
|
||||
function zeigeInfo() {
|
||||
const info = el('se-info');
|
||||
const h = ed.hover;
|
||||
const f = h && flaecheBei(h[0], h[1]);
|
||||
if (f) {
|
||||
info.textContent = `${f.teil.name} · ${SEITE_NAME[f.seite]} · ${SCHICHT_NAME[f.schicht]} · Pixel ${h[0]}, ${h[1]}`;
|
||||
return;
|
||||
}
|
||||
info.textContent = ed.modus === '3d'
|
||||
? 'Linke Maustaste malt · rechte Maustaste dreht · Mausrad zoomt · Alt+Klick nimmt Farbe auf'
|
||||
: 'Linke Maustaste malt · Alt+Klick nimmt eine Farbe auf · abgedunkelte Bereiche gehören zur anderen Schicht';
|
||||
}
|
||||
|
||||
function status(text) { el('se-status').textContent = text || ''; el('se-status').title = text || ''; }
|
||||
|
||||
// ---------- Zeichnen ----------
|
||||
function texturGeaendert() {
|
||||
texturCtx.putImageData(new ImageData(ed.daten, N, N), 0, 0);
|
||||
// Schatten: Grundschicht-Pixel an die Stelle der zugehörigen 2. Schicht legen
|
||||
const s = schattenCtx.createImageData(N, N);
|
||||
for (const f of aufteilung().flaechen) {
|
||||
if (f.schicht !== 1) continue;
|
||||
const r = seiten(f.teil, 0, ed.slim)[f.seite];
|
||||
for (let yy = 0; yy < f.h; yy++) {
|
||||
for (let xx = 0; xx < f.w; xx++) {
|
||||
const q = ((r[1] + yy) * N + r[0] + xx) * 4;
|
||||
const z = ((f.y + yy) * N + f.x + xx) * 4;
|
||||
s.data[z] = ed.daten[q]; s.data[z + 1] = ed.daten[q + 1]; s.data[z + 2] = ed.daten[q + 2]; s.data[z + 3] = ed.daten[q + 3];
|
||||
}
|
||||
}
|
||||
}
|
||||
schattenCtx.putImageData(s, 0, 0);
|
||||
planeZeichnen();
|
||||
}
|
||||
|
||||
function planeZeichnen() {
|
||||
if (ed.zeichnenGeplant) return;
|
||||
ed.zeichnenGeplant = true;
|
||||
requestAnimationFrame(() => {
|
||||
ed.zeichnenGeplant = false;
|
||||
zeichne2D();
|
||||
zeichne3D();
|
||||
zeigeInfo();
|
||||
});
|
||||
}
|
||||
|
||||
function hintergrundFuer(slim) {
|
||||
const k = slim ? 1 : 0;
|
||||
if (hintergrund[k] && hintergrund[k].dunkel === dunkel()) return hintergrund[k].canvas;
|
||||
const c = document.createElement('canvas');
|
||||
c.width = c.height = N;
|
||||
const g = c.getContext('2d');
|
||||
const img = g.createImageData(N, N);
|
||||
const [a, b] = dunkel() ? [[58, 58, 64], [48, 48, 54]] : [[236, 236, 238], [218, 219, 223]];
|
||||
for (const f of AUFTEILUNG[k].flaechen) {
|
||||
for (let yy = f.y; yy < f.y + f.h; yy++) {
|
||||
for (let xx = f.x; xx < f.x + f.w; xx++) {
|
||||
const farbe = (xx + yy) % 2 ? a : b;
|
||||
const i = (yy * N + xx) * 4;
|
||||
img.data[i] = farbe[0]; img.data[i + 1] = farbe[1]; img.data[i + 2] = farbe[2]; img.data[i + 3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
g.putImageData(img, 0, 0);
|
||||
hintergrund[k] = { canvas: c, dunkel: dunkel() };
|
||||
return c;
|
||||
}
|
||||
|
||||
// sichtbare Textur für die 2D-Ansicht: ohne ungenutzte Bereiche und, falls
|
||||
// ausgeblendet, ohne 2. Schicht
|
||||
const sicht = document.createElement('canvas');
|
||||
sicht.width = sicht.height = N;
|
||||
const sichtCtx = sicht.getContext('2d');
|
||||
|
||||
function zeichne2D() {
|
||||
const canvas = el('se-2d-canvas');
|
||||
const k = Number(canvas.dataset.k) || 0;
|
||||
if (!k) return;
|
||||
const g = canvas.getContext('2d');
|
||||
g.setTransform(1, 0, 0, 1, 0, 0);
|
||||
g.clearRect(0, 0, canvas.width, canvas.height);
|
||||
g.imageSmoothingEnabled = false;
|
||||
g.setTransform(k, 0, 0, k, 0, 0);
|
||||
g.fillStyle = dunkel() ? '#1c1c1f' : '#c9ccd1';
|
||||
g.fillRect(0, 0, N, N);
|
||||
g.drawImage(hintergrundFuer(ed.slim), 0, 0);
|
||||
|
||||
if (ed.schicht === 1 && ed.durchscheinen) {
|
||||
g.globalAlpha = 0.35;
|
||||
g.drawImage(schatten, 0, 0);
|
||||
g.globalAlpha = 1;
|
||||
}
|
||||
sichtCtx.clearRect(0, 0, N, N);
|
||||
const img = sichtCtx.createImageData(N, N);
|
||||
const karte = aufteilung().karte;
|
||||
for (let i = 0; i < N * N; i++) {
|
||||
const f = karte[i];
|
||||
if (!f || (f.schicht === 1 && !ed.zeigeAussen)) continue;
|
||||
const q = i * 4;
|
||||
img.data[q] = ed.daten[q]; img.data[q + 1] = ed.daten[q + 1]; img.data[q + 2] = ed.daten[q + 2]; img.data[q + 3] = ed.daten[q + 3];
|
||||
}
|
||||
sichtCtx.putImageData(img, 0, 0);
|
||||
g.drawImage(sicht, 0, 0);
|
||||
|
||||
// andere Schicht abdunkeln
|
||||
g.fillStyle = dunkel() ? 'rgba(20,20,22,.62)' : 'rgba(40,44,52,.45)';
|
||||
for (const f of aufteilung().flaechen) if (f.schicht !== ed.schicht) g.fillRect(f.x, f.y, f.w, f.h);
|
||||
|
||||
const px = 1 / k;
|
||||
// Pixelraster
|
||||
if (k >= 6) {
|
||||
g.strokeStyle = dunkel() ? 'rgba(255,255,255,.07)' : 'rgba(0,0,0,.08)';
|
||||
g.lineWidth = px;
|
||||
g.beginPath();
|
||||
for (let i = 1; i < N; i++) { g.moveTo(i, 0); g.lineTo(i, N); g.moveTo(0, i); g.lineTo(N, i); }
|
||||
g.stroke();
|
||||
}
|
||||
// Flächenränder
|
||||
g.lineWidth = 1.5 * px;
|
||||
for (const f of aufteilung().flaechen) {
|
||||
g.strokeStyle = f.schicht === ed.schicht
|
||||
? (dunkel() ? 'rgba(255,255,255,.45)' : 'rgba(0,0,0,.4)')
|
||||
: (dunkel() ? 'rgba(255,255,255,.12)' : 'rgba(0,0,0,.12)');
|
||||
g.strokeRect(f.x, f.y, f.w, f.h);
|
||||
}
|
||||
// Mauszeiger und gespiegelter Partner
|
||||
if (ed.hover) {
|
||||
const [hx, hy] = ed.hover;
|
||||
g.lineWidth = 2 * px;
|
||||
g.strokeStyle = dunkel() ? '#ffffff' : '#000000';
|
||||
g.strokeRect(hx + px, hy + px, 1 - 2 * px, 1 - 2 * px);
|
||||
if (ed.spiegeln) {
|
||||
const s = spiegelPixel(hx, hy);
|
||||
if (s && (s[0] !== hx || s[1] !== hy)) {
|
||||
g.setLineDash([2 * px, 2 * px]);
|
||||
g.strokeRect(s[0] + px, s[1] + px, 1 - 2 * px, 1 - 2 * px);
|
||||
g.setLineDash([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function zeichne3D() {
|
||||
const v = ed.viewer;
|
||||
if (!v || !v.skinTexture) return;
|
||||
const g = v.skinCanvas.getContext('2d');
|
||||
if (v.skinCanvas.width !== N || v.skinCanvas.height !== N) { v.skinCanvas.width = N; v.skinCanvas.height = N; }
|
||||
g.clearRect(0, 0, N, N);
|
||||
g.drawImage(textur, 0, 0);
|
||||
// Pixel unter der Maus hervorheben
|
||||
if (ed.hover && ed.werkzeug !== 'pipette') {
|
||||
const ziele = ed.spiegeln ? zielPixel(ed.hover[0], ed.hover[1]) : [ed.hover];
|
||||
for (const [hx, hy] of ziele) {
|
||||
const i = (hy * N + hx) * 4;
|
||||
const hell = ed.daten[i + 3] === 0 || (ed.daten[i] + ed.daten[i + 1] + ed.daten[i + 2]) < 380;
|
||||
g.fillStyle = hell ? 'rgba(255,255,255,.75)' : 'rgba(0,0,0,.55)';
|
||||
g.fillRect(hx, hy, 1, 1);
|
||||
}
|
||||
}
|
||||
v.skinTexture.needsUpdate = true;
|
||||
v.playerObject.skin.setOuterLayerVisible(ed.zeigeAussen);
|
||||
}
|
||||
|
||||
// ---------- Größen ----------
|
||||
function passeGroessenAn() {
|
||||
const h2 = el('se-2d');
|
||||
const c = el('se-2d-canvas');
|
||||
const platz = Math.min(h2.clientWidth, h2.clientHeight) - 8;
|
||||
const s = Math.max(1, Math.floor(platz / N));
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
c.style.width = c.style.height = (s * N) + 'px';
|
||||
c.width = c.height = Math.round(s * N * dpr);
|
||||
c.dataset.k = String((s * N * dpr) / N);
|
||||
const h3 = el('se-3d');
|
||||
if (ed.viewer && h3.clientWidth && h3.clientHeight) ed.viewer.setSize(h3.clientWidth, h3.clientHeight);
|
||||
planeZeichnen();
|
||||
}
|
||||
|
||||
// ---------- 2D-Eingabe ----------
|
||||
function texelAus2D(e) {
|
||||
const c = el('se-2d-canvas');
|
||||
const r = c.getBoundingClientRect();
|
||||
const x = Math.floor(((e.clientX - r.left) / r.width) * N);
|
||||
const y = Math.floor(((e.clientY - r.top) / r.height) * N);
|
||||
return x >= 0 && y >= 0 && x < N && y < N ? [x, y] : null;
|
||||
}
|
||||
|
||||
function verbinde2D() {
|
||||
const c = el('se-2d-canvas');
|
||||
c.addEventListener('pointerdown', (e) => {
|
||||
if (e.button !== 0) return;
|
||||
const t = texelAus2D(e);
|
||||
if (!t) return;
|
||||
e.preventDefault();
|
||||
try { c.setPointerCapture(e.pointerId); } catch { /* egal */ }
|
||||
if (e.altKey) { nimmFarbe(t[0], t[1]); return; }
|
||||
beginneStrich();
|
||||
ed.strich.zweiD = true;
|
||||
wendeAn(t[0], t[1]);
|
||||
});
|
||||
c.addEventListener('pointermove', (e) => {
|
||||
const t = texelAus2D(e);
|
||||
if (ed.strich && t) wendeAn(t[0], t[1]);
|
||||
const neu = t && flaecheBei(t[0], t[1]) ? t : null;
|
||||
if (String(neu) !== String(ed.hover)) { ed.hover = neu; planeZeichnen(); }
|
||||
});
|
||||
const ende = () => beendeStrich();
|
||||
c.addEventListener('pointerup', ende);
|
||||
c.addEventListener('pointercancel', ende);
|
||||
c.addEventListener('pointerleave', () => { if (!ed.strich && ed.hover) { ed.hover = null; planeZeichnen(); } });
|
||||
}
|
||||
|
||||
// ---------- 3D: Strahl gegen die Figur ----------
|
||||
// three.js' Raycaster wird von skinview3d nicht mitgeliefert – daher eigene
|
||||
// Schnittprüfung Strahl/Dreieck (Möller–Trumbore) gegen die Quader der Figur.
|
||||
function schnitt(o, d, a, b, c) {
|
||||
const e1x = b.x - a.x, e1y = b.y - a.y, e1z = b.z - a.z;
|
||||
const e2x = c.x - a.x, e2y = c.y - a.y, e2z = c.z - a.z;
|
||||
const px = d.y * e2z - d.z * e2y, py = d.z * e2x - d.x * e2z, pz = d.x * e2y - d.y * e2x;
|
||||
const det = e1x * px + e1y * py + e1z * pz;
|
||||
if (Math.abs(det) < 1e-9) return null;
|
||||
const inv = 1 / det;
|
||||
const tx = o.x - a.x, ty = o.y - a.y, tz = o.z - a.z;
|
||||
const u = (tx * px + ty * py + tz * pz) * inv;
|
||||
if (u < 0 || u > 1) return null;
|
||||
const qx = ty * e1z - tz * e1y, qy = tz * e1x - tx * e1z, qz = tx * e1y - ty * e1x;
|
||||
const v = (d.x * qx + d.y * qy + d.z * qz) * inv;
|
||||
if (v < 0 || u + v > 1) return null;
|
||||
const t = (e2x * qx + e2y * qy + e2z * qz) * inv;
|
||||
return t > 1e-6 ? { t, u, v } : null;
|
||||
}
|
||||
|
||||
// liefert den Texturpixel der getroffenen Schicht (0/1) oder null
|
||||
function strahl(clientX, clientY, schicht) {
|
||||
const v = ed.viewer;
|
||||
if (!v) return null;
|
||||
const r = v.canvas.getBoundingClientRect();
|
||||
if (!r.width || !r.height) return null;
|
||||
const cam = v.camera;
|
||||
const V3 = cam.position.constructor;
|
||||
cam.updateMatrixWorld();
|
||||
const o = new V3().setFromMatrixPosition(cam.matrixWorld);
|
||||
const nx = ((clientX - r.left) / r.width) * 2 - 1;
|
||||
const ny = -((clientY - r.top) / r.height) * 2 + 1;
|
||||
const d = new V3(nx, ny, 0.5).unproject(cam).sub(o).normalize();
|
||||
v.playerObject.updateMatrixWorld(true);
|
||||
const ecken = [new V3(), new V3(), new V3()];
|
||||
let best = null;
|
||||
for (const teil of v.playerObject.skin.getBodyParts()) {
|
||||
if (!teil.visible) continue;
|
||||
const mesh = schicht ? teil.outerLayer : teil.innerLayer;
|
||||
if (!mesh || !mesh.visible) continue;
|
||||
const geo = mesh.geometry;
|
||||
const pos = geo.attributes.position;
|
||||
const idx = geo.index;
|
||||
const anzahl = idx ? idx.count : pos.count;
|
||||
for (let i = 0; i < anzahl; i += 3) {
|
||||
const ia = idx ? [idx.getX(i), idx.getX(i + 1), idx.getX(i + 2)] : [i, i + 1, i + 2];
|
||||
for (let k = 0; k < 3; k++) ecken[k].fromBufferAttribute(pos, ia[k]).applyMatrix4(mesh.matrixWorld);
|
||||
const h = schnitt(o, d, ecken[0], ecken[1], ecken[2]);
|
||||
if (h && (!best || h.t < best.t)) best = { t: h.t, b: [1 - h.u - h.v, h.u, h.v], ia, uv: geo.attributes.uv };
|
||||
}
|
||||
}
|
||||
if (!best) return null;
|
||||
let U = 0, W = 0;
|
||||
let uMin = 1, uMax = 0, vMin = 1, vMax = 0;
|
||||
for (let k = 0; k < 3; k++) {
|
||||
const u = best.uv.getX(best.ia[k]);
|
||||
const w = best.uv.getY(best.ia[k]);
|
||||
U += best.b[k] * u; W += best.b[k] * w;
|
||||
uMin = Math.min(uMin, u); uMax = Math.max(uMax, u); vMin = Math.min(vMin, w); vMax = Math.max(vMax, w);
|
||||
}
|
||||
// Dreieck gehört zu genau einer Seitenfläche – Rundung an der Kante darf
|
||||
// nicht in die Nachbarfläche rutschen
|
||||
const x0 = Math.round(uMin * N), x1 = Math.round(uMax * N) - 1;
|
||||
const y0 = Math.round((1 - vMax) * N), y1 = Math.round((1 - vMin) * N) - 1;
|
||||
const x = Math.min(x1, Math.max(x0, Math.floor(U * N)));
|
||||
const y = Math.min(y1, Math.max(y0, Math.floor((1 - W) * N)));
|
||||
return { t: best.t, x, y };
|
||||
}
|
||||
|
||||
// sichtbarer Pixel für die Pipette: erst die 2. Schicht (falls dort Farbe ist)
|
||||
function sichtbarerPixel(cx, cy) {
|
||||
if (ed.zeigeAussen) {
|
||||
const a = strahl(cx, cy, 1);
|
||||
if (a && ed.daten[(a.y * N + a.x) * 4 + 3] > 0) return [a.x, a.y];
|
||||
}
|
||||
const b = strahl(cx, cy, 0);
|
||||
return b ? [b.x, b.y] : null;
|
||||
}
|
||||
|
||||
function verbinde3D() {
|
||||
const huelle = el('se-3d');
|
||||
const canvas = el('se-3d-canvas');
|
||||
// in der Einfangphase: trifft die Maus die Figur, malen wir und die
|
||||
// Drehsteuerung von skinview3d bekommt das Ereignis gar nicht erst
|
||||
huelle.addEventListener('pointerdown', (e) => {
|
||||
if (e.button !== 0 || !ed.viewer) return;
|
||||
if (e.altKey || ed.werkzeug === 'pipette') {
|
||||
const t = sichtbarerPixel(e.clientX, e.clientY);
|
||||
if (!t) return;
|
||||
e.stopPropagation(); e.preventDefault();
|
||||
nimmFarbe(t[0], t[1]);
|
||||
return;
|
||||
}
|
||||
const t = strahl(e.clientX, e.clientY, ed.schicht);
|
||||
if (!t) return;
|
||||
e.stopPropagation(); e.preventDefault();
|
||||
try { canvas.setPointerCapture(e.pointerId); } catch { /* egal */ }
|
||||
beginneStrich();
|
||||
ed.strich.letzterBild = [e.clientX, e.clientY];
|
||||
wendeAn(t.x, t.y);
|
||||
}, true);
|
||||
canvas.addEventListener('pointermove', (e) => {
|
||||
if (ed.strich) {
|
||||
// Strecke seit dem letzten Ereignis in kleinen Schritten abtasten
|
||||
const [ax, ay] = ed.strich.letzterBild || [e.clientX, e.clientY];
|
||||
const schritte = Math.max(1, Math.ceil(Math.hypot(e.clientX - ax, e.clientY - ay) / 2));
|
||||
for (let i = 1; i <= schritte; i++) {
|
||||
const t = strahl(ax + ((e.clientX - ax) * i) / schritte, ay + ((e.clientY - ay) * i) / schritte, ed.schicht);
|
||||
if (t) wendeAn(t.x, t.y);
|
||||
}
|
||||
ed.strich.letzterBild = [e.clientX, e.clientY];
|
||||
}
|
||||
if (e.buttons && !ed.strich) return; // dreht gerade
|
||||
const t = strahl(e.clientX, e.clientY, ed.schicht);
|
||||
const neu = t ? [t.x, t.y] : null;
|
||||
if (String(neu) !== String(ed.hover)) { ed.hover = neu; planeZeichnen(); }
|
||||
});
|
||||
const ende = () => beendeStrich();
|
||||
canvas.addEventListener('pointerup', ende);
|
||||
canvas.addEventListener('pointercancel', ende);
|
||||
canvas.addEventListener('pointerleave', () => { if (!ed.strich && ed.hover) { ed.hover = null; planeZeichnen(); } });
|
||||
}
|
||||
|
||||
async function erzeugeViewer() {
|
||||
if (ed.viewer || typeof skinview3d === 'undefined') {
|
||||
if (typeof skinview3d === 'undefined') el('se-3d-leer').textContent = '3D-Ansicht nicht verfügbar';
|
||||
return;
|
||||
}
|
||||
const h = el('se-3d');
|
||||
const viewer = new skinview3d.SkinViewer({
|
||||
canvas: el('se-3d-canvas'),
|
||||
width: h.clientWidth || 300,
|
||||
height: h.clientHeight || 300,
|
||||
skin: textur,
|
||||
model: ed.slim ? 'slim' : 'default',
|
||||
});
|
||||
await new Promise((resolve) => {
|
||||
const check = (n) => {
|
||||
if ((viewer.playerObject && viewer.playerObject.skin && viewer.playerObject.skin.map) || n > 150) resolve();
|
||||
else setTimeout(() => check(n + 1), 40);
|
||||
};
|
||||
check(0);
|
||||
});
|
||||
viewer.fov = 36;
|
||||
viewer.zoom = 0.78;
|
||||
viewer.autoRotate = false;
|
||||
viewer.animation = null;
|
||||
// leicht abgespreizte Arme/Beine, damit man auch an die Innenseiten kommt
|
||||
const p = viewer.playerObject;
|
||||
p.rotation.set(0, -0.35, 0);
|
||||
p.skin.leftArm.rotation.set(0, 0, 0.2);
|
||||
p.skin.rightArm.rotation.set(0, 0, -0.2);
|
||||
p.skin.leftLeg.rotation.set(0, 0, 0.06);
|
||||
p.skin.rightLeg.rotation.set(0, 0, -0.06);
|
||||
viewer.controls.enableRotate = true;
|
||||
viewer.controls.enableZoom = true;
|
||||
viewer.controls.enablePan = false;
|
||||
// rechte Maustaste dreht immer – auch wenn man gerade auf die Figur zeigt
|
||||
viewer.controls.mouseButtons = { LEFT: 0, MIDDLE: 1, RIGHT: 0 };
|
||||
ed.viewer = viewer;
|
||||
el('se-3d-leer').classList.add('hidden');
|
||||
passeGroessenAn();
|
||||
}
|
||||
|
||||
async function setzeArmform(slim) {
|
||||
ed.slim = !!slim;
|
||||
el('se-variante').value = ed.slim ? 'slim' : 'classic';
|
||||
ed.hover = null;
|
||||
if (ed.viewer) {
|
||||
await ed.viewer.loadSkin(textur, { model: ed.slim ? 'slim' : 'default' });
|
||||
}
|
||||
texturGeaendert();
|
||||
}
|
||||
|
||||
// ---------- Laden / Vorlage ----------
|
||||
function vorlage() {
|
||||
const d = new Uint8ClampedArray(N * N * 4);
|
||||
const farben = { head: '#c69c6d', body: '#2f9e9e', rightArm: '#c69c6d', leftArm: '#c69c6d', rightLeg: '#3b3f9e', leftLeg: '#3b3f9e' };
|
||||
for (const f of AUFTEILUNG[ed.slim ? 1 : 0].flaechen) {
|
||||
if (f.schicht !== 0) continue;
|
||||
const [r, g, b] = hexZuRgb(farben[f.teil.key]);
|
||||
for (let yy = f.y; yy < f.y + f.h; yy++) {
|
||||
for (let xx = f.x; xx < f.x + f.w; xx++) {
|
||||
const i = (yy * N + xx) * 4;
|
||||
d[i] = r; d[i + 1] = g; d[i + 2] = b; d[i + 3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
function ladeBild(dataUri) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
if (img.width !== N || (img.height !== N && img.height !== N / 2)) {
|
||||
reject(new Error(`Ein Skin muss 64×64 oder 64×32 Pixel groß sein (diese Datei: ${img.width}×${img.height}).`));
|
||||
return;
|
||||
}
|
||||
const c = document.createElement('canvas');
|
||||
c.width = c.height = N;
|
||||
const g = c.getContext('2d', { willReadFrequently: true });
|
||||
g.drawImage(img, 0, 0);
|
||||
const daten = g.getImageData(0, 0, N, N).data;
|
||||
if (img.height === N / 2) {
|
||||
// alte 64×32-Skins: linker Arm und linkes Bein sind dort gespiegelte
|
||||
// Kopien der rechten – wie Minecraft es selbst macht
|
||||
const karte = AUFTEILUNG[0].karte;
|
||||
for (let i = 0; i < N * N / 2; i++) {
|
||||
const f = karte[i];
|
||||
if (!f || f.schicht !== 0 || (f.teil.key !== 'rightArm' && f.teil.key !== 'rightLeg')) continue;
|
||||
const partner = TEIL[f.teil.spiegel];
|
||||
const seite = SEITE_GEGENSTUECK[f.seite] || f.seite;
|
||||
const r = seiten(partner, 0, false)[seite];
|
||||
const x = i % N, y = Math.floor(i / N);
|
||||
const z = ((r[1] + (y - f.y)) * N + r[0] + (r[2] - 1 - (x - f.x))) * 4;
|
||||
for (let k = 0; k < 4; k++) daten[z + k] = daten[i * 4 + k];
|
||||
}
|
||||
}
|
||||
resolve(daten);
|
||||
};
|
||||
img.onerror = () => reject(new Error('Bild konnte nicht gelesen werden.'));
|
||||
img.src = dataUri;
|
||||
});
|
||||
}
|
||||
|
||||
async function uebernimmDaten(daten, variante, text) {
|
||||
if (variante) await setzeArmform(variante === 'slim');
|
||||
aenderung(() => { ed.daten = new Uint8ClampedArray(daten); });
|
||||
ed.geaendert = false;
|
||||
texturGeaendert();
|
||||
status(text || '');
|
||||
}
|
||||
|
||||
function wennVerwerfenOk(aktion) {
|
||||
if (!ed.geaendert) { aktion(); return; }
|
||||
askConfirm('Änderungen verwerfen?', 'Der Skin im Editor hat ungespeicherte Änderungen. Trotzdem fortfahren?', 'Verwerfen', aktion);
|
||||
}
|
||||
|
||||
async function ladeAktuellen() {
|
||||
status('Aktueller Skin wird geladen …');
|
||||
const res = await window.api.skinCurrentData();
|
||||
if (!res.ok) {
|
||||
if (res.standard) {
|
||||
await uebernimmDaten(vorlage(), res.variante, 'Dein Konto nutzt den Standard-Skin – hier ist eine leere Vorlage zum Loslegen.');
|
||||
} else {
|
||||
status('✖ ' + (res.message || 'Skin konnte nicht geladen werden.'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await uebernimmDaten(await ladeBild(res.dataUri), res.variante, `Skin von ${res.name || 'deinem Konto'} geladen.`);
|
||||
} catch (err) { status('✖ ' + err.message); }
|
||||
}
|
||||
|
||||
async function ladeDatei(pfad, variante) {
|
||||
const res = pfad ? await window.api.skinReadData(pfad) : await window.api.skinChoose();
|
||||
if (res.canceled) return;
|
||||
if (!res.ok) { status('✖ ' + (res.message || 'Datei konnte nicht geladen werden.')); return; }
|
||||
try {
|
||||
await uebernimmDaten(await ladeBild(res.dataUri), variante, `${(res.pfad || pfad || '').split(/[\\/]/).pop()} geladen.`);
|
||||
} catch (err) { status('✖ ' + err.message); }
|
||||
}
|
||||
|
||||
// ---------- Speichern / Übernehmen ----------
|
||||
function alsBase64() {
|
||||
const c = document.createElement('canvas');
|
||||
c.width = c.height = N;
|
||||
c.getContext('2d').putImageData(new ImageData(ed.daten, N, N), 0, 0);
|
||||
return c.toDataURL('image/png').split(',')[1];
|
||||
}
|
||||
|
||||
// Minecraft zeigt durchsichtige Pixel der Grundschicht schwarz an
|
||||
function loecherInGrundschicht() {
|
||||
let n = 0;
|
||||
for (const f of aufteilung().flaechen) {
|
||||
if (f.schicht !== 0) continue;
|
||||
for (let yy = f.y; yy < f.y + f.h; yy++) for (let xx = f.x; xx < f.x + f.w; xx++) if (ed.daten[(yy * N + xx) * 4 + 3] < 255) n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
function loecherHinweis() {
|
||||
const n = loecherInGrundschicht();
|
||||
return n ? ` Hinweis: ${n} durchsichtige Pixel in der Grundschicht erscheinen im Spiel schwarz.` : '';
|
||||
}
|
||||
|
||||
async function speichere() {
|
||||
const res = await window.api.skinSaveData(alsBase64());
|
||||
if (res.canceled) return;
|
||||
if (!res.ok) { status('✖ ' + (res.message || 'Speichern fehlgeschlagen.')); return; }
|
||||
ed.geaendert = false;
|
||||
status('✔ Gespeichert: ' + res.pfad.split(/[\\/]/).pop() + '.' + loecherHinweis());
|
||||
}
|
||||
|
||||
async function uebernimm() {
|
||||
const btn = el('se-uebernehmen');
|
||||
btn.disabled = true;
|
||||
status('Wird hochgeladen …');
|
||||
try {
|
||||
const res = await window.api.skinApplyData(alsBase64(), ed.slim ? 'slim' : 'classic');
|
||||
if (res.ok) {
|
||||
ed.geaendert = false;
|
||||
status('✔ Skin gesetzt. Im Spiel kann es einen Moment dauern.' + loecherHinweis());
|
||||
toast('Skin geändert.');
|
||||
} else {
|
||||
status('✖ ' + (res.message || 'Fehlgeschlagen.'));
|
||||
}
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Öffnen ----------
|
||||
let verdrahtet = false;
|
||||
|
||||
function verdrahte() {
|
||||
if (verdrahtet) return;
|
||||
verdrahtet = true;
|
||||
document.querySelectorAll('[data-se-tool]').forEach((b) => b.addEventListener('click', () => waehleWerkzeug(b.dataset.seTool)));
|
||||
document.querySelectorAll('[data-se-schicht]').forEach((b) => b.addEventListener('click', () => waehleSchicht(Number(b.dataset.seSchicht))));
|
||||
document.querySelectorAll('[data-se-modus]').forEach((b) => b.addEventListener('click', () => waehleModus(b.dataset.seModus)));
|
||||
el('se-farbe').addEventListener('input', () => setzeFarbe(el('se-farbe').value));
|
||||
el('se-hex').addEventListener('change', () => {
|
||||
let v = el('se-hex').value.trim();
|
||||
if (!v.startsWith('#')) v = '#' + v;
|
||||
if (/^#[0-9a-f]{6}$/i.test(v)) setzeFarbe(v);
|
||||
else el('se-hex').value = ed.farbe;
|
||||
});
|
||||
el('se-aussen').addEventListener('change', () => {
|
||||
ed.zeigeAussen = el('se-aussen').checked;
|
||||
if (!ed.zeigeAussen && ed.schicht === 1) waehleSchicht(0);
|
||||
planeZeichnen();
|
||||
});
|
||||
el('se-durch').addEventListener('change', () => { ed.durchscheinen = el('se-durch').checked; planeZeichnen(); });
|
||||
el('se-spiegeln').addEventListener('change', () => { ed.spiegeln = el('se-spiegeln').checked; planeZeichnen(); });
|
||||
el('se-leeren').addEventListener('click', () => {
|
||||
askConfirm('2. Schicht leeren?', 'Alle Pixel der 2. Schicht werden durchsichtig. Mit Strg+Z lässt sich das rückgängig machen.', 'Leeren', () => {
|
||||
const geaendert = aenderung(() => {
|
||||
for (const f of aufteilung().flaechen) {
|
||||
if (f.schicht !== 1) continue;
|
||||
for (let yy = f.y; yy < f.y + f.h; yy++) for (let xx = f.x; xx < f.x + f.w; xx++) setzePixel(xx, yy, [0, 0, 0, 0]);
|
||||
}
|
||||
});
|
||||
status(geaendert ? '2. Schicht geleert.' : 'Die 2. Schicht war schon leer.');
|
||||
});
|
||||
});
|
||||
el('se-variante').addEventListener('change', () => setzeArmform(el('se-variante').value === 'slim'));
|
||||
el('se-undo').addEventListener('click', rueckgaengig);
|
||||
el('se-redo').addEventListener('click', wiederholen);
|
||||
el('se-neu').addEventListener('click', () => wennVerwerfenOk(() => uebernimmDaten(vorlage(), null, 'Neue Vorlage – leg los!')));
|
||||
el('se-laden').addEventListener('click', () => wennVerwerfenOk(ladeAktuellen));
|
||||
el('se-oeffnen').addEventListener('click', () => wennVerwerfenOk(() => ladeDatei(null, null)));
|
||||
el('se-speichern').addEventListener('click', speichere);
|
||||
el('se-uebernehmen').addEventListener('click', uebernimm);
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (el('modal-skineditor').classList.contains('hidden')) return;
|
||||
if (!el('modal-confirm').classList.contains('hidden')) return;
|
||||
const a = document.activeElement;
|
||||
if (a && (['INPUT', 'TEXTAREA', 'SELECT'].includes(a.tagName) && a.type !== 'checkbox')) return;
|
||||
const strg = e.ctrlKey || e.metaKey;
|
||||
const k = e.key.toLowerCase();
|
||||
if (strg && k === 'z' && !e.shiftKey) { e.preventDefault(); rueckgaengig(); return; }
|
||||
if (strg && (k === 'y' || (k === 'z' && e.shiftKey))) { e.preventDefault(); wiederholen(); return; }
|
||||
if (strg || e.altKey) return;
|
||||
const werkzeug = { b: 'stift', e: 'radierer', i: 'pipette', g: 'fuellen' }[k];
|
||||
if (werkzeug) { e.preventDefault(); waehleWerkzeug(werkzeug); return; }
|
||||
if (k === 'm') { e.preventDefault(); el('se-spiegeln').click(); return; }
|
||||
if (k === 'l') { e.preventDefault(); waehleSchicht(ed.schicht ? 0 : 1); }
|
||||
});
|
||||
|
||||
verbinde2D();
|
||||
verbinde3D();
|
||||
new ResizeObserver(() => passeGroessenAn()).observe(el('se-buehne'));
|
||||
new ResizeObserver(() => passeGroessenAn()).observe(el('se-mini'));
|
||||
// Hell/Dunkel gewechselt -> Hintergrund neu
|
||||
new MutationObserver(() => planeZeichnen()).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
||||
}
|
||||
|
||||
async function openSkinEditor(p = {}) {
|
||||
if (POPOUT !== 'skineditor') { window.api.popoutOpen('skineditor', p); return; }
|
||||
verdrahte();
|
||||
show('modal-skineditor');
|
||||
if (!ed.bereit) {
|
||||
ed.bereit = true;
|
||||
el('se-farbe').value = ed.farbe;
|
||||
el('se-hex').value = ed.farbe;
|
||||
zeigeZuletzt();
|
||||
waehleWerkzeug('stift');
|
||||
waehleSchicht(0);
|
||||
aktualisiereKnoepfe();
|
||||
ed.daten = vorlage();
|
||||
texturGeaendert();
|
||||
waehleModus('2d');
|
||||
await erzeugeViewer();
|
||||
texturGeaendert();
|
||||
}
|
||||
const variante = p.variante === 'slim' || p.variante === 'classic' ? p.variante : null;
|
||||
if (p.pfad) wennVerwerfenOk(() => ladeDatei(p.pfad, variante));
|
||||
else if (p.quelle === 'aktuell') wennVerwerfenOk(ladeAktuellen);
|
||||
else if (variante && !ed.geaendert) await setzeArmform(variante === 'slim');
|
||||
}
|
||||
|
||||
window.openSkinEditor = openSkinEditor;
|
||||
// für Tests
|
||||
window.__skinEditor = { ed, strahl, spiegelPixel, seiten, AUFTEILUNG };
|
||||
})();
|
||||
@@ -75,9 +75,21 @@ const DEFAULT_SETTINGS = {
|
||||
sortBy: 'lastPlayed', // lastPlayed | name
|
||||
iso3dIcons: true, // Block-Symbole räumlich darstellen
|
||||
bedrockEnabled: true, // Bedrock-Bereich anzeigen (nur wenn auch installiert)
|
||||
desktopNotifications: true, // Windows-Meldung bei fertigem Hintergrund-Download/-Backup
|
||||
friendsEnabled: true,
|
||||
friendsShareServer: true, // Freunde sehen, auf welchem Server man spielt (für "Beitreten")
|
||||
autoModUpdates: false, // vor jedem Start Mods über Modrinth aktualisieren
|
||||
schnellstart: {}, // Strg+1 … Strg+5 -> Instanz-ID ({ "1": "Survival", … })
|
||||
autoWorldBackup: true, // nach jeder Spielsitzung die zuletzt gespielte Welt sichern (letzte 5 Stände) // Freundesystem: eigene Online-Meldung + auffindbar für Anfragen
|
||||
friendNicknames: {}, // rein lokal: uuid -> eigener Spitzname für einen Freund
|
||||
autostart: false, // mit Windows starten, im Infobereich statt als Fenster
|
||||
discordClientId: '', // optional, nur per Hand in settings.json; leer => eingebaute AeroMC-App-ID
|
||||
bgSkin: '', // '' = aus, 'active' = aktives Konto, sonst Konto-UUID
|
||||
bgSkinOpacity: 55, // Deckkraft des Hintergrund-Skins in %
|
||||
bgSkinPose: 'walking', // Pose des 3D-Renders (falls der Pose-Dienst erreichbar ist)
|
||||
bgSkinModel: 'auto', // Armform der Figur: 'auto' (vom Konto), 'classic' (Steve), 'slim' (Alex)
|
||||
bgSkinCycle: false, // bei 2+ angemeldeten Konten automatisch durchwechseln
|
||||
bgSkinCycleSeconds: 30, // Sekunden zwischen zwei Kontowechseln
|
||||
wrapperCommand: '', // Externe Programme: Startprogramm, das Java umschließt
|
||||
proxyEnabled: false, // HTTP-Proxy für Downloads/Netzwerk
|
||||
proxyHost: '',
|
||||
@@ -85,6 +97,16 @@ const DEFAULT_SETTINGS = {
|
||||
proxyUser: '',
|
||||
proxyPass: '',
|
||||
theme: 'dark',
|
||||
// Spielaufzeichnung - bleibt über Neustarts erhalten (letzte Wahl je Feld)
|
||||
recordingResolution: 'native',
|
||||
recordingQuality: 'medium',
|
||||
recordingFramerate: 30,
|
||||
recordingAudioSystem: true,
|
||||
recordingAudioGame: false,
|
||||
recordingVolSystem: 100,
|
||||
recordingVolGame: 100,
|
||||
recordingMicSelected: [], // Liste gewählter Mikrofon-deviceIds
|
||||
recordingMicVolumes: {}, // deviceId -> Lautstärke (0-100)
|
||||
};
|
||||
|
||||
function settingsFile() {
|
||||
@@ -130,8 +152,12 @@ function gameDir(id) {
|
||||
* "mod" behält die bisherigen Namen, damit vorhandene Instanzen unverändert
|
||||
* weiterlaufen.
|
||||
*/
|
||||
const INHALT_ORDNER = { mod: 'mods', resourcepack: 'resourcepacks', shader: 'shaderpacks' };
|
||||
const INHALT_INDEX = { mod: 'mods.json', resourcepack: 'resourcepacks.json', shader: 'shaders.json' };
|
||||
const INHALT_ORDNER = {
|
||||
mod: 'mods', resourcepack: 'resourcepacks', shader: 'shaderpacks', datapack: 'datapacks',
|
||||
};
|
||||
const INHALT_INDEX = {
|
||||
mod: 'mods.json', resourcepack: 'resourcepacks.json', shader: 'shaders.json', datapack: 'datapacks.json',
|
||||
};
|
||||
|
||||
function inhaltsDir(id, art = 'mod') {
|
||||
return path.join(gameDir(id), INHALT_ORDNER[art] || INHALT_ORDNER.mod);
|
||||
@@ -232,6 +258,24 @@ function addMod(id, entry, art = 'mod') {
|
||||
return list;
|
||||
}
|
||||
|
||||
/*
|
||||
* Findet installierte Mods, die den per "filename" identifizierten Mod als
|
||||
* Pflicht-Abhängigkeit brauchen (requiredDeps wird beim Installieren über
|
||||
* AeroMC gesetzt – von Hand hinzugefügte oder vor diesem Feature installierte
|
||||
* Mods haben keine requiredDeps und werden daher nicht als Ziel erkannt,
|
||||
* können aber selbst weiterhin als Abhängiger auftauchen).
|
||||
* -> [Titel, Titel, ...] der abhängigen Mods
|
||||
*/
|
||||
function findDependents(id, filename, art = 'mod') {
|
||||
const list = listMods(id, art);
|
||||
const target = list.find((m) => m.filename === filename);
|
||||
if (!target || !target.projectId) return [];
|
||||
return list
|
||||
.filter((m) => m.filename !== filename
|
||||
&& Array.isArray(m.requiredDeps) && m.requiredDeps.includes(target.projectId))
|
||||
.map((m) => m.title || m.filename);
|
||||
}
|
||||
|
||||
function removeMod(id, filename, art = 'mod') {
|
||||
const list = listMods(id, art).filter((m) => m.filename !== filename);
|
||||
writeJson(modsIndexFile(id, art), list);
|
||||
@@ -256,6 +300,7 @@ function defaultInstance(name) {
|
||||
created: now,
|
||||
lastPlayed: null,
|
||||
totalPlaySeconds: 0,
|
||||
launchCount: 0, // Anzahl Spielstarts (Spielzeit-Statistik)
|
||||
minecraft: {
|
||||
version: '', // z. B. "1.21.4" – wird in Phase 2 aus dem Mojang-Manifest befüllt
|
||||
loader: 'vanilla', // vanilla | fabric | forge | neoforge | quilt
|
||||
@@ -387,6 +432,7 @@ function createOrReplaceImportedInstance(data) {
|
||||
inst.created = existing.created || inst.created;
|
||||
inst.lastPlayed = existing.lastPlayed || null;
|
||||
inst.totalPlaySeconds = existing.totalPlaySeconds || 0;
|
||||
inst.launchCount = existing.launchCount || 0;
|
||||
|
||||
const p = instancePath(existing.id);
|
||||
if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true });
|
||||
@@ -419,6 +465,7 @@ function duplicateInstance(id) {
|
||||
copy.created = new Date().toISOString();
|
||||
copy.lastPlayed = null;
|
||||
copy.totalPlaySeconds = 0;
|
||||
copy.launchCount = 0;
|
||||
ensureDir(gameDir(copy.id));
|
||||
// Spielinhalte mitkopieren (Mods/Configs) – ohne Fehler wenn leer
|
||||
try {
|
||||
@@ -443,6 +490,7 @@ module.exports = {
|
||||
listMods,
|
||||
addMod,
|
||||
removeMod,
|
||||
findDependents,
|
||||
setModEnabled,
|
||||
listInstances,
|
||||
getInstance,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
html,body{margin:0;height:100%;font-family:var(--font);font-size:12px;color:var(--text);background:var(--win)}
|
||||
body{display:grid;grid-template-rows:auto 1fr auto;height:100vh;overflow:hidden}
|
||||
body{display:grid;grid-template-rows:auto auto 1fr auto;grid-template-columns:minmax(0,1fr);height:100vh;overflow:hidden}
|
||||
.hidden{display:none!important}
|
||||
.mono{font-family:var(--mono);font-size:12px}
|
||||
.spacer{flex:1}
|
||||
@@ -32,6 +32,7 @@ body{display:grid;grid-template-rows:auto 1fr auto;height:100vh;overflow:hidden}
|
||||
:root[data-theme="dark"] .field select,
|
||||
:root[data-theme="dark"] .field textarea,
|
||||
:root[data-theme="dark"] .mods-search input,
|
||||
:root[data-theme="dark"] .path-row input,
|
||||
:root[data-theme="dark"] .login-usercode{background:#1e1e1e;color:var(--text);border-color:var(--border)}
|
||||
:root[data-theme="dark"] .modal-card{background:#2d2d30;border-color:#161618}
|
||||
:root[data-theme="dark"] .modal-head{background:#333337}
|
||||
@@ -67,15 +68,26 @@ body{display:grid;grid-template-rows:auto 1fr auto;height:100vh;overflow:hidden}
|
||||
:root[data-theme="dark"] #btn-console svg{color:#c8c8cc}
|
||||
:root[data-theme="dark"] .acct-av svg{color:#c8c8cc}
|
||||
:root[data-theme="dark"] .mods-filters select,
|
||||
:root[data-theme="dark"] .mods-search select{background:#1e1e1e;color:var(--text);border-color:var(--border)}
|
||||
:root[data-theme="dark"] .mods-search select,
|
||||
:root[data-theme="dark"] .mmc-bar select{background:#1e1e1e;color:var(--text);border-color:var(--border)}
|
||||
:root[data-theme="dark"] .upd-notes{background:#1e1e1e;color:var(--text)}
|
||||
:root[data-theme="dark"] .modal-close{color:#aaa}
|
||||
:root[data-theme="dark"] .modal-close:hover{color:#fff}
|
||||
:root[data-theme="dark"] ::-webkit-scrollbar-thumb{background:#4a4a52;border-color:var(--area)}
|
||||
|
||||
/* ---------- Titelleiste (ersetzt die weiße Windows-Leiste) ---------- */
|
||||
/* Farben passend zu TITELLEISTE in main.js (dort für die Fensterknöpfe) */
|
||||
:root{--titel-bg:#f0f0f0;--titel-fg:#1c1c1c}
|
||||
:root[data-theme="dark"]{--titel-bg:#2d2d30;--titel-fg:#e4e4e6}
|
||||
.titelleiste{display:flex;align-items:center;gap:8px;height:32px;padding:0 150px 0 10px;background:var(--titel-bg);color:var(--titel-fg);-webkit-app-region:drag;user-select:none;font-size:12px}
|
||||
.titel-logo{width:16px;height:16px;flex:none}
|
||||
.titel-text{white-space:nowrap}
|
||||
body.fenster-inaktiv .titelleiste>*{opacity:.55}
|
||||
body.is-popout>.titelleiste{display:none}
|
||||
|
||||
/* ---------- Toolbar ---------- */
|
||||
.toolbar{display:flex;align-items:center;gap:2px;padding:5px 8px;background:var(--win);border-bottom:1px solid var(--border)}
|
||||
.tbtn{display:inline-flex;align-items:center;gap:7px;padding:6px 10px;border:1px solid transparent;border-radius:3px;background:none;color:var(--text);cursor:pointer;font-family:inherit;font-size:12px}
|
||||
.toolbar{display:flex;align-items:center;gap:2px;padding:5px 8px;min-width:0;overflow:hidden;background:var(--win);border-bottom:1px solid var(--border)}
|
||||
.tbtn{display:inline-flex;align-items:center;gap:7px;white-space:nowrap;padding:6px 10px;border:1px solid transparent;border-radius:3px;background:none;color:var(--text);cursor:pointer;font-family:inherit;font-size:12px}
|
||||
.tbtn:hover{background:#e2e6ec;border-color:var(--border2)}
|
||||
.tbtn:active{background:#d7dde5}
|
||||
.tbtn svg{width:18px;height:18px;color:#4b5563}
|
||||
@@ -93,7 +105,7 @@ body{display:grid;grid-template-rows:auto 1fr auto;height:100vh;overflow:hidden}
|
||||
.search-wrap input:focus{outline:none;border-color:var(--accent)}
|
||||
|
||||
/* ---------- Arbeitsbereich ---------- */
|
||||
.workspace{display:grid;grid-template-columns:1fr 172px;overflow:hidden;background:var(--area)}
|
||||
.workspace{display:grid;grid-template-columns:minmax(0,1fr) 172px;overflow:hidden;min-width:0;background:var(--area)}
|
||||
.list-area{position:relative;overflow:hidden;min-height:0}
|
||||
/* Skin-Hintergrund (wie MultiMCs Katze) – liegt hinter den Instanzen */
|
||||
.list-area::after{
|
||||
@@ -141,13 +153,24 @@ body{display:grid;grid-template-rows:auto 1fr auto;height:100vh;overflow:hidden}
|
||||
.group-header.drop-hover,.group-section.drop-hover{outline:1px dashed var(--accent);outline-offset:1px}
|
||||
.card.dragging{opacity:.4}
|
||||
|
||||
/* Datei von außen übers Fenster gezogen (.jar/.mrpack/.zip) */
|
||||
body.file-drop-active::after{
|
||||
content:"Datei loslassen zum Importieren";
|
||||
position:fixed;inset:0;z-index:9999;
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
background:rgba(0,0,0,.55);
|
||||
color:#fff;font-size:20px;font-weight:600;
|
||||
border:3px dashed var(--accent);
|
||||
pointer-events:none;
|
||||
}
|
||||
|
||||
/* ---------- Leerer Zustand ---------- */
|
||||
.empty{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;color:var(--dim);gap:8px}
|
||||
.empty h2{color:#333;margin:0}
|
||||
.empty p{max-width:340px;margin:0 0 8px}
|
||||
|
||||
/* ---------- Statusleiste (zwei Zeilen) ---------- */
|
||||
.statusbar{display:flex;flex-direction:column;background:var(--win);border-top:1px solid var(--border)}
|
||||
.statusbar{display:flex;flex-direction:column;min-width:0;background:var(--win);border-top:1px solid var(--border)}
|
||||
.st-row{display:flex;justify-content:space-between;padding:3px 10px;font-size:11px;color:#333}
|
||||
.st-row2{background:#e4e4e4;border-top:1px solid var(--border2);color:#444}
|
||||
.auth-warn{
|
||||
@@ -174,6 +197,42 @@ body{display:grid;grid-template-rows:auto 1fr auto;height:100vh;overflow:hidden}
|
||||
.modal-body{padding:16px;overflow:auto;background:#f6f6f6}
|
||||
.modal-foot{display:flex;justify-content:flex-end;gap:8px;padding:10px 14px;border-top:1px solid var(--border);background:#efefef}
|
||||
|
||||
/* Popup-Fenster (Einstellungen/Konsole): Haupt-UI ausblenden, das eine Modal
|
||||
füllt das ganze (rahmenlose) Fenster; der eigene Kopfbereich ersetzt die
|
||||
native Titelleiste – per -webkit-app-region per Maus verschiebbar */
|
||||
body.is-popout>header.toolbar,body.is-popout>main.workspace,body.is-popout>footer.statusbar{display:none}
|
||||
.popout-host{position:static;inset:auto;background:none;display:block}
|
||||
/* !important: schlägt auch die spezifischere #modal-settings-Breitenregel oben */
|
||||
.popout-host .modal-card{width:100%!important;max-width:none!important;height:100vh!important;max-height:none!important;border:none;border-radius:0;box-shadow:none;margin:0}
|
||||
/* Inhalt soll die restliche Fensterhöhe füllen (scrollen statt Leerraum unter dem Fuß) */
|
||||
.popout-host .modal-body{flex:1 1 auto;min-height:0}
|
||||
/* Konsole/Logs/Screenshots haben einen einzelnen scrollbaren Kasten mit
|
||||
fester Höhe (für den normalen Dialog gedacht) – im Popup-Fenster soll der
|
||||
stattdessen die volle verbleibende Höhe ausfüllen statt Leerraum darunter
|
||||
zu lassen */
|
||||
#modal-console.popout-host .modal-body,
|
||||
#modal-logs.popout-host .modal-body,
|
||||
#modal-screens.popout-host .modal-body,
|
||||
#modal-servers.popout-host .modal-body,
|
||||
#modal-mods.popout-host .modal-body,
|
||||
#modal-record.popout-host .modal-body{display:flex;flex-direction:column}
|
||||
.popout-host .console-out,
|
||||
#modal-logs.popout-host .crash-voll,
|
||||
.popout-host .mods-list,
|
||||
.popout-host .record-stage{flex:1 1 auto;height:auto;max-height:none;min-height:0}
|
||||
/* Screenshots/Aufzeichnungen NICHT auf die volle Fensterhöhe zwingen (Grid-
|
||||
Zeilen strecken sich sonst nicht mit, es bliebe bei wenigen Einträgen ein
|
||||
großer sichtbar leerer Kasten stehen) - nur die feste 460px-Deckelung
|
||||
aufheben, damit bei VIELEN Einträgen trotzdem mehr Platz genutzt wird */
|
||||
.popout-host .screens-grid{max-height:none}
|
||||
/* Mods-Fenster: die aktive Ansicht (Katalog/Installiert) muss selbst ein
|
||||
Flex-Container werden, damit ihre eigene Liste (.mods-list) die Regel oben
|
||||
nutzen und den restlichen Platz füllen kann */
|
||||
#modal-mods.popout-host #m-pane-catalog,
|
||||
#modal-mods.popout-host #m-pane-installed{display:flex;flex-direction:column;flex:1 1 auto;min-height:0}
|
||||
.popout-host .modal-head{-webkit-app-region:drag;cursor:default}
|
||||
.popout-host .modal-head button,.popout-host .modal-head input,.popout-host .modal-head select{-webkit-app-region:no-drag}
|
||||
|
||||
/* Formular */
|
||||
.field{display:flex;flex-direction:column;gap:5px;margin-bottom:12px}
|
||||
.field>span{font-size:12px;color:#333}
|
||||
@@ -185,7 +244,8 @@ body{display:grid;grid-template-rows:auto 1fr auto;height:100vh;overflow:hidden}
|
||||
.backup-select-field{margin-top:14px;margin-bottom:0}
|
||||
.backup-select-field select{width:100%}
|
||||
.hint{font-size:12px}
|
||||
.path-row{display:flex;gap:8px}.path-row input{flex:1}
|
||||
.path-row{display:flex;gap:8px}
|
||||
.path-row input{flex:1;padding:6px 8px;border:1px solid var(--border);border-radius:3px;background:#fff;color:var(--text);font-family:inherit;font-size:13px}
|
||||
.advanced{border:1px solid var(--border);border-radius:3px;padding:12px 14px 2px;margin-top:4px;background:#fbfbfb}
|
||||
.advanced legend{color:var(--dim);font-size:12px;padding:0 6px}
|
||||
.version-row{display:flex;gap:8px}.version-row #f-version-type{max-width:120px}.version-row #f-version{flex:1}
|
||||
@@ -275,6 +335,8 @@ body{display:grid;grid-template-rows:auto 1fr auto;height:100vh;overflow:hidden}
|
||||
/* Die gerenderte Figur soll das Feld ausfüllen; eine rohe 64x64-Textur wird
|
||||
ohne Weichzeichnen vergrößert, damit sie nicht verschwommen wirkt. */
|
||||
.skin-bild img{max-width:100%;max-height:100%;image-rendering:pixelated}
|
||||
.skin-bild canvas{max-width:100%;max-height:100%;cursor:grab}
|
||||
.skin-bild canvas:active{cursor:grabbing}
|
||||
.crash-details{margin-top:12px}
|
||||
.crash-details summary{cursor:pointer;font-size:12px;color:var(--dim)}
|
||||
.crash-voll{max-height:320px;overflow:auto;margin-top:8px;padding:10px;background:rgba(0,0,0,.25);border-radius:4px;font-family:var(--mono);font-size:11px;line-height:1.45;white-space:pre-wrap;word-break:break-word}
|
||||
@@ -288,6 +350,8 @@ body{display:grid;grid-template-rows:auto 1fr auto;height:100vh;overflow:hidden}
|
||||
|
||||
/* MultiMC-Import */
|
||||
.mmc-bar{display:flex;gap:18px;align-items:center;flex-wrap:wrap;margin-bottom:10px}
|
||||
.mmc-bar select{padding:6px 8px;border:1px solid var(--border);border-radius:3px;background:var(--input,#fff);color:var(--text);font-family:inherit;font-size:12px}
|
||||
.mmc-bar select:focus{outline:none;border-color:var(--accent)}
|
||||
.mmc-item{display:flex;gap:10px;align-items:center;padding:7px 8px;border-radius:3px}
|
||||
.mmc-item+.mmc-item{border-top:1px solid var(--border2)}
|
||||
.mmc-item:hover{background:rgba(128,128,128,.10)}
|
||||
@@ -328,6 +392,131 @@ img.account-av{object-fit:cover;image-rendering:auto}
|
||||
.screens-toolbar{display:flex;align-items:center;gap:8px;margin-bottom:10px;flex-wrap:wrap}
|
||||
.screens-toolbar .hint{font-size:11px}
|
||||
.screens-empty{color:var(--dim);text-align:center;padding:30px;grid-column:1/-1}
|
||||
.record-thumb-img-wrap{position:relative}
|
||||
.record-thumb-img-wrap img{background:#000}
|
||||
.record-thumb-play{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#fff;font-size:22px;text-shadow:0 1px 4px rgba(0,0,0,.7);pointer-events:none}
|
||||
.record-thumb-size{font-size:10px;color:var(--dim);padding:0 6px 4px}
|
||||
|
||||
/* Freunde */
|
||||
.tb-badge{display:inline-block;min-width:16px;height:16px;padding:0 4px;margin-left:6px;border-radius:8px;background:#d9453c;color:#fff;font-size:11px;font-weight:600;line-height:16px;text-align:center;box-sizing:border-box}
|
||||
#modal-friends .field-row{margin-bottom:14px}
|
||||
#modal-friends .field-row input{flex:1}
|
||||
/* ---- Spielzeit-Statistik: Kennzahlen + Balken pro Instanz ---- */
|
||||
:root{--stat-bar:#3a6ea5}
|
||||
:root[data-theme="dark"]{--stat-bar:#3d8fe0}
|
||||
.stats-tiles{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:6px}
|
||||
.stat-tile{border:1px solid var(--border);border-radius:4px;padding:8px 10px;min-width:0}
|
||||
.stat-tile .stat-label{font-size:11px;color:var(--dim)}
|
||||
.stat-tile .stat-value{font-size:18px;font-weight:600;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.stat-tile .stat-sub{font-size:11px;color:var(--dim);line-height:1.35}
|
||||
.stats-list{display:flex;flex-direction:column;gap:2px}
|
||||
.stat-row{display:grid;grid-template-columns:minmax(0,38%) 1fr auto;align-items:center;gap:10px;padding:5px 4px;border-radius:3px}
|
||||
.stat-row:hover{background:var(--sel)}
|
||||
.stat-name{min-width:0}
|
||||
.stat-name b{display:block;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.stat-name span{font-size:11px;color:var(--dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block}
|
||||
.stat-track{height:8px}
|
||||
.stat-bar{height:8px;background:var(--stat-bar);border-radius:0 4px 4px 0;min-width:2px}
|
||||
.stat-value-cell{font-variant-numeric:tabular-nums;white-space:nowrap;text-align:right;min-width:92px}
|
||||
.popout-host .stats-list{max-height:none}
|
||||
|
||||
/* Clip-Modus-Leiste im Aufzeichnungsfenster */
|
||||
.clip-bar{display:flex;align-items:center;gap:14px;flex-wrap:wrap;padding:8px 12px;border:1px solid var(--border);border-radius:4px;background:var(--area)}
|
||||
.clip-bar.aktiv{border-color:var(--accent);box-shadow:inset 3px 0 0 var(--accent)}
|
||||
.clip-switch{display:inline-flex;align-items:center;gap:8px;cursor:pointer;font-size:13px;font-weight:600;user-select:none}
|
||||
.clip-switch input{position:absolute;opacity:0;width:1px;height:1px;pointer-events:none}
|
||||
.clip-switch-track{position:relative;width:32px;height:18px;border-radius:9px;background:var(--border);transition:background .15s}
|
||||
.clip-switch-knob{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.35);transition:left .15s}
|
||||
.clip-switch input:checked + .clip-switch-track{background:var(--accent)}
|
||||
.clip-switch input:checked + .clip-switch-track .clip-switch-knob{left:16px}
|
||||
.clip-switch input:focus-visible + .clip-switch-track{outline:2px solid var(--accent);outline-offset:2px}
|
||||
.clip-length{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--dim)}
|
||||
.clip-length select{padding:3px 6px;border:1px solid var(--border);border-radius:3px;background:var(--input,#fff);color:var(--text);font-family:inherit;font-size:12px}
|
||||
.clip-length select:disabled{opacity:.6}
|
||||
.clip-status{flex:1;min-width:200px;font-size:12px;color:var(--dim)}
|
||||
.clip-bar.aktiv .clip-status{color:var(--text)}
|
||||
.clip-bar.aktiv .clip-status::before{content:"";display:inline-block;width:8px;height:8px;margin-right:7px;border-radius:50%;background:var(--danger);vertical-align:1px;animation:rec-pulse 1.6s infinite}
|
||||
|
||||
/* ---- Absturz-Assistent ---- */
|
||||
.crash-hilfe{display:flex;flex-direction:column;gap:8px;margin-bottom:12px}
|
||||
.crash-hilfe-titel{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.03em}
|
||||
.crash-fund{display:flex;gap:12px;align-items:flex-start;padding:10px 12px;border:1px solid var(--border);border-left:3px solid var(--accent);border-radius:4px;background:var(--area)}
|
||||
.crash-fund-text{flex:1;min-width:0}
|
||||
.crash-fund-text b{display:block;font-size:13px;margin-bottom:3px}
|
||||
.crash-fund-text span{font-size:12px;color:var(--dim);line-height:1.45}
|
||||
.crash-fund .btn{flex:0 0 auto;align-self:center}
|
||||
.crash-fund.erledigt{border-left-color:var(--play)}
|
||||
.crash-unklar{padding:10px 12px;border:1px dashed var(--border);border-radius:4px;font-size:12px;color:var(--dim)}
|
||||
|
||||
/* ---- Instanz teilen ---- */
|
||||
.share-code-box{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 16px;border:1px solid var(--accent);border-radius:4px;background:var(--area);margin:10px 0}
|
||||
.share-code{font-family:var(--mono);font-size:24px;font-weight:600;letter-spacing:.12em;user-select:all}
|
||||
.share-warn{color:var(--danger)}
|
||||
.share-code-input{text-transform:uppercase;letter-spacing:.1em;font-family:var(--mono)}
|
||||
.share-preview{margin-top:12px;padding:12px 14px;border:1px solid var(--border);border-radius:4px;background:var(--area)}
|
||||
.share-preview b{font-size:14px}
|
||||
.share-preview .hint{margin:4px 0 0}
|
||||
|
||||
/* ---- Kurznachrichten ---- */
|
||||
.friends-chat{display:flex;flex-direction:column;gap:8px;height:100%;min-height:340px}
|
||||
.chat-head{display:flex;align-items:center;gap:10px}
|
||||
.chat-head b{font-size:13px}
|
||||
.chat-verlauf{flex:1;min-height:200px;max-height:300px;overflow-y:auto;display:flex;flex-direction:column;gap:6px;padding:8px;border:1px solid var(--border);border-radius:4px;background:var(--area)}
|
||||
.popout-host .chat-verlauf{max-height:none}
|
||||
.chat-leer{margin:auto;font-size:12px;color:var(--dim);text-align:center}
|
||||
.chat-blase{max-width:78%;padding:6px 10px;border-radius:10px;font-size:12.5px;line-height:1.4;word-break:break-word;background:var(--win);border:1px solid var(--border);align-self:flex-start}
|
||||
.chat-blase.ich{align-self:flex-end;background:var(--accent);border-color:var(--accent);color:#fff}
|
||||
.chat-zeit{display:block;font-size:10px;opacity:.65;margin-top:2px}
|
||||
.chat-schnell{display:flex;flex-wrap:wrap;gap:6px}
|
||||
.chat-btn{position:relative}
|
||||
.chat-badge{position:absolute;top:-6px;right:-6px;min-width:16px;height:16px;padding:0 4px;border-radius:8px;background:#e5484d;color:#fff;font-size:10px;font-weight:600;line-height:16px;text-align:center}
|
||||
.report-liste{margin:6px 0 10px;padding-left:20px;font-size:12px;line-height:1.6}
|
||||
|
||||
/* ---- Auswahl-Dialog ---- */
|
||||
.wahl-liste{display:flex;flex-direction:column;gap:4px;max-height:320px;overflow-y:auto}
|
||||
.popout-host .wahl-liste{max-height:none}
|
||||
.wahl-option{display:flex;align-items:center;gap:10px;width:100%;padding:7px 10px;border:1px solid var(--border);border-radius:4px;background:var(--area);color:var(--text);font-family:inherit;text-align:left;cursor:pointer}
|
||||
.wahl-option:hover{border-color:var(--accent)}
|
||||
.wahl-option.an{border-color:var(--accent);background:var(--sel)}
|
||||
.wahl-option:disabled{opacity:.5;cursor:default}
|
||||
.wahl-bild{width:26px;height:26px;flex:0 0 auto}
|
||||
.wahl-label{display:flex;flex-direction:column;min-width:0}
|
||||
.wahl-label b{font-size:13px;font-weight:600}
|
||||
.wahl-label small{font-size:11px;color:var(--dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
|
||||
/* ---- Schnellstart-Taste auf der Kachel ---- */
|
||||
.card{position:relative}
|
||||
.card-taste{position:absolute;top:4px;right:4px;padding:1px 5px;border-radius:3px;background:var(--accent);color:#fff;font-size:10px;font-weight:600;line-height:14px}
|
||||
|
||||
/* ---- Bilder im Chat ---- */
|
||||
.chat-bild{display:block;max-width:100%;max-height:180px;border-radius:6px;margin-bottom:3px;cursor:zoom-in}
|
||||
.screen-thumb .screen-teilen{right:32px;background:var(--accent)}
|
||||
|
||||
.friends-section-title{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.03em;margin:10px 0 6px}
|
||||
.friend-row{display:flex;align-items:center;gap:10px;padding:6px 4px;border-radius:3px}
|
||||
.friend-row:hover{background:var(--sel,rgba(0,0,0,.04))}
|
||||
.friend-dot{width:9px;height:9px;border-radius:50%;background:#8a8a8a;flex:0 0 auto}
|
||||
.friend-dot.online{background:#3bb54a}
|
||||
.friend-avatar{width:28px;height:28px;border-radius:4px;flex:0 0 auto;background:#1e1e1e;object-fit:contain}
|
||||
.friend-info{flex:1;min-width:0}
|
||||
.friend-name{font-size:13px}
|
||||
.friend-status{font-size:11px;color:var(--dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.friends-empty{color:var(--dim);font-size:12px;padding:8px 4px}
|
||||
|
||||
/* Spielaufzeichnung */
|
||||
.record-search{display:flex;flex-direction:column;align-items:center;gap:10px;padding:48px 0;color:var(--dim)}
|
||||
.record-stage{display:flex;flex-direction:column;gap:10px}
|
||||
.record-video{width:100%;flex:1 1 auto;min-height:360px;background:#000;border-radius:4px;object-fit:contain}
|
||||
.record-toolbar{display:flex;flex-direction:column;align-items:flex-end;gap:6px}
|
||||
.record-live-badge{display:flex;align-items:center;gap:8px;color:var(--danger);font-size:13px;font-weight:600}
|
||||
#record-timer{font-variant-numeric:tabular-nums}
|
||||
.rec-dot{width:10px;height:10px;border-radius:50%;background:var(--danger);animation:rec-pulse 1s infinite}
|
||||
@keyframes rec-pulse{0%,100%{opacity:1}50%{opacity:.25}}
|
||||
.record-audio-bar{display:flex;align-items:center;gap:10px;padding:2px 2px}
|
||||
.record-mic-list{display:flex;flex-direction:column;gap:8px;margin-top:8px}
|
||||
.record-source-row{display:flex;align-items:center;gap:10px;margin-bottom:8px}
|
||||
.record-source-row .checkbox-field{margin-bottom:0;flex:1;min-width:0;font-size:13px;gap:6px}
|
||||
.record-vol{width:110px}
|
||||
|
||||
/* Konsole (dunkel wie ein Log) */
|
||||
.console-out{height:440px;overflow:auto;margin:0;background:#1e1e1e;border:1px solid var(--border);border-radius:3px;padding:10px;font-family:var(--mono);font-size:12px;color:#d4d4d4;white-space:pre-wrap;word-break:break-word;line-height:1.5}
|
||||
@@ -364,6 +553,7 @@ img.account-av{object-fit:cover;image-rendering:auto}
|
||||
.info-card{max-width:760px}
|
||||
.info-actions{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin:18px 0 0;padding-top:14px;border-top:1px solid var(--border2)}
|
||||
.info-status{font-size:12px;color:var(--dim)}
|
||||
.modal-sep{border-top:1px solid var(--border2)}
|
||||
.info-meta{margin-top:10px}
|
||||
.info-meta .settings-kv-row{align-items:center}
|
||||
.info-meta .settings-kv-label{text-transform:uppercase;letter-spacing:.04em;font-size:11px}
|
||||
@@ -401,6 +591,13 @@ img.account-av{object-fit:cover;image-rendering:auto}
|
||||
.path-row{flex-direction:column}
|
||||
}
|
||||
|
||||
/* Popup-Fenster sind eigenständige, meist schmalere Fenster -- die
|
||||
Zeilenumbruch-Regel oben ist für die (deutlich breitere) Haupt-App gedacht
|
||||
und soll hier nicht greifen (sonst z. B. Version/Icon im Instanz-Fenster
|
||||
untereinander statt nebeneinander) */
|
||||
.is-popout .field-row{flex-direction:row}
|
||||
.is-popout .path-row{flex-direction:row}
|
||||
|
||||
/* Konto-Button mit 3D-Skin-Kopf */
|
||||
.acct-btn{gap:8px}
|
||||
.acct-av{display:inline-flex;align-items:center;justify-content:center}
|
||||
@@ -453,3 +650,57 @@ img.account-av{object-fit:cover;image-rendering:auto}
|
||||
::-webkit-scrollbar{width:14px;height:14px}
|
||||
::-webkit-scrollbar-thumb{background:#c1c1c1;border:3px solid var(--area);border-radius:7px}
|
||||
::-webkit-scrollbar-thumb:hover{background:#a8a8a8}
|
||||
|
||||
/* ===================== Skin-Editor ===================== */
|
||||
#modal-skineditor .se-karte{width:1100px;max-width:calc(100vw - 32px);height:760px;max-height:calc(100vh - 40px)}
|
||||
.se-modus{display:inline-flex;border:1px solid var(--border);border-radius:4px;overflow:hidden;background:var(--bar)}
|
||||
.se-modus-btn{padding:5px 14px;border:none;background:none;color:var(--dim);font:inherit;font-size:12px;cursor:pointer}
|
||||
.se-modus-btn+.se-modus-btn{border-left:1px solid var(--border)}
|
||||
.se-modus-btn:hover{color:var(--text)}
|
||||
.se-modus-btn.on{background:var(--accent);color:#fff;font-weight:600}
|
||||
.se-body{display:grid;grid-template-columns:200px minmax(0,1fr) 270px;gap:14px;flex:1 1 auto;min-height:0;overflow:hidden;padding:12px 14px}
|
||||
.se-werkzeuge{display:flex;flex-direction:column;gap:14px;overflow:auto;min-height:0;padding-right:2px}
|
||||
.se-gruppe{display:flex;flex-direction:column;gap:6px}
|
||||
.se-titel{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--dim)}
|
||||
.se-tools{display:grid;grid-template-columns:1fr 1fr;gap:4px}
|
||||
.se-tool{display:flex;flex-direction:column;align-items:center;gap:3px;padding:7px 4px;border:1px solid var(--border);border-radius:4px;background:var(--bar);color:var(--text);font:inherit;font-size:11px;cursor:pointer}
|
||||
.se-tool svg{width:18px;height:18px}
|
||||
.se-tool:hover:not(:disabled){border-color:var(--sel-br)}
|
||||
.se-tool.on{background:var(--sel);border-color:var(--sel-br);font-weight:600}
|
||||
.se-tool:disabled{opacity:.4;cursor:default}
|
||||
.se-farbzeile{display:flex;gap:6px;align-items:center}
|
||||
.se-farbzeile input[type=color]{width:42px;height:30px;padding:0 2px;border:1px solid var(--border);border-radius:4px;background:var(--bar);cursor:pointer}
|
||||
.se-farbzeile input[type=text]{flex:1;min-width:0;height:30px;padding:0 8px;border:1px solid var(--border);border-radius:4px;font-family:var(--mono);font-size:12px;background:#fff;color:var(--text)}
|
||||
:root[data-theme="dark"] .se-farbzeile input[type=text]{background:#1e1e1e}
|
||||
.se-zuletzt{display:grid;grid-template-columns:repeat(8,1fr);gap:3px}
|
||||
.se-farbfeld{aspect-ratio:1;border:1px solid rgba(0,0,0,.25);border-radius:3px;cursor:pointer;padding:0}
|
||||
.se-farbfeld:hover{transform:scale(1.12)}
|
||||
.se-farbfeld.on{outline:2px solid var(--sel-br);outline-offset:1px}
|
||||
.se-ebenen{display:grid;grid-template-columns:1fr 1fr;border:1px solid var(--border);border-radius:4px;overflow:hidden}
|
||||
.se-ebene{padding:6px 4px;border:none;background:var(--bar);color:var(--dim);font:inherit;font-size:12px;cursor:pointer}
|
||||
.se-ebene+.se-ebene{border-left:1px solid var(--border)}
|
||||
.se-ebene.on{background:var(--sel);color:var(--text);font-weight:600}
|
||||
.se-check{display:flex;align-items:center;gap:6px;cursor:pointer}
|
||||
.se-check input{margin:0}
|
||||
.se-check:has(input:disabled){opacity:.5;cursor:default}
|
||||
.se-feld{display:flex;flex-direction:column;gap:3px}
|
||||
.se-feld select{height:28px;border:1px solid var(--border);border-radius:3px;background:#fff;color:var(--text);font:inherit}
|
||||
:root[data-theme="dark"] .se-feld select{background:#1e1e1e}
|
||||
.se-undo{display:grid;grid-template-columns:1fr 1fr;gap:4px}
|
||||
.se-undo .btn{justify-content:center}
|
||||
.se-gruppe>.btn-mini{align-self:flex-start}
|
||||
.se-mitte{display:flex;flex-direction:column;gap:6px;min-width:0;min-height:0}
|
||||
.se-buehne{flex:1 1 auto;min-height:0;position:relative;border:1px solid var(--border);border-radius:6px;background:var(--area);overflow:hidden}
|
||||
.se-info{min-height:16px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.se-seite{display:flex;flex-direction:column;gap:6px;min-height:0}
|
||||
.se-mini{position:relative;height:320px;border:1px solid var(--border);border-radius:6px;background:var(--area);overflow:hidden}
|
||||
.se-tipp{margin:4px 0 0;line-height:1.45}
|
||||
.se-ansicht{position:absolute;inset:0;display:flex;align-items:center;justify-content:center}
|
||||
.se-ansicht canvas{display:block}
|
||||
.se-2d canvas{cursor:crosshair;border-radius:2px}
|
||||
.se-3d canvas{cursor:grab}
|
||||
.se-3d canvas:active{cursor:grabbing}
|
||||
.se-2d[data-werkzeug="pipette"] canvas{cursor:copy}
|
||||
.se-3d-leer{position:absolute}
|
||||
.se-fuss{align-items:center}
|
||||
.se-status{flex:1;min-width:0;margin-left:6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* technicdiscover.js – Technic-Modpacks direkt über die Technic-Plattform
|
||||
* durchsuchen und installieren, ohne vorher manuell eine Datei herunterladen
|
||||
* zu müssen. Genutzt wird dieselbe öffentliche API wie bei Prism Launcher:
|
||||
*
|
||||
* Suche: GET https://api.technicpack.net/search?build=multimc&q=<term>
|
||||
* Beliebt: GET https://api.technicpack.net/trending?build=multimc
|
||||
* Pack-Infos: GET https://api.technicpack.net/modpack/<slug>?build=multimc
|
||||
* -> entweder "url" (ein fertiges ZIP) oder "solder" (Solder-Server)
|
||||
* Solder: GET <solder>modpack/<slug> – Liste der Builds
|
||||
* GET <solder>modpack/<slug>/<build> – Mods dieser Fassung
|
||||
* { minecraft, forge, mods: [{ name, url, ... }] }
|
||||
*
|
||||
* "build=multimc" ist die Kennung, mit der sich auch andere Drittlauncher
|
||||
* bei der Technic-Plattform melden – kein echter Minecraft-Build, sondern
|
||||
* nur ein Client-Identifier, den die API erwartet.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const forge = require('./forge');
|
||||
const technicimport = require('./technicimport');
|
||||
|
||||
const API_BASE = 'https://api.technicpack.net/';
|
||||
const API_BUILD = 'multimc';
|
||||
const UA = 'AeroMC-Launcher/1.0';
|
||||
|
||||
async function getJson(url) {
|
||||
const res = await fetch(url, { headers: { 'User-Agent': UA, Accept: 'application/json' } });
|
||||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function getBuffer(url) {
|
||||
const res = await fetch(url, { headers: { 'User-Agent': UA } });
|
||||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||
return Buffer.from(await res.arrayBuffer());
|
||||
}
|
||||
|
||||
/*
|
||||
* query leer -> beliebte Packs (trending), sonst Suchtreffer.
|
||||
* -> [{ slug, name, iconUrl }]
|
||||
*/
|
||||
async function search(query) {
|
||||
const q = String(query || '').trim();
|
||||
const url = q
|
||||
? `${API_BASE}search?build=${API_BUILD}&q=${encodeURIComponent(q)}`
|
||||
: `${API_BASE}trending?build=${API_BUILD}`;
|
||||
const data = await getJson(url);
|
||||
const packs = Array.isArray(data.modpacks) ? data.modpacks : [];
|
||||
return packs
|
||||
.filter((p) => p && p.slug && p.slug !== 'vanilla')
|
||||
.map((p) => ({ slug: p.slug, name: p.name || p.slug, iconUrl: p.iconUrl || '' }));
|
||||
}
|
||||
|
||||
/*
|
||||
* Entpackt einen ZIP-Buffer nach destDir.
|
||||
* Enthält der ZIP-Inhalt bereits eine erkennbare Ordnerstruktur (mods/,
|
||||
* config/, …), wird sie 1:1 übernommen. Liegen nur einzelne Dateien direkt
|
||||
* im ZIP-Wurzelverzeichnis (typisch für ältere, einzeln gepackte Solder-
|
||||
* Mods), landen sie stattdessen im mods-Ordner.
|
||||
*/
|
||||
function extractZipBuffer(buf, destDir) {
|
||||
const idx = forge.readZipIndex(buf);
|
||||
const namen = Object.keys(idx);
|
||||
const hatOrdnerStruktur = namen.some((n) => /^(mods|config|resources|resourcepacks|scripts|structures|bin)\//i.test(n));
|
||||
for (const name of namen) {
|
||||
if (name.endsWith('/')) continue; // reines Verzeichnis – wird beim Schreiben automatisch angelegt
|
||||
const relPfad = hatOrdnerStruktur ? name : path.posix.join('mods', name);
|
||||
const zielPfad = path.join(destDir, ...relPfad.split('/'));
|
||||
if (!zielPfad.startsWith(destDir + path.sep)) continue; // Pfad-Ausbruch verhindern
|
||||
fs.mkdirSync(path.dirname(zielPfad), { recursive: true });
|
||||
fs.writeFileSync(zielPfad, forge.readZipEntry(buf, idx[name]));
|
||||
}
|
||||
}
|
||||
|
||||
// Solder-Basis-URLs enden mal mit "/api/", mal ohne abschließenden Slash
|
||||
function solderUrl(basis, pfad) {
|
||||
const b = String(basis || '').replace(/\/+$/, '');
|
||||
return `${b}/${pfad}`;
|
||||
}
|
||||
|
||||
async function installSolderBuild(solderBasis, slug, gameDir, onProgress) {
|
||||
const info = await getJson(solderUrl(solderBasis, 'modpack/' + encodeURIComponent(slug)));
|
||||
const build = info.recommended || info.latest || (info.builds || [])[0];
|
||||
if (!build) throw new Error('Keine Fassung (Build) für dieses Modpack gefunden.');
|
||||
const daten = await getJson(solderUrl(solderBasis, `modpack/${encodeURIComponent(slug)}/${encodeURIComponent(build)}`));
|
||||
const mods = Array.isArray(daten.mods) ? daten.mods : [];
|
||||
|
||||
for (let i = 0; i < mods.length; i++) {
|
||||
const mod = mods[i];
|
||||
if (onProgress) onProgress({ current: i + 1, total: mods.length, name: mod.name || '' });
|
||||
if (!mod || !mod.url) continue;
|
||||
try {
|
||||
const buf = await getBuffer(mod.url);
|
||||
extractZipBuffer(buf, gameDir);
|
||||
} catch { /* einzelner Mod fehlgeschlagen – Rest trotzdem weiter versuchen */ }
|
||||
}
|
||||
|
||||
let loader = 'vanilla';
|
||||
const loaderVersion = daten.forge || '';
|
||||
if (loaderVersion) loader = 'forge';
|
||||
return { version: daten.minecraft || '', loader, loaderVersion };
|
||||
}
|
||||
|
||||
async function installSingleZip(url, gameDir) {
|
||||
const buf = await getBuffer(url);
|
||||
// in einen Temp-Ordner entpacken, damit wir wie beim Ordner-Import
|
||||
// (technicimport.js) Version/Loader aus bin/modpack.jar erkennen können,
|
||||
// bevor die Dateien an ihren endgültigen Platz kommen
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'technic-dl-'));
|
||||
try {
|
||||
extractZipBuffer(buf, tmp);
|
||||
let base = tmp;
|
||||
if (!fs.existsSync(path.join(base, 'bin')) && !fs.existsSync(path.join(base, 'mods'))) {
|
||||
// manche Packs haben einen umschließenden Ordner im ZIP
|
||||
const sub = fs.readdirSync(tmp).map((n) => path.join(tmp, n))
|
||||
.find((p) => { try { return fs.statSync(p).isDirectory(); } catch { return false; } });
|
||||
if (sub) base = sub;
|
||||
}
|
||||
const { version, loader, loaderVersion } = technicimport.detectVersion(base);
|
||||
fs.cpSync(base, gameDir, { recursive: true });
|
||||
return { version, loader, loaderVersion };
|
||||
} finally {
|
||||
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch { /* egal */ }
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Lädt ein Modpack von der Technic-Plattform und legt daraus eine neue
|
||||
* Instanz an. onProgress(info) wird bei Solder-Packs pro Mod aufgerufen.
|
||||
* -> { ok, instance } oder { ok: false, message }
|
||||
*/
|
||||
async function installFromPlatform(store, slug, onProgress) {
|
||||
let created = null;
|
||||
try {
|
||||
const detail = await getJson(`${API_BASE}modpack/${encodeURIComponent(slug)}?build=${API_BUILD}`);
|
||||
// die Technic-API antwortet bei unbekanntem Slug mit HTTP 200 und
|
||||
// {"error": "..."} statt einem Fehlerstatus – daher explizit prüfen
|
||||
if (detail.error) throw new Error(detail.error);
|
||||
|
||||
created = store.createOrReplaceImportedInstance({
|
||||
name: detail.displayName || detail.name || slug,
|
||||
notes: detail.description || '',
|
||||
});
|
||||
const gameDir = store.gameDir(created.id);
|
||||
fs.mkdirSync(gameDir, { recursive: true });
|
||||
|
||||
let ergebnis;
|
||||
if (detail.solder) {
|
||||
ergebnis = await installSolderBuild(detail.solder, slug, gameDir, onProgress);
|
||||
} else if (detail.url) {
|
||||
ergebnis = await installSingleZip(detail.url, gameDir);
|
||||
} else {
|
||||
throw new Error('Für dieses Modpack ist kein Download hinterlegt.');
|
||||
}
|
||||
store.updateInstance(created.id, {
|
||||
minecraft: {
|
||||
version: ergebnis.version || detail.minecraft || '',
|
||||
loader: ergebnis.loader || 'vanilla',
|
||||
loaderVersion: ergebnis.loaderVersion || '',
|
||||
},
|
||||
});
|
||||
return { ok: true, instance: store.getInstance(created.id) };
|
||||
} catch (err) {
|
||||
// fehlgeschlagene Installation nicht als leere Karteileiche stehen lassen
|
||||
if (created) { try { store.deleteInstance(created.id); } catch { /* egal */ } }
|
||||
return { ok: false, message: 'Installation fehlgeschlagen: ' + err.message };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { search, installFromPlatform };
|
||||
@@ -0,0 +1,172 @@
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* technicimport.js – Instanzen aus dem Technic Launcher übernehmen
|
||||
* -------------------------------------------------------------------
|
||||
* Der Technic Launcher legt jedes installierte Modpack als eigenen Ordner
|
||||
* unter "<Technic-Wurzel>/modpacks/<name>" an (Standard unter Windows:
|
||||
* %APPDATA%\.technic\modpacks). Darin liegen "bin" (Launcher-interner
|
||||
* Jar-/Versions-Cache), "config" und "mods" als Geschwisterordner – Welten,
|
||||
* Ressourcenpakete usw. landen ebenfalls direkt im Pack-Ordner, genau wie
|
||||
* bei einem klassischen .minecraft.
|
||||
*
|
||||
* Es gibt keine für uns nutzbare, stabil dokumentierte JSON-Datei mit einem
|
||||
* schönen Anzeigenamen – "installedPacks" im Wurzelordner listet Packs nur
|
||||
* über ihren Ordnernamen (Slug). Wir lesen sie deshalb nur als optionale,
|
||||
* fehlertolerante Ergänzung; die eigentliche Erkennung läuft – wie bei den
|
||||
* anderen Importern – über die tatsächlich vorhandenen Ordner/Dateien.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const forge = require('./forge');
|
||||
|
||||
// Ordner, die zum Technic-internen Verwaltungs-/Jar-Cache gehören und beim
|
||||
// Übernehmen NICHT mit ins Spielverzeichnis der neuen Instanz sollen.
|
||||
const SKIP_DIRS = new Set(['bin']);
|
||||
|
||||
function technicRoot() {
|
||||
return path.join(process.env.APPDATA || '', '.technic');
|
||||
}
|
||||
|
||||
function readJson(file) {
|
||||
try { return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^/, '')); }
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
// findet den modpacks-Ordner, egal ob Technic-Wurzel, modpacks-Ordner direkt
|
||||
// oder ein einzelner Pack-Ordner angegeben wurde
|
||||
function resolveModpacksDir(inputPath) {
|
||||
const root = inputPath && inputPath.trim() ? inputPath.trim() : technicRoot();
|
||||
if (!fs.existsSync(root)) return null;
|
||||
|
||||
const direct = path.join(root, 'modpacks');
|
||||
if (fs.existsSync(direct)) return direct;
|
||||
if (path.basename(root).toLowerCase() === 'modpacks') return root;
|
||||
return root; // z. B. ein selbst gewählter Ordner mit Pack-Unterordnern
|
||||
}
|
||||
|
||||
// ein Pack-Ordner hat i. d. R. bin/, config/ und/oder mods/ als Geschwister
|
||||
function looksLikePack(dir) {
|
||||
const markers = ['bin', 'mods', 'config', 'saves', 'resourcepacks', 'options.txt'];
|
||||
return markers.some((name) => fs.existsSync(path.join(dir, name)));
|
||||
}
|
||||
|
||||
// optionale Anreicherung aus der "installedPacks"-Datei der Technic-Wurzel;
|
||||
// bewusst rein additiv und fehlertolerant, da das Format nicht offiziell
|
||||
// als stabile API dokumentiert ist
|
||||
function readInstalledPacksMeta(root) {
|
||||
const data = readJson(path.join(root, 'installedPacks'));
|
||||
const packs = data && data.installedPacks;
|
||||
return packs && typeof packs === 'object' ? packs : {};
|
||||
}
|
||||
|
||||
// versucht, aus bin/modpack.jar (version.json) oder der alten bin/version
|
||||
// -Textdatei Minecraft-Version und Mod-Loader zu bestimmen. Rein
|
||||
// bestmöglich – schlägt die Erkennung fehl, bleibt die Instanz einfach
|
||||
// ohne Version/Loader stehen und lässt sich manuell nachtragen.
|
||||
function detectVersion(packDir) {
|
||||
const bin = path.join(packDir, 'bin');
|
||||
const jarFile = path.join(bin, 'modpack.jar');
|
||||
if (fs.existsSync(jarFile)) {
|
||||
try {
|
||||
const buf = fs.readFileSync(jarFile);
|
||||
const idx = forge.readZipIndex(buf);
|
||||
const entry = idx['version.json'];
|
||||
if (entry) {
|
||||
// manche Jars speichern JSON mit BOM – ohne Entfernen bricht JSON.parse
|
||||
const text = forge.readZipEntry(buf, entry).toString('utf8').replace(/^/, '');
|
||||
const profile = JSON.parse(text);
|
||||
const libs = Array.isArray(profile.libraries) ? profile.libraries : [];
|
||||
let loader = 'vanilla';
|
||||
let loaderVersion = '';
|
||||
for (const lib of libs) {
|
||||
const name = String((lib && lib.name) || '');
|
||||
if (/net\.minecraftforge:forge:/i.test(name)) {
|
||||
loader = 'forge';
|
||||
loaderVersion = (name.split(':')[2] || '').split('-').pop() || '';
|
||||
} else if (/net\.neoforged:neoforge:/i.test(name) && loader === 'vanilla') {
|
||||
loader = 'neoforge';
|
||||
loaderVersion = name.split(':')[2] || '';
|
||||
}
|
||||
}
|
||||
const id = String(profile.id || profile.inheritsFrom || '');
|
||||
const version = String(profile.inheritsFrom || id.split('-')[0] || '').trim();
|
||||
if (version) return { version, loader, loaderVersion };
|
||||
}
|
||||
} catch { /* modpack.jar nicht lesbar/kein gültiges ZIP – weiter unten Fallback */ }
|
||||
}
|
||||
// ältere Packs hinterlegen nur eine reine Textdatei mit der MC-Version
|
||||
const legacy = path.join(bin, 'version');
|
||||
try {
|
||||
if (fs.existsSync(legacy)) {
|
||||
const version = fs.readFileSync(legacy, 'utf8').trim();
|
||||
if (version) return { version, loader: 'vanilla', loaderVersion: '' };
|
||||
}
|
||||
} catch { /* egal, bleibt leer */ }
|
||||
return { version: '', loader: 'vanilla', loaderVersion: '' };
|
||||
}
|
||||
|
||||
function scan(inputPath) {
|
||||
const modpacksDir = resolveModpacksDir(inputPath);
|
||||
if (!modpacksDir || !fs.existsSync(modpacksDir)) return { ok: false, reason: 'not-found' };
|
||||
|
||||
const root = path.basename(modpacksDir).toLowerCase() === 'modpacks'
|
||||
? path.dirname(modpacksDir) : modpacksDir;
|
||||
const meta = readInstalledPacksMeta(root);
|
||||
|
||||
const list = [];
|
||||
for (const entry of fs.readdirSync(modpacksDir, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const dir = path.join(modpacksDir, entry.name);
|
||||
if (!looksLikePack(dir)) continue;
|
||||
|
||||
const { version, loader, loaderVersion } = detectVersion(dir);
|
||||
const info = meta[entry.name] || {};
|
||||
list.push({
|
||||
folder: entry.name,
|
||||
dir,
|
||||
name: info.displayName || entry.name,
|
||||
group: '',
|
||||
notes: '',
|
||||
version,
|
||||
loader,
|
||||
loaderVersion,
|
||||
gameDir: dir,
|
||||
hasGameData: true,
|
||||
});
|
||||
}
|
||||
list.sort((a, b) => a.name.localeCompare(b.name));
|
||||
return { ok: true, instancesDir: modpacksDir, count: list.length, instances: list };
|
||||
}
|
||||
|
||||
// kopiert den Pack-Ordner, lässt aber den Technic-internen bin-Cache weg
|
||||
function copyInto(sourceDir, targetDir) {
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
for (const name of fs.readdirSync(sourceDir)) {
|
||||
if (SKIP_DIRS.has(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,
|
||||
},
|
||||
});
|
||||
|
||||
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, resolveModpacksDir, technicRoot, detectVersion };
|
||||
@@ -42,7 +42,7 @@ function computeInstallKind() {
|
||||
// 2. Notnagel über den Dateinamen, falls die Variablen einmal fehlen
|
||||
if (/portable/i.test(path.basename(exe))) return 'portable';
|
||||
|
||||
// 3. Setup: NSIS legt neben der EXE immer einen Uninstaller ab. Nur wenn der
|
||||
// 3. Setup: das Setup legt neben der EXE immer "Uninstall AeroMC.exe" ab. Nur wenn der
|
||||
// da ist, ist es wirklich eine Installation.
|
||||
if (hasUninstaller(path.dirname(exe))) return 'setup';
|
||||
|
||||
@@ -230,8 +230,17 @@ async function downloadAndInstall(asset, onProgress) {
|
||||
|
||||
const dir = updateDir();
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const dest = path.join(dir, asset.name.replace(/[^\w.\-]/g, '_'));
|
||||
fs.rmSync(dest, { force: true });
|
||||
let dest = path.join(dir, asset.name.replace(/[^\w.\-]/g, '_'));
|
||||
try {
|
||||
fs.rmSync(dest, { force: true });
|
||||
} catch {
|
||||
// Alte Datei ist gesperrt (Virenscanner prüft sie gerade, oder ein vorheriger
|
||||
// Installer-Versuch läuft noch) - dann eben unter neuem Namen laden, statt mit
|
||||
// EPERM abzubrechen. Der Rest wird bei nächster Gelegenheit von
|
||||
// cleanupUpdateCache() aufgeräumt (ab 10 Minuten Alter).
|
||||
const parsed = path.parse(dest);
|
||||
dest = path.join(parsed.dir, `${parsed.name}-${Date.now()}${parsed.ext}`);
|
||||
}
|
||||
|
||||
const res = await fetch(asset.url, { headers: { 'User-Agent': UA } });
|
||||
if (!res.ok) throw new Error('Download fehlgeschlagen: HTTP ' + res.status);
|
||||
@@ -310,7 +319,7 @@ exit /b 1
|
||||
|
||||
/*
|
||||
* Wendet das geladene Update an.
|
||||
* setup -> NSIS-Installer starten, er überschreibt die Installation selbst
|
||||
* setup -> AeroMC-Setup mit --update starten, es überschreibt die Installation selbst
|
||||
* portable -> Skript starten, das die laufende EXE nach dem Beenden ersetzt
|
||||
* Der Launcher muss sich danach beenden.
|
||||
*/
|
||||
@@ -322,7 +331,8 @@ function applyUpdate(file) {
|
||||
const kind = installKind;
|
||||
|
||||
if (kind !== 'portable') {
|
||||
const child = spawn(file.path, [], { detached: true, stdio: 'ignore' });
|
||||
// --update: das AeroMC-Setup aktualisiert ohne Rückfragen und startet AeroMC danach neu
|
||||
const child = spawn(file.path, ['--update'], { detached: true, stdio: 'ignore' });
|
||||
child.unref();
|
||||
return { started: 'installer' };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: AeroMC Freunde
|
||||
* Description: Backend für das Freundesystem des AeroMC Launchers - meldet, wer gerade online/im Spiel ist, verwaltet Freundschaftsanfragen, Einladungen, Kurznachrichten, per Code geteilte Instanzen und Fehlerberichte aus dem Absturz-Assistenten. Anmeldung über Mojangs Sitzungsprüfung (wie bei Minecraft-Servern) - ohne eigenes Passwort.
|
||||
* Version: 1.4.0
|
||||
* Author: M_Viper
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit; // kein direkter Aufruf der Datei
|
||||
}
|
||||
|
||||
define('AEROMC_FRIENDS_VERSION', '1.4.0');
|
||||
define('AEROMC_FRIENDS_PRESENCE_TABLE', 'aeromc_presence');
|
||||
define('AEROMC_FRIENDS_FRIENDS_TABLE', 'aeromc_friends');
|
||||
define('AEROMC_FRIENDS_INVITES_TABLE', 'aeromc_invites');
|
||||
define('AEROMC_FRIENDS_SESSIONS_TABLE', 'aeromc_sessions');
|
||||
define('AEROMC_FRIENDS_SHARES_TABLE', 'aeromc_shares');
|
||||
define('AEROMC_FRIENDS_MESSAGES_TABLE', 'aeromc_messages');
|
||||
define('AEROMC_FRIENDS_REPORTS_TABLE', 'aeromc_reports');
|
||||
|
||||
// Übergangsmodus: false = Anfragen ohne Anmeldung werden noch angenommen, aber
|
||||
// nur für Spieler, die sich noch NIE über die neue Anmeldung gemeldet haben
|
||||
// (ältere Launcher-Versionen funktionieren weiter). Sobald alle Nutzer
|
||||
// aktualisiert haben, in der wp-config.php auf true setzen:
|
||||
// define('AEROMC_FRIENDS_REQUIRE_AUTH', true);
|
||||
if (!defined('AEROMC_FRIENDS_REQUIRE_AUTH')) {
|
||||
define('AEROMC_FRIENDS_REQUIRE_AUTH', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Legt die beiden Tabellen an (bzw. bringt sie bei einem Update auf den
|
||||
* neuesten Stand - dbDelta erkennt bestehende Tabellen/Spalten selbst).
|
||||
*/
|
||||
function aeromc_friends_install() {
|
||||
global $wpdb;
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
$presence_table = $wpdb->prefix . AEROMC_FRIENDS_PRESENCE_TABLE;
|
||||
$friends_table = $wpdb->prefix . AEROMC_FRIENDS_FRIENDS_TABLE;
|
||||
$invites_table = $wpdb->prefix . AEROMC_FRIENDS_INVITES_TABLE;
|
||||
$sessions_table = $wpdb->prefix . AEROMC_FRIENDS_SESSIONS_TABLE;
|
||||
$shares_table = $wpdb->prefix . AEROMC_FRIENDS_SHARES_TABLE;
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||
|
||||
// uuid ohne Bindestriche gespeichert (siehe aeromc_friends_normalize_uuid
|
||||
// in includes/routes.php) - vermeidet doppelte Zeilen durch
|
||||
// unterschiedliche Schreibweisen derselben UUID
|
||||
$sql_presence = "CREATE TABLE $presence_table (
|
||||
uuid varchar(32) NOT NULL,
|
||||
username varchar(32) NOT NULL,
|
||||
instance_name varchar(128) DEFAULT NULL,
|
||||
server_label varchar(128) DEFAULT NULL,
|
||||
mc_version varchar(32) DEFAULT NULL,
|
||||
last_seen datetime NOT NULL,
|
||||
PRIMARY KEY (uuid)
|
||||
) $charset_collate;";
|
||||
dbDelta($sql_presence);
|
||||
|
||||
$sql_friends = "CREATE TABLE $friends_table (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
uuid_a varchar(32) NOT NULL,
|
||||
uuid_b varchar(32) NOT NULL,
|
||||
status varchar(20) NOT NULL DEFAULT 'pending',
|
||||
created_at datetime NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY uuid_a (uuid_a),
|
||||
KEY uuid_b (uuid_b)
|
||||
) $charset_collate;";
|
||||
dbDelta($sql_friends);
|
||||
|
||||
// Einladungen "Komm auf meinen Server" - gelten nur ein paar Minuten
|
||||
$sql_invites = "CREATE TABLE $invites_table (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
from_uuid varchar(32) NOT NULL,
|
||||
to_uuid varchar(32) NOT NULL,
|
||||
server varchar(128) NOT NULL,
|
||||
mc_version varchar(32) DEFAULT NULL,
|
||||
created_at datetime NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY to_uuid (to_uuid)
|
||||
) $charset_collate;";
|
||||
dbDelta($sql_invites);
|
||||
|
||||
// Anmelde-Sitzungen: nur der SHA-256 des Tokens wird gespeichert
|
||||
$sql_sessions = "CREATE TABLE $sessions_table (
|
||||
token_hash char(64) NOT NULL,
|
||||
uuid varchar(32) NOT NULL,
|
||||
username varchar(32) NOT NULL,
|
||||
created_at datetime NOT NULL,
|
||||
last_used datetime NOT NULL,
|
||||
PRIMARY KEY (token_hash),
|
||||
KEY uuid (uuid)
|
||||
) $charset_collate;";
|
||||
dbDelta($sql_sessions);
|
||||
|
||||
// per Code geteilte Instanzen (Mod-Liste, keine Dateien)
|
||||
$sql_shares = "CREATE TABLE $shares_table (
|
||||
code varchar(9) NOT NULL,
|
||||
uuid varchar(32) NOT NULL,
|
||||
manifest longtext NOT NULL,
|
||||
created_at datetime NOT NULL,
|
||||
PRIMARY KEY (code),
|
||||
KEY uuid (uuid)
|
||||
) $charset_collate;";
|
||||
dbDelta($sql_shares);
|
||||
|
||||
// Kurznachrichten zwischen Freunden (werden nach einigen Tagen gelöscht)
|
||||
$messages_table = $wpdb->prefix . AEROMC_FRIENDS_MESSAGES_TABLE;
|
||||
$sql_messages = "CREATE TABLE $messages_table (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
from_uuid varchar(32) NOT NULL,
|
||||
to_uuid varchar(32) NOT NULL,
|
||||
text varchar(400) NOT NULL,
|
||||
image_url varchar(255) DEFAULT NULL,
|
||||
created_at datetime NOT NULL,
|
||||
read_at datetime DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY to_uuid (to_uuid),
|
||||
KEY from_uuid (from_uuid)
|
||||
) $charset_collate;";
|
||||
dbDelta($sql_messages);
|
||||
|
||||
// Fehlerberichte aus dem Absturz-Assistenten (Ansicht: Werkzeuge → AeroMC Fehlerberichte)
|
||||
$reports_table = $wpdb->prefix . AEROMC_FRIENDS_REPORTS_TABLE;
|
||||
$sql_reports = "CREATE TABLE $reports_table (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
created_at datetime NOT NULL,
|
||||
uuid varchar(32) DEFAULT NULL,
|
||||
username varchar(32) DEFAULT NULL,
|
||||
launcher_version varchar(32) DEFAULT NULL,
|
||||
mc_version varchar(32) DEFAULT NULL,
|
||||
loader varchar(32) DEFAULT NULL,
|
||||
loader_version varchar(64) DEFAULT NULL,
|
||||
system varchar(128) DEFAULT NULL,
|
||||
funde text,
|
||||
mods longtext,
|
||||
bericht longtext,
|
||||
kommentar text,
|
||||
status varchar(20) NOT NULL DEFAULT 'neu',
|
||||
PRIMARY KEY (id),
|
||||
KEY status (status)
|
||||
) $charset_collate;";
|
||||
dbDelta($sql_reports);
|
||||
|
||||
update_option('aeromc_friends_db_version', AEROMC_FRIENDS_VERSION);
|
||||
}
|
||||
register_activation_hook(__FILE__, 'aeromc_friends_install');
|
||||
|
||||
// Beim Ersetzen der Plugin-Dateien (Update per Upload) läuft der
|
||||
// Aktivierungs-Hook NICHT erneut - neue Spalten/Tabellen deshalb anlegen,
|
||||
// sobald die gespeicherte DB-Version nicht mehr zur Plugin-Version passt.
|
||||
add_action('plugins_loaded', function () {
|
||||
if (get_option('aeromc_friends_db_version') !== AEROMC_FRIENDS_VERSION) {
|
||||
aeromc_friends_install();
|
||||
}
|
||||
});
|
||||
|
||||
require_once plugin_dir_path(__FILE__) . 'includes/routes.php';
|
||||
if (is_admin()) {
|
||||
require_once plugin_dir_path(__FILE__) . 'includes/admin.php';
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin-Seite "AeroMC Fehlerberichte": Liste, Detailansicht, Löschen.
|
||||
* Nur für Administratoren (manage_options).
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
add_action('admin_menu', function () {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . AEROMC_FRIENDS_REPORTS_TABLE;
|
||||
$neu = (int) $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE status = 'neu'");
|
||||
$titel = 'AeroMC Fehler' . ($neu ? ' <span class="awaiting-mod">' . $neu . '</span>' : '');
|
||||
add_menu_page('AeroMC Fehlerberichte', $titel, 'manage_options', 'aeromc-reports', 'aeromc_friends_reports_page', 'dashicons-sos', 81);
|
||||
});
|
||||
|
||||
function aeromc_friends_reports_page() {
|
||||
global $wpdb;
|
||||
if (!current_user_can('manage_options')) {
|
||||
return;
|
||||
}
|
||||
$table = $wpdb->prefix . AEROMC_FRIENDS_REPORTS_TABLE;
|
||||
$basis = admin_url('admin.php?page=aeromc-reports');
|
||||
|
||||
// Löschen (einzeln oder alle erledigten)
|
||||
if (isset($_POST['aeromc_loeschen']) && check_admin_referer('aeromc_reports')) {
|
||||
$id = (int) $_POST['aeromc_loeschen'];
|
||||
$wpdb->delete($table, ['id' => $id]);
|
||||
echo '<div class="notice notice-success"><p>Bericht #' . esc_html($id) . ' gelöscht.</p></div>';
|
||||
}
|
||||
if (isset($_POST['aeromc_status'], $_POST['aeromc_id']) && check_admin_referer('aeromc_reports')) {
|
||||
$status = in_array($_POST['aeromc_status'], ['neu', 'gelesen', 'erledigt'], true) ? $_POST['aeromc_status'] : 'gelesen';
|
||||
$wpdb->update($table, ['status' => $status], ['id' => (int) $_POST['aeromc_id']]);
|
||||
}
|
||||
|
||||
echo '<div class="wrap"><h1>AeroMC Fehlerberichte</h1>';
|
||||
|
||||
// Detailansicht
|
||||
if (isset($_GET['report'])) {
|
||||
$id = (int) $_GET['report'];
|
||||
$r = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE id = %d", $id));
|
||||
if (!$r) {
|
||||
echo '<p>Bericht nicht gefunden.</p></div>';
|
||||
return;
|
||||
}
|
||||
if ($r->status === 'neu') {
|
||||
$wpdb->update($table, ['status' => 'gelesen'], ['id' => $id]);
|
||||
$r->status = 'gelesen';
|
||||
}
|
||||
echo '<p><a href="' . esc_url($basis) . '">← Zur Liste</a></p>';
|
||||
echo '<h2>Bericht #' . esc_html($r->id) . '</h2>';
|
||||
echo '<table class="widefat striped" style="max-width:900px"><tbody>';
|
||||
$zeile = function ($k, $v) {
|
||||
echo '<tr><th style="width:180px">' . esc_html($k) . '</th><td>' . esc_html($v !== null && $v !== '' ? $v : '–') . '</td></tr>';
|
||||
};
|
||||
$zeile('Eingegangen (UTC)', $r->created_at);
|
||||
$zeile('Spieler', $r->username ? $r->username . ' (' . $r->uuid . ')' : 'anonym');
|
||||
$zeile('Launcher-Version', $r->launcher_version);
|
||||
$zeile('Minecraft', $r->mc_version);
|
||||
$zeile('Loader', trim($r->loader . ' ' . $r->loader_version));
|
||||
$zeile('System', $r->system);
|
||||
$zeile('Status', $r->status);
|
||||
echo '</tbody></table>';
|
||||
|
||||
if ($r->kommentar) {
|
||||
echo '<h3>Kommentar</h3><div style="max-width:900px;padding:10px 14px;background:#fff;border:1px solid #ccd0d4;white-space:pre-wrap">' . esc_html($r->kommentar) . '</div>';
|
||||
}
|
||||
$funde = json_decode((string) $r->funde, true) ?: [];
|
||||
echo '<h3>Erkannte Ursachen (' . count($funde) . ')</h3>';
|
||||
if ($funde) {
|
||||
echo '<ul style="list-style:disc;padding-left:20px">';
|
||||
foreach ($funde as $f) {
|
||||
echo '<li><strong>' . esc_html($f['titel'] ?? '') . '</strong> – ' . esc_html($f['text'] ?? '') . '</li>';
|
||||
}
|
||||
echo '</ul>';
|
||||
} else {
|
||||
echo '<p>Keine – der Assistent konnte die Ursache nicht eindeutig erkennen.</p>';
|
||||
}
|
||||
$mods = json_decode((string) $r->mods, true) ?: [];
|
||||
echo '<h3>Mods (' . count($mods) . ')</h3>';
|
||||
if ($mods) {
|
||||
echo '<table class="widefat striped" style="max-width:900px"><thead><tr><th>Datei</th><th>Name</th><th>Version</th><th>Aktiv</th></tr></thead><tbody>';
|
||||
foreach ($mods as $m) {
|
||||
echo '<tr><td>' . esc_html($m['datei'] ?? '') . '</td><td>' . esc_html($m['name'] ?? '') . '</td><td>'
|
||||
. esc_html($m['version'] ?? '') . '</td><td>' . (!empty($m['aktiv']) ? 'ja' : 'nein') . '</td></tr>';
|
||||
}
|
||||
echo '</tbody></table>';
|
||||
}
|
||||
echo '<h3>Bericht / Protokoll</h3><textarea readonly style="width:100%;max-width:1200px;height:420px;font-family:monospace;font-size:12px">'
|
||||
. esc_textarea($r->bericht) . '</textarea>';
|
||||
|
||||
echo '<form method="post" style="margin-top:14px;display:flex;gap:8px">';
|
||||
wp_nonce_field('aeromc_reports');
|
||||
echo '<input type="hidden" name="aeromc_id" value="' . esc_attr($r->id) . '" />';
|
||||
echo '<button class="button" name="aeromc_status" value="erledigt">Als erledigt markieren</button>';
|
||||
echo '<button class="button" name="aeromc_status" value="neu">Wieder als neu markieren</button>';
|
||||
echo '<button class="button button-link-delete" name="aeromc_loeschen" value="' . esc_attr($r->id) . '" onclick="return confirm(\'Bericht wirklich löschen?\')">Löschen</button>';
|
||||
echo '</form></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Liste
|
||||
$rows = $wpdb->get_results("SELECT id, created_at, username, launcher_version, mc_version, loader, funde, kommentar, status FROM $table ORDER BY id DESC LIMIT 200");
|
||||
if (!$rows) {
|
||||
echo '<p>Noch keine Fehlerberichte eingegangen.</p></div>';
|
||||
return;
|
||||
}
|
||||
echo '<table class="widefat striped"><thead><tr><th>#</th><th>Eingegangen (UTC)</th><th>Spieler</th><th>Launcher</th><th>Minecraft</th><th>Erkannte Ursache</th><th>Kommentar</th><th>Status</th></tr></thead><tbody>';
|
||||
foreach ($rows as $r) {
|
||||
$funde = json_decode((string) $r->funde, true) ?: [];
|
||||
$ursache = $funde ? ($funde[0]['titel'] ?? '') . (count($funde) > 1 ? ' (+' . (count($funde) - 1) . ')' : '') : '–';
|
||||
$fett = $r->status === 'neu' ? 'font-weight:600' : '';
|
||||
echo '<tr style="' . $fett . '"><td><a href="' . esc_url(add_query_arg('report', $r->id, $basis)) . '">#' . esc_html($r->id) . '</a></td>'
|
||||
. '<td>' . esc_html($r->created_at) . '</td>'
|
||||
. '<td>' . esc_html($r->username ?: 'anonym') . '</td>'
|
||||
. '<td>' . esc_html($r->launcher_version) . '</td>'
|
||||
. '<td>' . esc_html(trim($r->mc_version . ' ' . $r->loader)) . '</td>'
|
||||
. '<td>' . esc_html($ursache) . '</td>'
|
||||
. '<td>' . esc_html(mb_strimwidth((string) $r->kommentar, 0, 60, '…')) . '</td>'
|
||||
. '<td>' . esc_html($r->status) . '</td></tr>';
|
||||
}
|
||||
echo '</tbody></table></div>';
|
||||
}
|
||||