Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
972a3e73da | ||
|
|
e83c951256 | ||
|
|
67fe4cba48 | ||
|
|
e5ccf0b4cc | ||
|
|
10ee45cbc2 | ||
|
|
bc68395437 |
@@ -2,6 +2,44 @@
|
|||||||
|
|
||||||
Alle nennenswerten Änderungen an AeroMC werden hier festgehalten.
|
Alle nennenswerten Änderungen an AeroMC werden hier festgehalten.
|
||||||
|
|
||||||
|
## [1.1.7] - 2026-10-03
|
||||||
|
|
||||||
|
### Neu
|
||||||
|
- Umhänge im Skin-Fenster: offiziellen Mojang-Umhang auswählen oder ablegen (Liste zeigt alle Umhänge, die das Konto besitzt) und den eigenen AeroMC-Umhang verwalten – Figur und Skin-Hintergrund zeigen den Umhang mit
|
||||||
|
- Skin-Editor, neuer Modus „Umhang“: eigene Umhänge gestalten, auch in HD bis 1024×512 – mit allen Werkzeugen, Pinselgröße, Spiegeln (linke ↔ rechte Kante), Zoom und Verschieben, Rückgängig auch über Größenwechsel
|
||||||
|
- Umhang auf der 3D-Figur bemalen, wahlweise als Elytra ansehen, beliebiges Bild (PNG, JPG …) mit „Bild einsetzen…“ auf die Außenseite setzen, aktuellen Umhang laden, als PNG speichern oder mit „Als Umhang übernehmen“ hochladen
|
||||||
|
- AeroMC-Umhänge im Spiel: in jeder Minecraft-Version und mit jedem Loader (Vanilla, Fabric, Quilt, Forge, NeoForge) ohne zusätzliche Mod sichtbar – für einen selbst und für andere AeroMC-Spieler; abschaltbar unter Optionen → Spiel → Umhänge
|
||||||
|
- Mod „AeroMC Umhänge“ für Fabric und Quilt: damit sehen auch Spieler mit anderen Launchern (offizieller Launcher, MultiMC, Prism, Modrinth App …) die AeroMC-Umhänge – ein Jar für Minecraft 1.14 bis 26.x, nur auf dem Client nötig
|
||||||
|
|
||||||
|
### Geändert
|
||||||
|
- Suche in der Werkzeugleiste: statt des dauerhaft sichtbaren Suchfelds gibt es eine Lupe – ein Klick oder Strg+F klappt das Feld auf, Esc leert und schließt es, mit Suchbegriff bleibt es offen
|
||||||
|
- Benötigt für eigene Umhänge das Plugin „AeroMC Freunde“ 1.5.0 auf dem Server
|
||||||
|
|
||||||
|
## [1.1.6] - 2026-10-03
|
||||||
|
|
||||||
|
### 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 Klick auf eine Kachel zeigt das Design sofort zur Probe, nach dem Speichern gilt es 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
|
||||||
|
|
||||||
|
### Geändert
|
||||||
|
- Die Design-Auswahl (bisher „Design: Dunkel/Hell“ unter Optionen → Allgemein) steht jetzt unter Optionen → Darstellung
|
||||||
|
|
||||||
|
### 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
|
## [1.1.5] - 2026-10-02
|
||||||
|
|
||||||
### Neu
|
### Neu
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
Ein Minecraft-Launcher für Windows mit getrennten Instanzen, Mod-Unterstützung, Spielaufzeichnung, Freundesystem und Microsoft-Anmeldung.
|
Ein Minecraft-Launcher für Windows mit getrennten Instanzen, Mod-Unterstützung, Spielaufzeichnung, Freundesystem und Microsoft-Anmeldung.
|
||||||
|
|
||||||
Aktueller Stand: Version 1.1.5
|
Aktueller Stand: Version 1.1.7
|
||||||
|
|
||||||
<img src="docs/aeromc.png" alt="AeroMC – Instanzliste mit Seitenleiste und Skin-Vorschau" width="820" />
|
<img src="docs/aeromc.png" alt="AeroMC – Instanzliste mit Seitenleiste und Skin-Vorschau" width="820" />
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ Der Launcher konzentriert sich auf den Alltag: Instanz anlegen, Konto auswählen
|
|||||||
- Warnung beim Entfernen eines Mods, der noch als Pflicht-Abhängigkeit gebraucht wird
|
- Warnung beim Entfernen eines Mods, der noch als Pflicht-Abhängigkeit gebraucht wird
|
||||||
|
|
||||||
**Import**
|
**Import**
|
||||||
- Gemeinsamer Import für AeroMC-ZIP, Modrinth-Modpacks (`.mrpack`), Minecraft Launcher, CurseForge, MultiMC/Prism sowie Technic
|
- 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
|
- 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
|
- Von Freunden geteilte Instanzen per Code einlösen
|
||||||
|
|
||||||
@@ -64,7 +64,10 @@ Der Launcher konzentriert sich auf den Alltag: Instanz anlegen, Konto auswählen
|
|||||||
**Konten, Welten und Backups**
|
**Konten, Welten und Backups**
|
||||||
- Microsoft-Login mit mehreren Konten
|
- Microsoft-Login mit mehreren Konten
|
||||||
- Eigenen Skin im Launcher hochladen, mit Live-Vorschau anpassen (klassisch/schlank) und zurücksetzen
|
- Eigenen Skin im Launcher hochladen, mit Live-Vorschau anpassen (klassisch/schlank) und zurücksetzen
|
||||||
- Skin-Hintergrund des aktiven Kontos (mehrere Posen, einstellbare Deckkraft, optional wechselnd)
|
- **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
|
||||||
|
- **Eigene Umhänge, auch HD bis 1024×512**: im Skin-Editor gestalten, im Spiel in jeder Version und mit jedem Loader sichtbar – für Spieler mit anderen Launchern gibt es den Mod „AeroMC Umhänge“
|
||||||
|
- Offiziellen Mojang-Umhang direkt im Launcher wechseln oder ablegen
|
||||||
|
- Skin-Hintergrund des aktiven Kontos (mehrere Posen, passende Armform, einstellbare Deckkraft, optional wechselnd)
|
||||||
- Welten, Ressourcenpakete, Datenpakete, Screenshots, Serverliste und Logdateien verwalten
|
- Welten, Ressourcenpakete, Datenpakete, Screenshots, Serverliste und Logdateien verwalten
|
||||||
- Serverliste mit Live-Status und Desktop-Verknüpfung pro Server (startet die Instanz und verbindet direkt)
|
- 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
|
- Welten mit einem Klick in eine andere Instanz kopieren
|
||||||
@@ -80,7 +83,22 @@ Der Launcher konzentriert sich auf den Alltag: Instanz anlegen, Konto auswählen
|
|||||||
- Update-Suche sofort beim Start, Updates installieren sich ohne Rückfragen
|
- Update-Suche sofort beim Start, Updates installieren sich ohne Rückfragen
|
||||||
- Optionaler Autostart mit Windows (im Infobereich statt als Fenster)
|
- Optionaler Autostart mit Windows (im Infobereich statt als Fenster)
|
||||||
- Rechtsklick-Menü in allen Textfeldern (Ausschneiden, Kopieren, Einfügen)
|
- Rechtsklick-Menü in allen Textfeldern (Ausschneiden, Kopieren, Einfügen)
|
||||||
- Design Hell/Dunkel, optionaler HTTP-Proxy und Wrapper-Befehl
|
- **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
|
## Installation
|
||||||
|
|
||||||
@@ -105,7 +123,7 @@ Automatische Updates bleiben getrennt: Setup erhält nur Setup-Updates, Portable
|
|||||||
## Erste Schritte
|
## Erste Schritte
|
||||||
|
|
||||||
1. AeroMC installieren oder die portable Datei starten.
|
1. AeroMC installieren oder die portable Datei starten.
|
||||||
2. Über **Konten** ein Microsoft-Konto hinzufügen.
|
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.
|
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.
|
4. Optional Mods/Ressourcenpakete/Shader hinzufügen oder bestehende Setups über **Importieren** übernehmen.
|
||||||
5. Instanz starten (online oder offline).
|
5. Instanz starten (online oder offline).
|
||||||
@@ -146,7 +164,7 @@ Im Launcher:
|
|||||||
|
|
||||||
| Taste | Wirkung |
|
| Taste | Wirkung |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Strg+F | Suche fokussieren |
|
| Strg+F | Suche öffnen (Lupe in der Werkzeugleiste), Esc leert und schließt sie |
|
||||||
| Strg+N | Neue Instanz anlegen |
|
| Strg+N | Neue Instanz anlegen |
|
||||||
| Strg+1 … Strg+5 | Schnellstart-Instanz starten |
|
| Strg+1 … Strg+5 | Schnellstart-Instanz starten |
|
||||||
| Entf | Markierte Instanz(en) löschen |
|
| Entf | Markierte Instanz(en) löschen |
|
||||||
@@ -172,6 +190,7 @@ Im Spiel (solange ein über AeroMC gestartetes Minecraft läuft):
|
|||||||
| Modpack | Modrinth- oder CurseForge-Katalog direkt durchsuchen und installieren, oder Datei importieren: `.mrpack` sowie CurseForge-ZIP (`manifest.json`) |
|
| 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 |
|
| Minecraft Launcher | Profile aus dem offiziellen Launcher |
|
||||||
| CurseForge | Instanzen aus dem CurseForge-Ordner |
|
| 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 |
|
| MultiMC / Prism | Vorhandene MultiMC- oder Prism-Instanzen |
|
||||||
| Technic | Technic-Katalog direkt durchsuchen und installieren, oder eine vorhandene Technic-Installation importieren (`%APPDATA%\.technic\modpacks`) |
|
| 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**) |
|
| Per Code | Von einem Freund geteilte Instanz einlösen (siehe **Instanzen → Instanz teilen**) |
|
||||||
@@ -180,7 +199,9 @@ 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.
|
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.
|
||||||
|
|
||||||
**Schon bei der Installation:** Der Installer sucht nach Minecraft Launcher, CurseForge, MultiMC, Prism Launcher, PolyMC und Technic – ein portables MultiMC auch über seine Verknüpfung auf dem Desktop oder im Startmenü. Was dort angekreuzt wird, öffnet AeroMC beim ersten Start fertig durchsucht im Import; ausgewählt wird dann nur noch, was übernommen werden soll. In den anderen Launchern wird nichts verändert.
|
**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.
|
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.
|
||||||
|
|
||||||
@@ -222,7 +243,6 @@ Ressourcenpakete und Shader funktionieren auch in Vanilla-Instanzen. Mods benöt
|
|||||||
- Meldung im Spiel mit 3D-Spielerkopf, sobald ein Freund online kommt
|
- 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
|
- **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“
|
- **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.
|
- **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.
|
- **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.
|
||||||
|
|
||||||
@@ -234,15 +254,61 @@ Das Freundesystem funktioniert nur zwischen Spielern, die AeroMC nutzen und mind
|
|||||||
|
|
||||||
## Skin ändern
|
## Skin ändern
|
||||||
|
|
||||||
Über **Konten → Skin ändern…** lässt sich der Skin des aktiven Microsoft-Kontos direkt im Launcher ä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
|
- PNG-Datei auswählen, sofortige Prüfung auf gültige Maße
|
||||||
- Live-Vorschau am 3D-Modell vor dem Anwenden
|
- Live-Vorschau am 3D-Modell vor dem Anwenden
|
||||||
- Modell wählen: klassisch oder schlank (slim)
|
- Modell wählen: klassisch oder schlank (slim)
|
||||||
- Auf einen früheren Standard-Skin zurücksetzen
|
- 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.
|
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 |
|
||||||
|
| 0 | Umhang: Ansicht einpassen |
|
||||||
|
|
||||||
|
### Umhänge
|
||||||
|
|
||||||
|
Im Skin-Fenster (**Kontoname oben rechts → Skin ändern…**) gibt es den Bereich **Umhang**:
|
||||||
|
|
||||||
|
- **Offizieller Umhang:** zeigt alle Umhänge, die dein Konto bei Mojang besitzt (z. B. Migrator, Pan …) – auswählen oder „Kein Umhang“. Das gilt wie der Skin überall, auch außerhalb von AeroMC.
|
||||||
|
- **AeroMC-Umhang:** dein eigener Umhang, auch in HD. **Gestalten…** öffnet den Skin-Editor im Modus **Umhang**, **Entfernen** löscht ihn wieder.
|
||||||
|
|
||||||
|
Im Editor-Modus **Umhang**:
|
||||||
|
|
||||||
|
- Größe wählen: Normal (64×32) oder HD 128×64, 256×128, 512×256, 1024×512
|
||||||
|
- Alle Werkzeuge wie beim Skin, dazu Pinselgröße (1–16 Pixel); Spiegeln malt linke und rechte Kante gleichzeitig
|
||||||
|
- Mausrad zoomt, rechte oder mittlere Maustaste verschiebt die Ansicht, **0** passt sie wieder ein
|
||||||
|
- In der 3D-Ansicht direkt auf dem Umhang an der Figur malen; **Als Elytra zeigen** zeigt dieselbe Datei als Flügel
|
||||||
|
- **Bild einsetzen…** setzt ein beliebiges Bild (PNG, JPG …) auf die Außenseite
|
||||||
|
- **Aktuellen Umhang laden**, PNG öffnen oder speichern, **Als Umhang übernehmen** lädt ihn hoch
|
||||||
|
|
||||||
|
**Wer sieht den AeroMC-Umhang?**
|
||||||
|
|
||||||
|
| Spieler | sieht den AeroMC-Umhang? |
|
||||||
|
|---|---|
|
||||||
|
| Spielt über AeroMC (Optionen → Spiel → Umhänge eingeschaltet) | Ja – in jeder Minecraft-Version und mit jedem Loader, ohne Mod |
|
||||||
|
| Anderer Launcher mit Fabric oder Quilt und dem Mod **„AeroMC Umhänge“** | Ja – ein Jar für Minecraft 1.14 bis 26.x, nur auf dem Client nötig |
|
||||||
|
| Anderer Launcher ohne den Mod (auch mit OptiFine) | Nein – sieht weiter den offiziellen Mojang-Umhang |
|
||||||
|
|
||||||
|
Der AeroMC-Umhang hat Vorrang vor dem offiziellen Umhang. Benötigt das Plugin „AeroMC Freunde“ 1.5.0 auf dem Server.
|
||||||
|
|
||||||
## Backup und Wiederherstellung
|
## Backup und Wiederherstellung
|
||||||
|
|
||||||
Unter **Optionen → Backup**:
|
Unter **Optionen → Backup**:
|
||||||
@@ -261,7 +327,6 @@ Speicherort: `Dokumente\AeroMC Launcher\Backups`.
|
|||||||
## Einstellungen
|
## Einstellungen
|
||||||
|
|
||||||
### Allgemein
|
### Allgemein
|
||||||
- Design (Dunkel / Hell)
|
|
||||||
- Instanzen sortieren (nach letztem Start / nach Name)
|
- Instanzen sortieren (nach letztem Start / nach Name)
|
||||||
- Instanz-Verzeichnis
|
- Instanz-Verzeichnis
|
||||||
- Bereiche: Bedrock-Bereich anzeigen
|
- Bereiche: Bedrock-Bereich anzeigen
|
||||||
@@ -270,11 +335,13 @@ Speicherort: `Dokumente\AeroMC Launcher\Backups`.
|
|||||||
- 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.
|
- 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
|
### 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)
|
- Block-Symbole räumlich (3D)
|
||||||
- Skin-Hintergrund: Quelle, Pose, Deckkraft, optional automatischer Wechsel zwischen mehreren Konten
|
- Skin-Hintergrund: Quelle, Pose, Armform (automatisch vom Konto, normal oder schmal), Deckkraft, optional automatischer Wechsel zwischen mehreren Konten
|
||||||
|
|
||||||
### Spiel
|
### Spiel
|
||||||
- Standard-RAM (min./max.)
|
- Standard-RAM (min./max.)
|
||||||
|
- Umhänge: AeroMC-Umhänge im Spiel anzeigen (gilt ab dem nächsten Start)
|
||||||
- Fenstergröße bzw. maximiert starten
|
- Fenstergröße bzw. maximiert starten
|
||||||
- Mods: vor jedem Start automatisch nach Updates suchen und installieren
|
- Mods: vor jedem Start automatisch nach Updates suchen und installieren
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package de.aeromc.capes;
|
||||||
|
|
||||||
|
import java.lang.instrument.Instrumentation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AeroMC-Umhang-Agent.
|
||||||
|
*
|
||||||
|
* Wird vom Launcher per -javaagent an Minecraft angehängt (jede Version, jeder
|
||||||
|
* Loader). Er klinkt sich in Mojangs authlib ein – über sie lädt Minecraft für
|
||||||
|
* jeden Spieler Skin und Umhang – und setzt dort den AeroMC-Umhang des Spielers
|
||||||
|
* ein, falls er einen hat. Alles Weitere (Herunterladen, Zwischenspeichern,
|
||||||
|
* Anzeigen, auch HD) erledigt Minecraft selbst wie bei einem offiziellen Umhang.
|
||||||
|
*
|
||||||
|
* Argument: Basisadresse des Umhang-Servers (…/wp-json/aeromc/v1).
|
||||||
|
*/
|
||||||
|
public final class Agent {
|
||||||
|
/** Unter diesem Schlüssel liegt der Nachschlage-Dienst in den System-Properties. */
|
||||||
|
public static final String SCHLUESSEL = "aeromc.capes.hook";
|
||||||
|
|
||||||
|
private Agent() {}
|
||||||
|
|
||||||
|
public static void premain(String argument, Instrumentation inst) {
|
||||||
|
starte(argument, inst);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void agentmain(String argument, Instrumentation inst) {
|
||||||
|
starte(argument, inst);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void starte(String argument, Instrumentation inst) {
|
||||||
|
try {
|
||||||
|
String basis = argument != null && argument.startsWith("http") ? argument : Nachschlagen.STANDARD_BASIS;
|
||||||
|
// Die eingefügten Befehle in authlib holen sich den Dienst von hier – nur
|
||||||
|
// über JDK-Typen, damit es mit jedem Classloader und Modulsystem klappt.
|
||||||
|
System.getProperties().put(SCHLUESSEL, new Nachschlagen(basis));
|
||||||
|
inst.addTransformer(new Umbau());
|
||||||
|
System.out.println("[AeroMC] Umhang-Agent aktiv");
|
||||||
|
} catch (Throwable t) {
|
||||||
|
System.out.println("[AeroMC] Umhang-Agent konnte nicht starten: " + t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
package de.aeromc.capes;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.function.BiFunction;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wird aus authlib heraus mit (ergebnis, argument) aufgerufen und gibt das
|
||||||
|
* Ergebnis zurück – mit eingesetztem AeroMC-Umhang, falls der Spieler einen hat.
|
||||||
|
* Arbeitet nur über Reflexion, damit es zu jeder authlib-Fassung passt, und
|
||||||
|
* fängt jeden Fehler ab: im schlimmsten Fall bleibt alles wie ohne Agent.
|
||||||
|
*/
|
||||||
|
public final class Nachschlagen implements BiFunction<Object, Object, Object> {
|
||||||
|
public static final String STANDARD_BASIS = "https://viper-network.de/wp-json/aeromc/v1";
|
||||||
|
private static final long GEFUNDEN_MS = 5 * 60 * 1000L;
|
||||||
|
private static final long KEINER_MS = 2 * 60 * 1000L;
|
||||||
|
private static final Pattern URL_MUSTER = Pattern.compile("\"url\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\"");
|
||||||
|
private static final Pattern ID_MUSTER = Pattern.compile("\"profileId\"\\s*:\\s*\"([0-9a-fA-F-]{32,36})\"");
|
||||||
|
|
||||||
|
private static final class Eintrag {
|
||||||
|
final String url; // null = kein Umhang
|
||||||
|
final long zeit;
|
||||||
|
Eintrag(String url) { this.url = url; this.zeit = System.currentTimeMillis(); }
|
||||||
|
boolean frisch() { return System.currentTimeMillis() - zeit < (url != null ? GEFUNDEN_MS : KEINER_MS); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private final String basis;
|
||||||
|
private final Map<String, Eintrag> speicher = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, Boolean> unterwegs = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, Boolean> gemeldet = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public Nachschlagen(String basis) {
|
||||||
|
this.basis = basis.endsWith("/") ? basis.substring(0, basis.length() - 1) : basis;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object apply(Object ergebnis, Object argument) {
|
||||||
|
try {
|
||||||
|
String id = spielerId(argument);
|
||||||
|
if (id == null) return ergebnis;
|
||||||
|
String url = umhangUrl(id);
|
||||||
|
if (url == null) return ergebnis;
|
||||||
|
Object neu = mitUmhang(ergebnis, url, argument);
|
||||||
|
if (neu != ergebnis && gemeldet.putIfAbsent(id, Boolean.TRUE) == null) {
|
||||||
|
System.out.println("[AeroMC] Umhang fuer " + id + " eingesetzt");
|
||||||
|
}
|
||||||
|
return neu;
|
||||||
|
} catch (Throwable t) {
|
||||||
|
return ergebnis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Spieler erkennen ----------
|
||||||
|
private static String spielerId(Object argument) throws Exception {
|
||||||
|
if (argument == null) return null;
|
||||||
|
String klasse = argument.getClass().getName();
|
||||||
|
if (klasse.endsWith("GameProfile")) {
|
||||||
|
Object id = rufe(argument, "getId", "id");
|
||||||
|
return id instanceof UUID ? hex((UUID) id) : null;
|
||||||
|
}
|
||||||
|
if (klasse.endsWith("Property")) {
|
||||||
|
Object wert = rufe(argument, "getValue", "value");
|
||||||
|
if (!(wert instanceof String)) return null;
|
||||||
|
String json = new String(Base64.getDecoder().decode(((String) wert).trim()), StandardCharsets.UTF_8);
|
||||||
|
Matcher m = ID_MUSTER.matcher(json);
|
||||||
|
return m.find() ? m.group(1).replace("-", "").toLowerCase() : null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String hex(UUID u) {
|
||||||
|
return u.toString().replace("-", "").toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object rufe(Object ziel, String... namen) throws Exception {
|
||||||
|
for (String n : namen) {
|
||||||
|
try {
|
||||||
|
Method m = ziel.getClass().getMethod(n);
|
||||||
|
return m.invoke(ziel);
|
||||||
|
} catch (NoSuchMethodException ignoriert) {
|
||||||
|
// nächster Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Umhang-Server ----------
|
||||||
|
private String umhangUrl(String id) {
|
||||||
|
Eintrag e = speicher.get(id);
|
||||||
|
if (e != null && e.frisch()) return e.url;
|
||||||
|
// Auf dem Haupt-/Render-Thread nie warten: dort nur im Hintergrund holen;
|
||||||
|
// Minecraft fragt beim nächsten Laden des Spielers erneut
|
||||||
|
String thread = Thread.currentThread().getName();
|
||||||
|
boolean hauptThread = thread.equals("Render thread") || thread.equals("Client thread") || thread.equals("main");
|
||||||
|
if (hauptThread) {
|
||||||
|
if (unterwegs.putIfAbsent(id, Boolean.TRUE) == null) {
|
||||||
|
Thread t = new Thread(() -> { try { hole(id); } finally { unterwegs.remove(id); } }, "AeroMC-Umhang");
|
||||||
|
t.setDaemon(true);
|
||||||
|
t.start();
|
||||||
|
}
|
||||||
|
return e != null ? e.url : null;
|
||||||
|
}
|
||||||
|
return hole(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String hole(String id) {
|
||||||
|
try {
|
||||||
|
HttpURLConnection c = (HttpURLConnection) new URL(basis + "/capes?uuids=" + id).openConnection();
|
||||||
|
c.setConnectTimeout(2000);
|
||||||
|
c.setReadTimeout(3000);
|
||||||
|
c.setRequestProperty("User-Agent", "AeroMC-Umhang/1.0");
|
||||||
|
c.setRequestProperty("Accept", "application/json");
|
||||||
|
if (c.getResponseCode() != 200) {
|
||||||
|
speicher.put(id, new Eintrag(null));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String json;
|
||||||
|
try (InputStream in = c.getInputStream()) {
|
||||||
|
ByteArrayOutputStream puffer = new ByteArrayOutputStream();
|
||||||
|
byte[] b = new byte[4096];
|
||||||
|
int n;
|
||||||
|
while ((n = in.read(b)) > 0 && puffer.size() < 65536) puffer.write(b, 0, n);
|
||||||
|
json = new String(puffer.toByteArray(), StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
String url = null;
|
||||||
|
Matcher m = URL_MUSTER.matcher(json);
|
||||||
|
if (json.contains("\"" + id + "\"") && m.find()) {
|
||||||
|
url = m.group(1).replace("\\/", "/");
|
||||||
|
if (!url.startsWith("https://")) url = null;
|
||||||
|
}
|
||||||
|
speicher.put(id, new Eintrag(url));
|
||||||
|
return url;
|
||||||
|
} catch (Throwable t) {
|
||||||
|
// Server nicht erreichbar: kurz merken, damit nicht jeder Spieler hängt
|
||||||
|
speicher.put(id, new Eintrag(null));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Umhang einsetzen ----------
|
||||||
|
private static Object textur(ClassLoader cl, String url) throws Exception {
|
||||||
|
Class<?> k = Class.forName("com.mojang.authlib.minecraft.MinecraftProfileTexture", false, cl);
|
||||||
|
Constructor<?> ctor = k.getConstructor(String.class, Map.class);
|
||||||
|
return ctor.newInstance(url, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||||
|
private static Object mitUmhang(Object ergebnis, String url, Object argument) throws Exception {
|
||||||
|
ClassLoader cl = argument.getClass().getClassLoader();
|
||||||
|
if (ergebnis instanceof Map) {
|
||||||
|
// authlib 1–4: Map<MinecraftProfileTexture.Type, MinecraftProfileTexture>
|
||||||
|
Class<?> typ = Class.forName("com.mojang.authlib.minecraft.MinecraftProfileTexture$Type", false, cl);
|
||||||
|
Object cape = Enum.valueOf((Class<Enum>) typ, "CAPE");
|
||||||
|
Map neu = new HashMap((Map) ergebnis);
|
||||||
|
neu.put(cape, textur(cl, url));
|
||||||
|
return neu;
|
||||||
|
}
|
||||||
|
if (ergebnis == null) return null;
|
||||||
|
// authlib 5+: record MinecraftProfileTextures(skin, cape, elytra, signatureState)
|
||||||
|
Class<?> k = ergebnis.getClass();
|
||||||
|
Object skin = k.getMethod("skin").invoke(ergebnis);
|
||||||
|
Object elytra = k.getMethod("elytra").invoke(ergebnis);
|
||||||
|
Object signatur = k.getMethod("signatureState").invoke(ergebnis);
|
||||||
|
for (Constructor<?> ctor : k.getConstructors()) {
|
||||||
|
if (ctor.getParameterCount() == 4) {
|
||||||
|
return ctor.newInstance(skin, textur(cl, url), elytra, signatur);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ergebnis;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package de.aeromc.capes;
|
||||||
|
|
||||||
|
import java.lang.instrument.ClassFileTransformer;
|
||||||
|
import java.security.ProtectionDomain;
|
||||||
|
|
||||||
|
import org.objectweb.asm.ClassReader;
|
||||||
|
import org.objectweb.asm.ClassVisitor;
|
||||||
|
import org.objectweb.asm.ClassWriter;
|
||||||
|
import org.objectweb.asm.MethodVisitor;
|
||||||
|
import org.objectweb.asm.Opcodes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut YggdrasilMinecraftSessionService beim Laden um: vor jedem "return" der
|
||||||
|
* Textur-Methode wird das Ergebnis an den Nachschlage-Dienst gereicht, der den
|
||||||
|
* AeroMC-Umhang einsetzt.
|
||||||
|
*
|
||||||
|
* authlib 5+ (Minecraft 1.20.2 … 26.x):
|
||||||
|
* MinecraftProfileTextures unpackTextures(Property)
|
||||||
|
* authlib 1–4 (Minecraft 1.8 … 1.20.1):
|
||||||
|
* Map<Type, MinecraftProfileTexture> getTextures(GameProfile, boolean)
|
||||||
|
*
|
||||||
|
* Eingefügt wird nur geradliniger Code ohne Sprünge – die vorhandenen
|
||||||
|
* Stack-Map-Frames bleiben gültig, es muss nichts neu berechnet werden.
|
||||||
|
*/
|
||||||
|
final class Umbau implements ClassFileTransformer {
|
||||||
|
static final String ZIEL = "com/mojang/authlib/yggdrasil/YggdrasilMinecraftSessionService";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] transform(ClassLoader loader, String name, Class<?> umdefiniert, ProtectionDomain pd, byte[] klasse) {
|
||||||
|
if (!ZIEL.equals(name) || umdefiniert != null) return null;
|
||||||
|
try {
|
||||||
|
ClassReader leser = new ClassReader(klasse);
|
||||||
|
ClassWriter schreiber = new ClassWriter(leser, ClassWriter.COMPUTE_MAXS);
|
||||||
|
final int[] umgebaut = {0};
|
||||||
|
leser.accept(new ClassVisitor(Opcodes.ASM9, schreiber) {
|
||||||
|
@Override
|
||||||
|
public MethodVisitor visitMethod(int zugriff, String mname, String desc, String sig, String[] ausn) {
|
||||||
|
MethodVisitor mv = super.visitMethod(zugriff, mname, desc, sig, ausn);
|
||||||
|
if ((zugriff & Opcodes.ACC_STATIC) != 0) return mv;
|
||||||
|
String rueckgabe = null;
|
||||||
|
if ("unpackTextures".equals(mname) && desc.startsWith("(Lcom/mojang/authlib/properties/Property;)L")) {
|
||||||
|
rueckgabe = desc.substring(desc.indexOf(")L") + 2, desc.length() - 1);
|
||||||
|
} else if ("getTextures".equals(mname) && desc.equals("(Lcom/mojang/authlib/GameProfile;Z)Ljava/util/Map;")) {
|
||||||
|
rueckgabe = "java/util/Map";
|
||||||
|
}
|
||||||
|
if (rueckgabe == null) return mv;
|
||||||
|
umgebaut[0]++;
|
||||||
|
final String typ = rueckgabe;
|
||||||
|
return new MethodVisitor(Opcodes.ASM9, mv) {
|
||||||
|
@Override
|
||||||
|
public void visitInsn(int op) {
|
||||||
|
if (op == Opcodes.ARETURN) {
|
||||||
|
// Stapel: [ergebnis] -> [dienst, ergebnis, argument] -> apply -> [neu]
|
||||||
|
super.visitVarInsn(Opcodes.ALOAD, 1);
|
||||||
|
super.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "getProperties", "()Ljava/util/Properties;", false);
|
||||||
|
super.visitLdcInsn(Agent.SCHLUESSEL);
|
||||||
|
super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/util/Properties", "get", "(Ljava/lang/Object;)Ljava/lang/Object;", false);
|
||||||
|
super.visitTypeInsn(Opcodes.CHECKCAST, "java/util/function/BiFunction");
|
||||||
|
super.visitInsn(Opcodes.DUP_X2);
|
||||||
|
super.visitInsn(Opcodes.POP);
|
||||||
|
super.visitMethodInsn(Opcodes.INVOKEINTERFACE, "java/util/function/BiFunction", "apply",
|
||||||
|
"(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", true);
|
||||||
|
super.visitTypeInsn(Opcodes.CHECKCAST, typ);
|
||||||
|
}
|
||||||
|
super.visitInsn(op);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
if (umgebaut[0] == 0) return null;
|
||||||
|
System.out.println("[AeroMC] authlib fuer Umhaenge vorbereitet (" + umgebaut[0] + " Methode)");
|
||||||
|
return schreiber.toByteArray();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
System.out.println("[AeroMC] authlib konnte nicht angepasst werden: " + t);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.jar.Attributes;
|
||||||
|
import java.util.jar.JarEntry;
|
||||||
|
import java.util.jar.JarFile;
|
||||||
|
import java.util.jar.JarOutputStream;
|
||||||
|
import java.util.jar.Manifest;
|
||||||
|
|
||||||
|
import org.objectweb.asm.ClassReader;
|
||||||
|
import org.objectweb.asm.ClassWriter;
|
||||||
|
import org.objectweb.asm.commons.ClassRemapper;
|
||||||
|
import org.objectweb.asm.commons.Remapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut die Agent-Datei: eigene Klassen + ASM, dabei org/objectweb/asm nach
|
||||||
|
* de/aeromc/capes/asm umbenannt. So kann das ASM von Fabric/Forge (das auf
|
||||||
|
* demselben Klassenpfad liegt) nie mit unserem verwechselt werden.
|
||||||
|
*
|
||||||
|
* Aufruf: java Shade <ziel.jar> <klassenordner> <asm.jar>
|
||||||
|
*/
|
||||||
|
public class Shade {
|
||||||
|
static final Remapper UMBENENNEN = new Remapper() {
|
||||||
|
@Override
|
||||||
|
public String map(String name) {
|
||||||
|
return name.startsWith("org/objectweb/asm/") ? "de/aeromc/capes/asm/" + name.substring(18) : name;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static byte[] umbenennen(byte[] klasse) {
|
||||||
|
ClassReader r = new ClassReader(klasse);
|
||||||
|
ClassWriter w = new ClassWriter(0);
|
||||||
|
r.accept(new ClassRemapper(w, UMBENENNEN), 0);
|
||||||
|
return w.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
static byte[] lies(InputStream in) throws Exception {
|
||||||
|
ByteArrayOutputStream b = new ByteArrayOutputStream();
|
||||||
|
byte[] p = new byte[8192];
|
||||||
|
int n;
|
||||||
|
while ((n = in.read(p)) > 0) b.write(p, 0, n);
|
||||||
|
return b.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] a) throws Exception {
|
||||||
|
Manifest mf = new Manifest();
|
||||||
|
Attributes at = mf.getMainAttributes();
|
||||||
|
at.put(Attributes.Name.MANIFEST_VERSION, "1.0");
|
||||||
|
at.putValue("Premain-Class", "de.aeromc.capes.Agent");
|
||||||
|
at.putValue("Agent-Class", "de.aeromc.capes.Agent");
|
||||||
|
at.putValue("Can-Retransform-Classes", "false");
|
||||||
|
at.putValue("Implementation-Title", "AeroMC Umhang-Agent");
|
||||||
|
try (JarOutputStream out = new JarOutputStream(new FileOutputStream(a[0]), mf)) {
|
||||||
|
// eigene Klassen
|
||||||
|
File basis = new File(a[1]);
|
||||||
|
for (File f : Files.walk(basis.toPath()).map(p -> p.toFile()).filter(f -> f.getName().endsWith(".class")).toArray(File[]::new)) {
|
||||||
|
String name = basis.toPath().relativize(f.toPath()).toString().replace('\\', '/');
|
||||||
|
out.putNextEntry(new JarEntry(name));
|
||||||
|
out.write(umbenennen(Files.readAllBytes(f.toPath())));
|
||||||
|
out.closeEntry();
|
||||||
|
}
|
||||||
|
// ASM (umbenannt), ohne Modul-Infos und Signaturen
|
||||||
|
try (JarFile asm = new JarFile(a[2])) {
|
||||||
|
Enumeration<JarEntry> e = asm.entries();
|
||||||
|
while (e.hasMoreElements()) {
|
||||||
|
JarEntry je = e.nextElement();
|
||||||
|
String n = je.getName();
|
||||||
|
if (!n.startsWith("org/objectweb/asm/") || !n.endsWith(".class")) continue;
|
||||||
|
byte[] neu = umbenennen(lies(asm.getInputStream(je)));
|
||||||
|
out.putNextEntry(new JarEntry("de/aeromc/capes/asm/" + n.substring(18)));
|
||||||
|
out.write(neu);
|
||||||
|
out.closeEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 169 KiB |
@@ -5,7 +5,7 @@ using System.Web.Script.Serialization;
|
|||||||
namespace AeroMCSetup;
|
namespace AeroMCSetup;
|
||||||
|
|
||||||
/// <summary>Ein gefundener anderer Launcher, dessen Instanzen AeroMC übernehmen kann.</summary>
|
/// <summary>Ein gefundener anderer Launcher, dessen Instanzen AeroMC übernehmen kann.</summary>
|
||||||
/// <param name="kind">Kennung wie im Import-Dialog des Launchers: mc, cf, mmc, technic</param>
|
/// <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 sealed class FoundLauncher(string kind, string name, string path, int count, string unit)
|
||||||
{
|
{
|
||||||
public string Kind { get; } = kind;
|
public string Kind { get; } = kind;
|
||||||
@@ -37,6 +37,7 @@ public static class LauncherImport
|
|||||||
}
|
}
|
||||||
Add(DetectMinecraftLauncher);
|
Add(DetectMinecraftLauncher);
|
||||||
Add(DetectCurseForge);
|
Add(DetectCurseForge);
|
||||||
|
Add(DetectModrinthApp);
|
||||||
Add(() => DetectMultiMc("Prism Launcher", System.IO.Path.Combine(AppData, "PrismLauncher")));
|
Add(() => DetectMultiMc("Prism Launcher", System.IO.Path.Combine(AppData, "PrismLauncher")));
|
||||||
Add(() => DetectMultiMc("PolyMC", System.IO.Path.Combine(AppData, "PolyMC")));
|
Add(() => DetectMultiMc("PolyMC", System.IO.Path.Combine(AppData, "PolyMC")));
|
||||||
// MultiMC (und portable Prism/PolyMC) liegen irgendwo – über ihre Verknüpfungen finden
|
// MultiMC (und portable Prism/PolyMC) liegen irgendwo – über ihre Verknüpfungen finden
|
||||||
@@ -92,6 +93,23 @@ public static class LauncherImport
|
|||||||
return new FoundLauncher("cf", "CurseForge", dir, count, count == 1 ? "Instanz" : "Instanzen");
|
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) ----------
|
// ---------- Prism Launcher / PolyMC (MultiMC-Format) ----------
|
||||||
|
|
||||||
private static FoundLauncher? DetectMultiMc(string name, string root)
|
private static FoundLauncher? DetectMultiMc(string name, string root)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
},
|
},
|
||||||
"projects": {
|
"projects": {
|
||||||
"C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj": {
|
"C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj": {
|
||||||
"version": "1.1.5",
|
"version": "0.0.0",
|
||||||
"restore": {
|
"restore": {
|
||||||
"projectUniqueName": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj",
|
"projectUniqueName": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj",
|
||||||
"projectName": "AeroMC-Setup",
|
"projectName": "AeroMC-Setup",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -14,11 +14,11 @@ using System.Reflection;
|
|||||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
|
||||||
[assembly: System.Reflection.AssemblyCopyrightAttribute("Copyright (c) M_Viper. Alle Rechte vorbehalten.")]
|
[assembly: System.Reflection.AssemblyCopyrightAttribute("Copyright (c) M_Viper. Alle Rechte vorbehalten.")]
|
||||||
[assembly: System.Reflection.AssemblyDescriptionAttribute("Installiert AeroMC")]
|
[assembly: System.Reflection.AssemblyDescriptionAttribute("Installiert AeroMC")]
|
||||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.1.5.0")]
|
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.1.7.0")]
|
||||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.1.5")]
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.1.7")]
|
||||||
[assembly: System.Reflection.AssemblyProductAttribute("AeroMC Setup")]
|
[assembly: System.Reflection.AssemblyProductAttribute("AeroMC Setup")]
|
||||||
[assembly: System.Reflection.AssemblyTitleAttribute("AeroMC Setup")]
|
[assembly: System.Reflection.AssemblyTitleAttribute("AeroMC Setup")]
|
||||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.1.5.0")]
|
[assembly: System.Reflection.AssemblyVersionAttribute("1.1.7.0")]
|
||||||
[assembly: System.Resources.NeutralResourcesLanguageAttribute("de-DE")]
|
[assembly: System.Resources.NeutralResourcesLanguageAttribute("de-DE")]
|
||||||
|
|
||||||
// Von der MSBuild WriteCodeFragment-Klasse generiert.
|
// Von der MSBuild WriteCodeFragment-Klasse generiert.
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
6fa3eb63bf9e7c9a77cca67d608dff49330e3a93e70e18548b24192d4ef82422
|
acf80df582ccd59574a6332950a42ff0628e88f4288eb303a60e8577b37c7bef
|
||||||
|
|||||||
@@ -418,7 +418,7 @@
|
|||||||
"C:\\Users\\Viper\\.nuget\\packages\\": {}
|
"C:\\Users\\Viper\\.nuget\\packages\\": {}
|
||||||
},
|
},
|
||||||
"project": {
|
"project": {
|
||||||
"version": "1.1.5",
|
"version": "0.0.0",
|
||||||
"restore": {
|
"restore": {
|
||||||
"projectUniqueName": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj",
|
"projectUniqueName": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj",
|
||||||
"projectName": "AeroMC-Setup",
|
"projectName": "AeroMC-Setup",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"dgSpecHash": "za8GIOA75zI=",
|
"dgSpecHash": "FnRoda2hVew=",
|
||||||
"success": true,
|
"success": true,
|
||||||
"projectFilePath": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj",
|
"projectFilePath": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj",
|
||||||
"expectedPackageFiles": [
|
"expectedPackageFiles": [
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# AeroMC Capes
|
||||||
|
|
||||||
|
Shows the **custom capes (including HD up to 1024×512)** of players who designed their cape in the
|
||||||
|
[AeroMC Launcher](https://aeromc.viper.ipv64.net) – right in the game, even if you use a different
|
||||||
|
launcher yourself.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
- Drop the mod into your `mods` folder – done. No configuration, no Fabric API required.
|
||||||
|
- Whenever Minecraft loads a player's skin, the mod asks the AeroMC server whether that player has
|
||||||
|
an AeroMC cape and shows it instead of the regular cape.
|
||||||
|
- If a player has no AeroMC cape, nothing changes (official Mojang cape or none).
|
||||||
|
- **Client-side only** – servers don't need anything.
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
- **Fabric** and **Quilt**
|
||||||
|
- **One jar for all versions**: Minecraft 1.14 to 26.x
|
||||||
|
- Works with old Fabric Loader versions too (0.10 and newer)
|
||||||
|
- Players using the AeroMC Launcher don't need this mod (the launcher already does this on its
|
||||||
|
own) – but having it installed doesn't hurt either.
|
||||||
|
|
||||||
|
## Privacy
|
||||||
|
The mod only sends the (public) UUIDs of players whose skins are currently being loaded to
|
||||||
|
`viper-network.de` to look up their capes. Results are cached for a few minutes.
|
||||||
|
No account or login data is ever transmitted.
|
||||||
|
|
||||||
|
## Create your own cape
|
||||||
|
In the AeroMC Launcher: click your account name (top right) → **Skin ändern…** (Change skin) →
|
||||||
|
in the **Umhang** (Cape) section click **Gestalten…** (Design). You can paint your cape pixel by
|
||||||
|
pixel or insert any image, in sizes from 64×32 up to HD 1024×512.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Hinweise zum Hochladen (nicht Teil der Beschreibung)
|
||||||
|
- Name: AeroMC Capes (URL bleibt `aeromc-umhaenge`)
|
||||||
|
- Kurzbeschreibung: Shows custom capes (HD up to 1024×512) made in the AeroMC Launcher – even if you use another launcher. One jar for 1.14–26.x, client-side.
|
||||||
|
- Datei: `dist/aeromc-umhaenge-fabric-1.0.0.jar`
|
||||||
|
- Loader: Fabric, Quilt · Umgebung: Client · Server: nicht nötig
|
||||||
|
- Spielversionen: alle Releases von 1.14 bis zur aktuellen
|
||||||
|
- Kategorien: Sozial (hervorgehoben), Werkzeug („Cosmetic“ gibt es für Mods nicht)
|
||||||
|
- Abhängigkeiten: keine
|
||||||
|
- Lizenz: aktuell „All Rights Reserved“ (in `fabric.mod.json`) – Modrinth akzeptiert das, eine
|
||||||
|
offene Lizenz (z. B. MIT) wirkt aber vertrauenswürdiger.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"required": false,
|
||||||
|
"minVersion": "0.8",
|
||||||
|
"package": "de.aeromc.capes.mixin",
|
||||||
|
"compatibilityLevel": "JAVA_8",
|
||||||
|
"client": ["SessionServiceMixin", "SkinLaderMixin"],
|
||||||
|
"injectors": {
|
||||||
|
"defaultRequire": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"id": "aeromc-capes",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"name": "AeroMC Umhänge",
|
||||||
|
"description": "Zeigt die eigenen Umhänge (auch HD) von AeroMC-Spielern im Spiel – gestaltet im AeroMC Launcher. Nur auf dem Client nötig. Funktioniert mit jeder Minecraft-Version, die Fabric unterstützt.",
|
||||||
|
"authors": ["M_Viper"],
|
||||||
|
"contact": {
|
||||||
|
"homepage": "https://aeromc.viper.ipv64.net"
|
||||||
|
},
|
||||||
|
"license": "LicenseRef-All-Rights-Reserved",
|
||||||
|
"icon": "assets/aeromc-capes/icon.png",
|
||||||
|
"environment": "client",
|
||||||
|
"mixins": ["aeromc-capes.mixins.json"],
|
||||||
|
"depends": {
|
||||||
|
"fabricloader": ">=0.10.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package de.aeromc.capes.mixin;
|
||||||
|
|
||||||
|
import com.mojang.authlib.GameProfile;
|
||||||
|
import com.mojang.authlib.properties.Property;
|
||||||
|
import com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService;
|
||||||
|
import de.aeromc.capes.mod.Umhaenge;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hängt sich an die Textur-Methoden von authlib (beide Fassungen, siehe
|
||||||
|
* agent/…/Umbau.java). require = 0: Es greift immer nur die Methode, die es in
|
||||||
|
* der jeweiligen authlib-Fassung gibt, die andere wird still übersprungen.
|
||||||
|
* remap = false: authlib ist nicht verschleiert, Namen bleiben wie sie sind.
|
||||||
|
*/
|
||||||
|
@Mixin(value = YggdrasilMinecraftSessionService.class, remap = false)
|
||||||
|
public abstract class SessionServiceMixin {
|
||||||
|
|
||||||
|
// wird in den statischen Initialisierer von authlib übernommen: zeigt im Spielprotokoll, dass der Mod greift
|
||||||
|
static {
|
||||||
|
Umhaenge.authlibGreift = true;
|
||||||
|
Umhaenge.meldeAktiv("authlib");
|
||||||
|
}
|
||||||
|
|
||||||
|
// authlib 5+ (Minecraft 1.20.2 … 26.x)
|
||||||
|
@Inject(method = "unpackTextures", at = @At("RETURN"), cancellable = true, remap = false, require = 0)
|
||||||
|
private void aeromc$neu(Property eigenschaft, CallbackInfoReturnable<Object> cir) {
|
||||||
|
aeromc$ersetze(cir, eigenschaft);
|
||||||
|
}
|
||||||
|
|
||||||
|
// authlib 1–4 (Minecraft 1.14 … 1.20.1)
|
||||||
|
@Inject(method = "getTextures(Lcom/mojang/authlib/GameProfile;Z)Ljava/util/Map;", at = @At("RETURN"), cancellable = true, remap = false, require = 0)
|
||||||
|
private void aeromc$alt(GameProfile profil, boolean sicher, CallbackInfoReturnable<Object> cir) {
|
||||||
|
aeromc$ersetze(cir, profil);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void aeromc$ersetze(CallbackInfoReturnable<Object> cir, Object argument) {
|
||||||
|
try {
|
||||||
|
Object alt = cir.getReturnValue();
|
||||||
|
Object neu = Umhaenge.mitUmhang(alt, argument);
|
||||||
|
if (neu != alt) cir.setReturnValue(neu);
|
||||||
|
} catch (Throwable ignoriert) {
|
||||||
|
// im Zweifel bleibt alles wie ohne Mod
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package de.aeromc.capes.mixin;
|
||||||
|
|
||||||
|
import com.mojang.authlib.GameProfile;
|
||||||
|
import com.mojang.authlib.minecraft.MinecraftSessionService;
|
||||||
|
import de.aeromc.capes.mod.Umhaenge;
|
||||||
|
import net.minecraft.class_1071;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zweiter Weg für alte Fabric Loader (vor 0.12): Die lassen Mixins nur in
|
||||||
|
* Minecraft selbst zu, nicht in authlib – SessionServiceMixin greift dort also
|
||||||
|
* nie. Deshalb wird hier der Aufruf von getTextures im Skin-Lader von Minecraft
|
||||||
|
* (PlayerSkinProvider = class_1071, Intermediary-Name, gleich in 1.14 … 1.20.1)
|
||||||
|
* umgeleitet. Bei neuen Loadern erledigt das bereits SessionServiceMixin, dann
|
||||||
|
* reicht diese Umleitung das Ergebnis nur unverändert durch.
|
||||||
|
*/
|
||||||
|
@Mixin(value = class_1071.class, targets = "net.minecraft.class_1071$1", remap = false)
|
||||||
|
public abstract class SkinLaderMixin {
|
||||||
|
|
||||||
|
@Redirect(method = "*", at = @At(value = "INVOKE", remap = false,
|
||||||
|
target = "Lcom/mojang/authlib/minecraft/MinecraftSessionService;getTextures(Lcom/mojang/authlib/GameProfile;Z)Ljava/util/Map;"),
|
||||||
|
remap = false, require = 0)
|
||||||
|
private Map aeromc$texturen(MinecraftSessionService dienst, GameProfile profil, boolean sicher) {
|
||||||
|
Map texturen = dienst.getTextures(profil, sicher);
|
||||||
|
if (Umhaenge.authlibGreift) return texturen;
|
||||||
|
Umhaenge.meldeAktiv("Skin-Lader");
|
||||||
|
try {
|
||||||
|
Object neu = Umhaenge.mitUmhang(texturen, profil);
|
||||||
|
return neu instanceof Map ? (Map) neu : texturen;
|
||||||
|
} catch (Throwable ignoriert) {
|
||||||
|
return texturen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package de.aeromc.capes.mod;
|
||||||
|
|
||||||
|
import de.aeromc.capes.Nachschlagen;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AeroMC Umhänge – Fabric/Quilt-Mod für Spieler, die NICHT den AeroMC Launcher
|
||||||
|
* nutzen. Er macht dasselbe wie der Umhang-Agent des Launchers: In Mojangs
|
||||||
|
* authlib wird der AeroMC-Umhang eines Spielers eingesetzt, sobald Minecraft
|
||||||
|
* dessen Skin lädt. Da nur authlib angepasst wird (nicht Minecraft selbst),
|
||||||
|
* passt ein und derselbe Mod zu jeder Minecraft-Version, die Fabric kennt.
|
||||||
|
* Alte Fabric Loader (vor 0.12) erlauben keine Mixins in authlib, dort greift
|
||||||
|
* stattdessen SkinLaderMixin im Skin-Lader von Minecraft.
|
||||||
|
*/
|
||||||
|
public final class Umhaenge {
|
||||||
|
private static final Nachschlagen DIENST = new Nachschlagen(Nachschlagen.STANDARD_BASIS);
|
||||||
|
|
||||||
|
/** true, sobald SessionServiceMixin in authlib eingebaut wurde (dann ist SkinLaderMixin überflüssig). */
|
||||||
|
public static volatile boolean authlibGreift;
|
||||||
|
private static volatile boolean gemeldet;
|
||||||
|
|
||||||
|
private Umhaenge() {}
|
||||||
|
|
||||||
|
/** Schreibt einmalig ins Spielprotokoll, dass (und über welchen Weg) der Mod greift. */
|
||||||
|
public static void meldeAktiv(String weg) {
|
||||||
|
if (gemeldet || agentAktiv()) return;
|
||||||
|
gemeldet = true;
|
||||||
|
System.out.println("[AeroMC] Umhang-Mod aktiv (" + weg + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Läuft das Spiel über den AeroMC Launcher, setzt schon dessen Agent die Umhänge ein. */
|
||||||
|
public static boolean agentAktiv() {
|
||||||
|
return System.getProperties().containsKey("aeromc.capes.hook");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Object mitUmhang(Object ergebnis, Object argument) {
|
||||||
|
if (agentAktiv()) return ergebnis;
|
||||||
|
return DIENST.apply(ergebnis, argument);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package net.minecraft;
|
||||||
|
|
||||||
|
/** Nur zum Kompilieren: Platzhalter für Minecrafts PlayerSkinProvider (Intermediary-Name). Kommt nicht ins Mod-Jar. */
|
||||||
|
public class class_1071 {}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "aeromc",
|
"name": "aeromc",
|
||||||
"version": "1.1.5",
|
"version": "1.1.7",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "aeromc",
|
"name": "aeromc",
|
||||||
"version": "1.1.5",
|
"version": "1.1.7",
|
||||||
"license": "SEE LICENSE IN LICENSE",
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@xhayper/discord-rpc": "^1.3.4",
|
"@xhayper/discord-rpc": "^1.3.4",
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
"loopback-capture": "^3.0.2",
|
"loopback-capture": "^3.0.2",
|
||||||
"node-window-manager": "^2.2.4",
|
"node-window-manager": "^2.2.4",
|
||||||
"skinview3d": "^3.4.2",
|
"skinview3d": "^3.4.2",
|
||||||
|
"sql.js": "^1.14.2",
|
||||||
"undici": "^6.28.0",
|
"undici": "^6.28.0",
|
||||||
"unzipper": "^0.12.5"
|
"unzipper": "^0.12.5"
|
||||||
},
|
},
|
||||||
@@ -4346,6 +4347,12 @@
|
|||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"optional": true
|
"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": {
|
"node_modules/stat-mode": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "aeromc",
|
"name": "aeromc",
|
||||||
"productName": "AeroMC",
|
"productName": "AeroMC",
|
||||||
"version": "1.1.5",
|
"version": "1.1.7",
|
||||||
"description": "AeroMC – Instanz-basierter Minecraft-Launcher (à la MultiMC/Prism)",
|
"description": "AeroMC – Instanz-basierter Minecraft-Launcher (à la MultiMC/Prism)",
|
||||||
"homepage": "https://m-viper.de",
|
"homepage": "https://m-viper.de",
|
||||||
"launcherWebsite": "https://aeromc.viper.ipv64.net",
|
"launcherWebsite": "https://aeromc.viper.ipv64.net",
|
||||||
@@ -47,6 +47,7 @@
|
|||||||
"loopback-capture": "^3.0.2",
|
"loopback-capture": "^3.0.2",
|
||||||
"node-window-manager": "^2.2.4",
|
"node-window-manager": "^2.2.4",
|
||||||
"skinview3d": "^3.4.2",
|
"skinview3d": "^3.4.2",
|
||||||
|
"sql.js": "^1.14.2",
|
||||||
"undici": "^6.28.0",
|
"undici": "^6.28.0",
|
||||||
"unzipper": "^0.12.5"
|
"unzipper": "^0.12.5"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* build-agent.js – baut src/assets/agent/aeromc-capes.jar (AeroMC-Umhang-Agent)
|
||||||
|
* ------------------------------------------------------------------------------
|
||||||
|
* Braucht ein JDK (javac/java, ab 11) und ASM 9 – beides wird nur beim Bauen
|
||||||
|
* gebraucht, die fertige Datei liegt im Projekt. Gesucht wird das JDK über
|
||||||
|
* JAVA_HOME bzw. unter "C:\Program Files\Eclipse Adoptium", ASM im
|
||||||
|
* Bibliotheksordner von AeroMC (dort legen Fabric/Forge es ohnehin ab).
|
||||||
|
* Der Agent wird für Java 8 übersetzt, läuft also mit jeder Minecraft-Version.
|
||||||
|
*
|
||||||
|
* Aufruf: node scripts/build-agent.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const os = require('os');
|
||||||
|
const path = require('path');
|
||||||
|
const { execFileSync } = require('child_process');
|
||||||
|
|
||||||
|
const WURZEL = path.join(__dirname, '..');
|
||||||
|
const ZIEL = path.join(WURZEL, 'src', 'assets', 'agent', 'aeromc-capes.jar');
|
||||||
|
|
||||||
|
function findeJdk() {
|
||||||
|
const kandidaten = [];
|
||||||
|
if (process.env.JAVA_HOME) kandidaten.push(process.env.JAVA_HOME);
|
||||||
|
const adoptium = 'C:\\Program Files\\Eclipse Adoptium';
|
||||||
|
if (fs.existsSync(adoptium)) {
|
||||||
|
for (const d of fs.readdirSync(adoptium).filter((n) => /^jdk-/.test(n)).sort().reverse()) kandidaten.push(path.join(adoptium, d));
|
||||||
|
}
|
||||||
|
const jdk = kandidaten.find((d) => fs.existsSync(path.join(d, 'bin', process.platform === 'win32' ? 'javac.exe' : 'javac')));
|
||||||
|
if (!jdk) throw new Error('Kein JDK gefunden (JAVA_HOME setzen).');
|
||||||
|
return jdk;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findeAsm(name) {
|
||||||
|
const basis = path.join(process.env.APPDATA || os.homedir(), 'aeromc', 'shared', 'libraries', 'org', 'ow2', 'asm', name);
|
||||||
|
if (!fs.existsSync(basis)) throw new Error(`ASM (${name}) nicht gefunden unter ${basis}`);
|
||||||
|
const versionen = fs.readdirSync(basis).filter((v) => /^9\./.test(v))
|
||||||
|
.sort((a, b) => b.split('.').map(Number).reduce((s, x, i) => s || x - (a.split('.').map(Number)[i] || 0), 0));
|
||||||
|
const datei = path.join(basis, versionen[0], `${name}-${versionen[0]}.jar`);
|
||||||
|
if (!fs.existsSync(datei)) throw new Error('ASM-Datei fehlt: ' + datei);
|
||||||
|
return datei;
|
||||||
|
}
|
||||||
|
|
||||||
|
function quellen(ordner) {
|
||||||
|
return fs.readdirSync(ordner, { withFileTypes: true }).flatMap((e) => {
|
||||||
|
const p = path.join(ordner, e.name);
|
||||||
|
return e.isDirectory() ? quellen(p) : (e.name.endsWith('.java') ? [p] : []);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const jdk = findeJdk();
|
||||||
|
const javac = path.join(jdk, 'bin', 'javac');
|
||||||
|
const java = path.join(jdk, 'bin', 'java');
|
||||||
|
const asm = findeAsm('asm');
|
||||||
|
const asmCommons = findeAsm('asm-commons');
|
||||||
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'aeromc-agent-'));
|
||||||
|
const klassen = path.join(tmp, 'klassen');
|
||||||
|
const werkzeug = path.join(tmp, 'werkzeug');
|
||||||
|
console.log('JDK:', jdk);
|
||||||
|
console.log('ASM:', asm);
|
||||||
|
try {
|
||||||
|
execFileSync(javac, ['--release', '8', '-nowarn', '-encoding', 'UTF-8', '-cp', asm, '-d', klassen,
|
||||||
|
...quellen(path.join(WURZEL, 'agent', 'src'))], { stdio: 'inherit' });
|
||||||
|
execFileSync(javac, ['-encoding', 'UTF-8', '-cp', [asm, asmCommons].join(path.delimiter), '-d', werkzeug,
|
||||||
|
path.join(WURZEL, 'agent', 'tools', 'Shade.java')], { stdio: 'inherit' });
|
||||||
|
fs.mkdirSync(path.dirname(ZIEL), { recursive: true });
|
||||||
|
execFileSync(java, ['-cp', [werkzeug, asm, asmCommons].join(path.delimiter), 'Shade', ZIEL, klassen, asm], { stdio: 'inherit' });
|
||||||
|
console.log(`Fertig: ${ZIEL} (${fs.statSync(ZIEL).size} Bytes)`);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tmp, { recursive: true, force: true });
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* build-mod.js – baut den Fabric/Quilt-Mod "AeroMC Umhänge" (dist/aeromc-umhaenge-fabric-<version>.jar)
|
||||||
|
* -------------------------------------------------------------------------------------------------
|
||||||
|
* Für Spieler, die NICHT den AeroMC Launcher nutzen. Der Mod greift wie der
|
||||||
|
* Umhang-Agent nur in Mojangs authlib ein (Mixin, ohne Namensumsetzung) – darum
|
||||||
|
* braucht es weder Gradle noch Fabric Loom, und eine Datei passt zu allen
|
||||||
|
* Minecraft-Versionen, die Fabric unterstützt. Übersetzt wird für Java 8.
|
||||||
|
*
|
||||||
|
* Gebraucht werden (nur beim Bauen) ein JDK sowie sponge-mixin und authlib aus
|
||||||
|
* dem Bibliotheksordner von AeroMC. Der Nachschlage-Code ist derselbe wie im
|
||||||
|
* Agenten (agent/src/de/aeromc/capes/Nachschlagen.java).
|
||||||
|
*
|
||||||
|
* Aufruf: node scripts/build-mod.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const os = require('os');
|
||||||
|
const path = require('path');
|
||||||
|
const { execFileSync } = require('child_process');
|
||||||
|
|
||||||
|
const WURZEL = path.join(__dirname, '..');
|
||||||
|
const LIB = path.join(process.env.APPDATA || os.homedir(), 'aeromc', 'shared', 'libraries');
|
||||||
|
const modInfo = JSON.parse(fs.readFileSync(path.join(WURZEL, 'mod', 'resources', 'fabric.mod.json'), 'utf8'));
|
||||||
|
const ZIEL = path.join(WURZEL, 'dist', `aeromc-umhaenge-fabric-${modInfo.version}.jar`);
|
||||||
|
|
||||||
|
function findeJdk() {
|
||||||
|
const kandidaten = [];
|
||||||
|
if (process.env.JAVA_HOME) kandidaten.push(process.env.JAVA_HOME);
|
||||||
|
const adoptium = 'C:\\Program Files\\Eclipse Adoptium';
|
||||||
|
if (fs.existsSync(adoptium)) {
|
||||||
|
for (const d of fs.readdirSync(adoptium).filter((n) => /^jdk-/.test(n)).sort().reverse()) kandidaten.push(path.join(adoptium, d));
|
||||||
|
}
|
||||||
|
const jdk = kandidaten.find((d) => fs.existsSync(path.join(d, 'bin', process.platform === 'win32' ? 'javac.exe' : 'javac')));
|
||||||
|
if (!jdk) throw new Error('Kein JDK gefunden (JAVA_HOME setzen).');
|
||||||
|
return jdk;
|
||||||
|
}
|
||||||
|
|
||||||
|
// neueste (oder mit aelteste=true älteste) Fassung einer Bibliothek im AeroMC-Bibliotheksordner
|
||||||
|
function bibliothek(gruppe, name, aelteste = false) {
|
||||||
|
const basis = path.join(LIB, ...gruppe.split('.'), name);
|
||||||
|
if (!fs.existsSync(basis)) throw new Error(`${name} nicht gefunden unter ${basis}`);
|
||||||
|
const fassungen = fs.readdirSync(basis).sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
|
||||||
|
const v = aelteste ? fassungen[0] : fassungen.pop();
|
||||||
|
const datei = path.join(basis, v, `${name}-${v}.jar`);
|
||||||
|
if (!fs.existsSync(datei)) throw new Error('Datei fehlt: ' + datei);
|
||||||
|
return datei;
|
||||||
|
}
|
||||||
|
|
||||||
|
function quellen(ordner) {
|
||||||
|
return fs.readdirSync(ordner, { withFileTypes: true }).flatMap((e) => {
|
||||||
|
const p = path.join(ordner, e.name);
|
||||||
|
return e.isDirectory() ? quellen(p) : (e.name.endsWith('.java') ? [p] : []);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// authlib in der ältesten Fassung: SkinLaderMixin ruft getTextures(GameProfile, boolean) auf, das es nur bis authlib 4 gibt
|
||||||
|
const jdk = findeJdk();
|
||||||
|
const klassenpfad = [bibliothek('net.fabricmc', 'sponge-mixin'), bibliothek('com.mojang', 'authlib', true)].join(path.delimiter);
|
||||||
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'aeromc-mod-'));
|
||||||
|
const inhalt = path.join(tmp, 'inhalt');
|
||||||
|
const stubs = path.join(tmp, 'stubs');
|
||||||
|
console.log('JDK:', jdk);
|
||||||
|
try {
|
||||||
|
// Platzhalter für Minecraft-Klassen (nur zum Kompilieren, landen nicht im Jar)
|
||||||
|
execFileSync(path.join(jdk, 'bin', 'javac'), ['--release', '8', '-nowarn', '-encoding', 'UTF-8', '-d', stubs,
|
||||||
|
...quellen(path.join(WURZEL, 'mod', 'stubs'))], { stdio: 'inherit' });
|
||||||
|
execFileSync(path.join(jdk, 'bin', 'javac'), ['--release', '8', '-nowarn', '-encoding', 'UTF-8', '-proc:none', '-cp', klassenpfad + path.delimiter + stubs, '-d', inhalt,
|
||||||
|
...quellen(path.join(WURZEL, 'mod', 'src')),
|
||||||
|
path.join(WURZEL, 'agent', 'src', 'de', 'aeromc', 'capes', 'Nachschlagen.java')], { stdio: 'inherit' });
|
||||||
|
for (const datei of fs.readdirSync(path.join(WURZEL, 'mod', 'resources'))) {
|
||||||
|
fs.copyFileSync(path.join(WURZEL, 'mod', 'resources', datei), path.join(inhalt, datei));
|
||||||
|
}
|
||||||
|
fs.mkdirSync(path.join(inhalt, 'assets', 'aeromc-capes'), { recursive: true });
|
||||||
|
fs.copyFileSync(path.join(WURZEL, 'src', 'assets', 'logo.png'), path.join(inhalt, 'assets', 'aeromc-capes', 'icon.png'));
|
||||||
|
fs.mkdirSync(path.dirname(ZIEL), { recursive: true });
|
||||||
|
if (fs.existsSync(ZIEL)) fs.rmSync(ZIEL);
|
||||||
|
execFileSync(path.join(jdk, 'bin', 'jar'), ['--create', '--file', ZIEL, '-C', inhalt, '.'], { stdio: 'inherit' });
|
||||||
|
console.log(`Fertig: ${ZIEL} (${fs.statSync(ZIEL).size} Bytes)`);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tmp, { recursive: true, force: true });
|
||||||
|
}
|
||||||
@@ -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 |
@@ -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-search 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"] #btn-search svg,: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}
|
||||||
@@ -200,7 +200,32 @@ async function sendReport(daten) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- AeroMC-Umhänge ----------
|
||||||
|
// Hochladen/Entfernen nur angemeldet; Abrufen ist öffentlich (auch für den Mod)
|
||||||
|
function uploadCape(uuid, image) {
|
||||||
|
return anfrage('/cape', { method: 'POST', body: { uuid, image } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeCape(uuid) {
|
||||||
|
return anfrage('/cape/remove', { method: 'POST', body: { uuid } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCapes(uuids) {
|
||||||
|
try {
|
||||||
|
const liste = [...new Set((uuids || []).map((u) => String(u).replace(/-/g, '').toLowerCase()))].filter(Boolean);
|
||||||
|
if (!liste.length) return { ok: true, capes: {} };
|
||||||
|
const { res, daten } = await roheAnfrage('/capes', { method: 'GET', body: { uuids: liste.join(',') } });
|
||||||
|
// ältere Plugin-Fassung ohne Umhänge
|
||||||
|
if (res.status === 404) return { ok: false, fehlt: true, message: 'Der Server kennt noch keine Umhänge (Plugin „AeroMC Freunde“ 1.5.0 nötig).' };
|
||||||
|
if (!res.ok) return { ok: false, message: (daten && daten.message) || `Fehler ${res.status}` };
|
||||||
|
return { ok: true, capes: (daten && daten.capes) || {} };
|
||||||
|
} catch (err) {
|
||||||
|
return { ok: false, message: 'Umhang-Server nicht erreichbar: ' + err.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
BASIS_URL, uploadCape, removeCape, getCapes,
|
||||||
heartbeat, getFriends, getRequests, getSentRequests, sendRequest, respond, cancelRequest, removeFriend,
|
heartbeat, getFriends, getRequests, getSentRequests, sendRequest, respond, cancelRequest, removeFriend,
|
||||||
sendInvite, getInvites, dismissInvite, createShare, getShare, setzeZugangsHelfer,
|
sendInvite, getInvites, dismissInvite, createShare, getShare, setzeZugangsHelfer,
|
||||||
sendMessage, getMessages, getUnreadMessages, markMessagesRead, sendReport,
|
sendMessage, getMessages, getUnreadMessages, markMessagesRead, sendReport,
|
||||||
|
|||||||
@@ -6,8 +6,15 @@
|
|||||||
content="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'; img-src 'self' data: https:; connect-src 'self';" />
|
content="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'; img-src 'self' data: https:; connect-src 'self';" />
|
||||||
<title>AeroMC</title>
|
<title>AeroMC</title>
|
||||||
<link rel="stylesheet" href="styles.css" />
|
<link rel="stylesheet" href="styles.css" />
|
||||||
|
<link rel="stylesheet" href="designs.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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) -->
|
<!-- Obere Werkzeugleiste (MultiMC-Stil: Icon + Text) -->
|
||||||
<header class="toolbar">
|
<header class="toolbar">
|
||||||
<button id="btn-new" class="tbtn" data-ic="plus" title="Neue Instanz (Strg+N)">Instanz hinzufügen</button>
|
<button id="btn-new" class="tbtn" data-ic="plus" title="Neue Instanz (Strg+N)">Instanz hinzufügen</button>
|
||||||
@@ -20,8 +27,9 @@
|
|||||||
<button id="btn-stats" class="tbtn" data-ic="chart" title="Spielzeit-Statistik">Statistik</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>
|
<button id="btn-friends" class="tbtn" data-ic="users" title="Freunde (ingame: Strg+Alt+F zeigt, wer online ist)">Freunde<span id="friends-badge" class="tb-badge hidden"></span></button>
|
||||||
<span class="tbgrow"></span>
|
<span class="tbgrow"></span>
|
||||||
<div class="search-wrap">
|
<div id="search-wrap" class="search-wrap">
|
||||||
<input id="search" type="text" placeholder="Suchen… (Strg+F)" autocomplete="off" title="Strg+F fokussiert die Suche" />
|
<button id="btn-search" class="tbtn tb-lupe" data-ic="search" title="Instanzen suchen (Strg+F)"></button>
|
||||||
|
<input id="search" type="text" placeholder="Suchen… (Esc schließt)" autocomplete="off" title="Instanzen nach Name, Version oder Gruppe filtern" />
|
||||||
</div>
|
</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-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>
|
<button id="btn-settings" class="tbtn" data-ic="settings" title="Einstellungen">Optionen</button>
|
||||||
@@ -250,14 +258,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="settings-grid">
|
<div class="settings-grid">
|
||||||
<section class="settings-card">
|
<section class="settings-card">
|
||||||
<h5>Oberfläche</h5>
|
<h5>Instanzliste</h5>
|
||||||
<label class="field">
|
|
||||||
<span>Design</span>
|
|
||||||
<select id="s-theme">
|
|
||||||
<option value="dark">Dunkel</option>
|
|
||||||
<option value="light">Hell</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Instanzen sortieren</span>
|
<span>Instanzen sortieren</span>
|
||||||
<select id="s-sortby">
|
<select id="s-sortby">
|
||||||
@@ -323,6 +324,37 @@
|
|||||||
<p>Optik der Oberfläche und visuelle Extras für die Instanzansicht.</p>
|
<p>Optik der Oberfläche und visuelle Extras für die Instanzansicht.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-grid">
|
<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">
|
<section class="settings-card">
|
||||||
<h5>Icons</h5>
|
<h5>Icons</h5>
|
||||||
<label class="checkbox-field">
|
<label class="checkbox-field">
|
||||||
@@ -350,6 +382,14 @@
|
|||||||
<option value="spooky">Unheimlich</option>
|
<option value="spooky">Unheimlich</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</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">
|
<label class="field">
|
||||||
<span>Deckkraft: <span id="s-bgskin-val">55</span> %</span>
|
<span>Deckkraft: <span id="s-bgskin-val">55</span> %</span>
|
||||||
<input id="s-bgskin-opacity" type="range" min="10" max="100" step="5" />
|
<input id="s-bgskin-opacity" type="range" min="10" max="100" step="5" />
|
||||||
@@ -406,6 +446,14 @@
|
|||||||
</label>
|
</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>
|
<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>
|
</section>
|
||||||
|
|
||||||
|
<section class="settings-card">
|
||||||
|
<h5>Umhänge</h5>
|
||||||
|
<label class="checkbox-field">
|
||||||
|
<input id="s-aeromc-capes" type="checkbox" /> <span>AeroMC-Umhänge im Spiel anzeigen</span>
|
||||||
|
</label>
|
||||||
|
<small class="hint">Zeigt deinen eigenen Umhang und die anderer AeroMC-Spieler – in jeder Minecraft-Version und mit jedem Loader, ohne Mod. Gestalten im Skin-Fenster unter „Umhang“. Gilt ab dem nächsten Start.</small>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1210,9 +1258,30 @@
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<p id="skin-status" class="hint"></p>
|
<p id="skin-status" class="hint"></p>
|
||||||
|
<div class="skin-umhang">
|
||||||
|
<div class="skin-label">Umhang</div>
|
||||||
|
<div class="field-row">
|
||||||
|
<label class="field">
|
||||||
|
<span>Offizieller Umhang <small class="opt">(im Spiel für alle sichtbar)</small></span>
|
||||||
|
<select id="skin-cape-offiziell" title="Nur Umhänge, die dein Konto bei Mojang besitzt">
|
||||||
|
<option value="">Kein Umhang</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<div class="field">
|
||||||
|
<span>AeroMC-Umhang <small class="opt">(eigener, auch HD)</small></span>
|
||||||
|
<div class="skin-umhang-aero">
|
||||||
|
<span id="skin-cape-aero" class="hint">–</span>
|
||||||
|
<button id="skin-cape-edit" class="btn btn-mini" title="Im Skin-Editor einen eigenen Umhang gestalten">Gestalten…</button>
|
||||||
|
<button id="skin-cape-remove" class="btn btn-mini btn-danger hidden">Entfernen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<small id="skin-cape-status" class="hint">Den AeroMC-Umhang sehen im Spiel alle, die AeroMC nutzen oder den Mod „AeroMC Umhänge“ (Fabric/Quilt) installiert haben – dort hat er Vorrang vor dem offiziellen.</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-foot">
|
<div class="modal-foot">
|
||||||
<button id="skin-reset" class="btn btn-danger">Auf Standard zurücksetzen</button>
|
<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>
|
<div class="spacer"></div>
|
||||||
<button class="btn" data-close>Schließen</button>
|
<button class="btn" data-close>Schließen</button>
|
||||||
<button id="skin-apply" class="btn btn-primary" disabled>Skin setzen</button>
|
<button id="skin-apply" class="btn btn-primary" disabled>Skin setzen</button>
|
||||||
@@ -1220,6 +1289,115 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
<button class="se-modus-btn" data-se-modus="umhang" title="Eigenen Umhang (auch HD) gestalten">Umhang</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 id="se-gruppe-ebene" 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 id="se-gruppe-umhang" class="se-gruppe hidden">
|
||||||
|
<div class="se-titel">Umhang</div>
|
||||||
|
<label class="se-feld"><span>Größe</span>
|
||||||
|
<select id="se-u-groesse" title="Größere Umhänge (HD) erlauben feinere Motive">
|
||||||
|
<option value="64">Normal (64×32)</option>
|
||||||
|
<option value="128">HD 128×64</option>
|
||||||
|
<option value="256">HD 256×128</option>
|
||||||
|
<option value="512">HD 512×256</option>
|
||||||
|
<option value="1024">HD 1024×512</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="se-feld"><span>Pinselgröße</span>
|
||||||
|
<select id="se-u-pinsel">
|
||||||
|
<option value="1">1 Pixel</option>
|
||||||
|
<option value="2">2 Pixel</option>
|
||||||
|
<option value="4">4 Pixel</option>
|
||||||
|
<option value="8">8 Pixel</option>
|
||||||
|
<option value="16">16 Pixel</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<button id="se-u-bild" class="btn btn-mini" title="Ein beliebiges Bild (PNG, JPG …) auf die Außenseite setzen">Bild einsetzen…</button>
|
||||||
|
<label class="se-check" title="Dieselbe Datei trägt auch die Elytra"><input id="se-u-elytra" type="checkbox" /> Als Elytra zeigen</label>
|
||||||
|
<button id="se-u-einpassen" class="btn btn-mini" title="Zoom zurücksetzen (Taste 0)">Ansicht einpassen</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>
|
||||||
|
<!-- gerade nicht gezeigte Ansichten (je nach Modus) -->
|
||||||
|
<div id="se-ablage" class="se-ablage">
|
||||||
|
<div id="se-u2d" class="se-ansicht se-u2d"><canvas id="se-u2d-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 id="se-tipp" 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 ===================== -->
|
<!-- ===================== Dialog: Logdateien ===================== -->
|
||||||
<div id="modal-logs" class="modal hidden">
|
<div id="modal-logs" class="modal hidden">
|
||||||
<div class="modal-card modal-lg">
|
<div class="modal-card modal-lg">
|
||||||
@@ -1364,6 +1542,7 @@
|
|||||||
<button id="import-tab-modpack" class="mtab" data-kind="modpack">Modpack</button>
|
<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-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-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-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-technic" class="mtab" data-kind="technic">Technic</button>
|
||||||
<button id="import-tab-code" class="mtab" data-kind="code">Per Code</button>
|
<button id="import-tab-code" class="mtab" data-kind="code">Per Code</button>
|
||||||
@@ -1460,6 +1639,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div id="import-pane-mmc" class="import-pane hidden">
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>MultiMC- oder Prism-Ordner</span>
|
<span>MultiMC- oder Prism-Ordner</span>
|
||||||
@@ -1540,6 +1744,7 @@
|
|||||||
<div id="mc-progress" class="hint hidden"></div>
|
<div id="mc-progress" class="hint hidden"></div>
|
||||||
<div id="mmc-progress" class="hint hidden"></div>
|
<div id="mmc-progress" class="hint hidden"></div>
|
||||||
<div id="cf-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="technic-progress" class="hint hidden"></div>
|
||||||
<div id="code-progress" class="hint hidden"></div>
|
<div id="code-progress" class="hint hidden"></div>
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
@@ -1548,6 +1753,7 @@
|
|||||||
<button id="import-modpack-do" class="btn btn-primary hidden">Modpack 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="mc-do" class="btn btn-primary hidden" disabled>Übernehmen</button>
|
||||||
<button id="cf-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="mmc-do" class="btn btn-primary hidden" disabled>Übernehmen</button>
|
||||||
<button id="technic-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>
|
<button id="code-do" class="btn btn-primary hidden" disabled>Instanz anlegen</button>
|
||||||
@@ -1656,5 +1862,6 @@
|
|||||||
<canvas id="skin-canvas" width="420" height="700" style="display:none"></canvas>
|
<canvas id="skin-canvas" width="420" height="700" style="display:none"></canvas>
|
||||||
<script src="vendor/skinview3d.js"></script>
|
<script src="vendor/skinview3d.js"></script>
|
||||||
<script src="renderer.js"></script>
|
<script src="renderer.js"></script>
|
||||||
|
<script src="skineditor.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -344,6 +344,8 @@ function buildArgs(vj, ctx) {
|
|||||||
// RAM
|
// RAM
|
||||||
jvm = ['-Xms' + ctx.minMemMb + 'M', '-Xmx' + ctx.maxMemMb + 'M', ...jvm];
|
jvm = ['-Xms' + ctx.minMemMb + 'M', '-Xmx' + ctx.maxMemMb + 'M', ...jvm];
|
||||||
if (ctx.extraArgs) jvm = [...ctx.extraArgs.split(' ').filter(Boolean), ...jvm];
|
if (ctx.extraArgs) jvm = [...ctx.extraArgs.split(' ').filter(Boolean), ...jvm];
|
||||||
|
// AeroMC-Umhang-Agent (zeigt eigene Umhänge im Spiel, siehe agent/)
|
||||||
|
if (ctx.javaAgent) jvm = ['-javaagent:' + ctx.javaAgent, ...jvm];
|
||||||
|
|
||||||
return [...jvm, vj.mainClass, ...game];
|
return [...jvm, vj.mainClass, ...game];
|
||||||
}
|
}
|
||||||
@@ -528,6 +530,7 @@ async function prepareAndLaunch(opts, onProgress) {
|
|||||||
minMemMb: opts.minMemMb || 2048,
|
minMemMb: opts.minMemMb || 2048,
|
||||||
maxMemMb: opts.maxMemMb || 4096,
|
maxMemMb: opts.maxMemMb || 4096,
|
||||||
extraArgs: opts.extraArgs || '',
|
extraArgs: opts.extraArgs || '',
|
||||||
|
javaAgent: opts.javaAgent || '',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fenstergröße bzw. maximiert starten (Minecraft-Argumente)
|
// Fenstergröße bzw. maximiert starten (Minecraft-Argumente)
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const bedrock = require('./bedrock');
|
|||||||
const backup = require('./backup');
|
const backup = require('./backup');
|
||||||
const mmcimport = require('./mmcimport');
|
const mmcimport = require('./mmcimport');
|
||||||
const cfimport = require('./cfimport');
|
const cfimport = require('./cfimport');
|
||||||
|
const mrimport = require('./mrimport');
|
||||||
const mcimport = require('./mcimport');
|
const mcimport = require('./mcimport');
|
||||||
const technicimport = require('./technicimport');
|
const technicimport = require('./technicimport');
|
||||||
const technicdiscover = require('./technicdiscover');
|
const technicdiscover = require('./technicdiscover');
|
||||||
@@ -112,6 +113,7 @@ const POPOUT_SPEC = {
|
|||||||
screens: { title: 'AeroMC – Bildschirmfotos', width: 860, height: 680, minWidth: 640, minHeight: 480 },
|
screens: { title: 'AeroMC – Bildschirmfotos', width: 860, height: 680, minWidth: 640, minHeight: 480 },
|
||||||
servers: { title: 'AeroMC – Server', width: 860, height: 680, minWidth: 640, minHeight: 480 },
|
servers: { title: 'AeroMC – Server', width: 860, height: 680, minWidth: 640, minHeight: 480 },
|
||||||
skin: { title: 'AeroMC – Skin', width: 860, height: 680, minWidth: 640, minHeight: 480 },
|
skin: { title: 'AeroMC – Skin', width: 860, height: 680, minWidth: 640, minHeight: 480 },
|
||||||
|
skineditor: { title: 'AeroMC – Skin-Editor', width: 1180, height: 800, minWidth: 920, minHeight: 600 },
|
||||||
bedrock: { title: 'AeroMC – Bedrock', width: 860, height: 680, minWidth: 640, minHeight: 480 },
|
bedrock: { title: 'AeroMC – Bedrock', width: 860, height: 680, minWidth: 640, minHeight: 480 },
|
||||||
login: { title: 'AeroMC – Konten', width: 460, height: 460, minWidth: 420, minHeight: 380 },
|
login: { title: 'AeroMC – Konten', width: 460, height: 460, minWidth: 420, minHeight: 380 },
|
||||||
import: { title: 'AeroMC – Import', width: 860, height: 680, minWidth: 640, minHeight: 480 },
|
import: { title: 'AeroMC – Import', width: 860, height: 680, minWidth: 640, minHeight: 480 },
|
||||||
@@ -171,7 +173,7 @@ function openPopout(modalId, params) {
|
|||||||
minWidth: spec.minWidth,
|
minWidth: spec.minWidth,
|
||||||
minHeight: spec.minHeight,
|
minHeight: spec.minHeight,
|
||||||
title: spec.title || POPOUT_DEFAULT.title,
|
title: spec.title || POPOUT_DEFAULT.title,
|
||||||
backgroundColor: '#1b1e24',
|
backgroundColor: fensterFarbe(),
|
||||||
icon: windowIcon(),
|
icon: windowIcon(),
|
||||||
frame: false, // eigener dunkler Kopfbereich (Titel + ✕) ersetzt die native Titelleiste
|
frame: false, // eigener dunkler Kopfbereich (Titel + ✕) ersetzt die native Titelleiste
|
||||||
show: false,
|
show: false,
|
||||||
@@ -211,6 +213,12 @@ ipcMain.on('recording:guard', (_e, active) => { recordingGuardActive = !!active;
|
|||||||
|
|
||||||
ipcMain.on('debug:rendererError', (_e, msg) => console.error('[Renderer-Fehler]', msg));
|
ipcMain.on('debug:rendererError', (_e, msg) => console.error('[Renderer-Fehler]', msg));
|
||||||
ipcMain.on('popout:open', (_e, modalId, params) => openPopout(modalId, params));
|
ipcMain.on('popout:open', (_e, modalId, params) => openPopout(modalId, params));
|
||||||
|
// Design gewechselt -> Fensterknöpfe oben rechts umfärben (nur Hauptfenster,
|
||||||
|
// die Popup-Fenster haben ihren eigenen Kopf)
|
||||||
|
ipcMain.on('window:titleBar', (e, design) => {
|
||||||
|
if (!mainWindow || mainWindow.isDestroyed() || e.sender !== mainWindow.webContents) return;
|
||||||
|
try { mainWindow.setTitleBarOverlay(titelleistenFarben(design)); } catch { /* ältere Windows-Fassung */ }
|
||||||
|
});
|
||||||
ipcMain.on('popout:hide', (e) => {
|
ipcMain.on('popout:hide', (e) => {
|
||||||
const w = BrowserWindow.fromWebContents(e.sender);
|
const w = BrowserWindow.fromWebContents(e.sender);
|
||||||
if (w && !w.isDestroyed()) w.hide();
|
if (w && !w.isDestroyed()) w.hide();
|
||||||
@@ -714,6 +722,28 @@ function verknuepfungsIcon(ziel) {
|
|||||||
return windowIcon() || ziel;
|
return windowIcon() || ziel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Eigene Titelleiste im Hauptfenster: Minimieren/Maximieren/Schließen bleiben
|
||||||
|
// echte Windows-Knöpfe (Andocken, Fensterlayouts …), Windows zeichnet sie aber
|
||||||
|
// in den Farben des Designs. Die Farben müssen zu --titel-bg/--titel-fg in
|
||||||
|
// styles.css bzw. designs.css passen.
|
||||||
|
const TITELLEISTE = {
|
||||||
|
dark: { color: '#2d2d30', symbolColor: '#e4e4e6' },
|
||||||
|
light: { color: '#f0f0f0', symbolColor: '#1c1c1c' },
|
||||||
|
aero: { color: '#0a0e1a', symbolColor: '#e8ecfb' },
|
||||||
|
lila: { color: '#0f0a1a', symbolColor: '#f0eafb' },
|
||||||
|
minecraft: { color: '#1e1e1f', symbolColor: '#ffffff' },
|
||||||
|
};
|
||||||
|
function titelleistenFarben(design) {
|
||||||
|
return Object.assign({ height: 32 }, TITELLEISTE[design] || TITELLEISTE.dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grundfarbe eines neuen Fensters passend zum Design – sonst blitzt beim
|
||||||
|
// Öffnen kurz die falsche Farbe auf, bis die Oberfläche gezeichnet ist
|
||||||
|
function fensterFarbe() {
|
||||||
|
const design = (store.getSettings() || {}).theme;
|
||||||
|
return { light: '#f0f0f0', aero: '#05070d', lila: '#08050f', minecraft: '#313233' }[design] || '#1b1e24';
|
||||||
|
}
|
||||||
|
|
||||||
function windowIcon() {
|
function windowIcon() {
|
||||||
const png = path.join(__dirname, 'assets', 'logo.png');
|
const png = path.join(__dirname, 'assets', 'logo.png');
|
||||||
const ico = path.join(__dirname, 'assets', 'icon.ico');
|
const ico = path.join(__dirname, 'assets', 'icon.ico');
|
||||||
@@ -728,10 +758,12 @@ function createWindow(startHidden) {
|
|||||||
height: 820,
|
height: 820,
|
||||||
minWidth: 940,
|
minWidth: 940,
|
||||||
minHeight: 640,
|
minHeight: 640,
|
||||||
backgroundColor: '#1b1e24',
|
backgroundColor: fensterFarbe(),
|
||||||
title: 'AeroMC',
|
title: 'AeroMC',
|
||||||
icon: windowIcon(),
|
icon: windowIcon(),
|
||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
|
titleBarStyle: 'hidden',
|
||||||
|
titleBarOverlay: titelleistenFarben((store.getSettings() || {}).theme),
|
||||||
show: !startHidden,
|
show: !startHidden,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: path.join(__dirname, 'preload.js'),
|
preload: path.join(__dirname, 'preload.js'),
|
||||||
@@ -800,7 +832,7 @@ function oeffneVorbereitetenImport() {
|
|||||||
try {
|
try {
|
||||||
const daten = JSON.parse(fs.readFileSync(datei, 'utf8').replace(/^/, ''));
|
const daten = JSON.parse(fs.readFileSync(datei, 'utf8').replace(/^/, ''));
|
||||||
quellen = (Array.isArray(daten.sources) ? daten.sources : [])
|
quellen = (Array.isArray(daten.sources) ? daten.sources : [])
|
||||||
.filter((q) => q && ['mc', 'cf', 'mmc', 'technic'].includes(q.kind))
|
.filter((q) => q && ['mc', 'cf', 'mr', 'mmc', 'technic'].includes(q.kind))
|
||||||
.map((q) => ({ kind: q.kind, path: typeof q.path === 'string' ? q.path : '' }));
|
.map((q) => ({ kind: q.kind, path: typeof q.path === 'string' ? q.path : '' }));
|
||||||
} catch { /* beschädigt -> verwerfen */ }
|
} catch { /* beschädigt -> verwerfen */ }
|
||||||
try { fs.unlinkSync(datei); } catch { /* egal */ }
|
try { fs.unlinkSync(datei); } catch { /* egal */ }
|
||||||
@@ -1028,9 +1060,8 @@ ipcMain.handle('settings:save', (_e, patch) => {
|
|||||||
// (die speichert bei jedem Klick auf eine Instanz mit) – sonst baut das
|
// (die speichert bei jedem Klick auf eine Instanz mit) – sonst baut das
|
||||||
// Hauptfenster bei jedem Klick seine ganze Liste neu auf und die flackert.
|
// Hauptfenster bei jedem Klick seine ganze Liste neu auf und die flackert.
|
||||||
const nurLetzteInstanz = patch && Object.keys(patch).every((k) => k === 'lastSelectedId');
|
const nurLetzteInstanz = patch && Object.keys(patch).every((k) => k === 'lastSelectedId');
|
||||||
if (!nurLetzteInstanz && mainWindow && !mainWindow.isDestroyed()) {
|
// an alle Fenster: das Design muss auch in offenen Popup-Fenstern wechseln
|
||||||
mainWindow.webContents.send('settings:changed', s);
|
if (!nurLetzteInstanz) broadcast('settings:changed', s);
|
||||||
}
|
|
||||||
return s;
|
return s;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1733,6 +1764,30 @@ async function aktivesToken() {
|
|||||||
return acc && acc.mcAccessToken ? acc.mcAccessToken : null;
|
return acc && acc.mcAccessToken ? acc.mcAccessToken : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Armform (breit/schmal) eines beliebigen Kontos – steht in den Textur-Angaben
|
||||||
|
// des öffentlichen Profils bei Mojang (SKIN.metadata.model = "slim").
|
||||||
|
// Ergebnis 10 Minuten merken, nach eigenen Skin-Änderungen sofort verwerfen.
|
||||||
|
const armformCache = new Map(); // uuid -> { modell, zeit }
|
||||||
|
ipcMain.handle('skin:modelFor', async (_e, uuid) => {
|
||||||
|
const id = String(uuid || '').replace(/-/g, '').toLowerCase();
|
||||||
|
if (!/^[0-9a-f]{32}$/.test(id)) return 'classic';
|
||||||
|
const gemerkt = armformCache.get(id);
|
||||||
|
if (gemerkt && Date.now() - gemerkt.zeit < 10 * 60 * 1000) return gemerkt.modell;
|
||||||
|
try {
|
||||||
|
const res = await fetch('https://sessionserver.mojang.com/session/minecraft/profile/' + id, { headers: { 'User-Agent': 'AeroMC-Launcher' } });
|
||||||
|
if (!res.ok) return gemerkt ? gemerkt.modell : 'classic';
|
||||||
|
const profil = await res.json();
|
||||||
|
const eigenschaft = (profil.properties || []).find((p) => p.name === 'textures');
|
||||||
|
const texturen = eigenschaft ? JSON.parse(Buffer.from(eigenschaft.value, 'base64').toString('utf8')) : {};
|
||||||
|
const skin = texturen.textures && texturen.textures.SKIN;
|
||||||
|
const modell = skin && skin.metadata && skin.metadata.model === 'slim' ? 'slim' : 'classic';
|
||||||
|
armformCache.set(id, { modell, zeit: Date.now() });
|
||||||
|
return modell;
|
||||||
|
} catch {
|
||||||
|
return gemerkt ? gemerkt.modell : 'classic';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('skin:profile', async () => {
|
ipcMain.handle('skin:profile', async () => {
|
||||||
const token = await aktivesToken();
|
const token = await aktivesToken();
|
||||||
if (!token) return { ok: false, message: 'Kein angemeldetes Konto.' };
|
if (!token) return { ok: false, message: 'Kein angemeldetes Konto.' };
|
||||||
@@ -1764,7 +1819,7 @@ ipcMain.handle('skin:apply', async (_e, dateiPfad, variante) => {
|
|||||||
if (!token) return { ok: false, message: 'Kein angemeldetes Konto.' };
|
if (!token) return { ok: false, message: 'Kein angemeldetes Konto.' };
|
||||||
try {
|
try {
|
||||||
const res = await skins.setzeSkin(token, dateiPfad, variante);
|
const res = await skins.setzeSkin(token, dateiPfad, variante);
|
||||||
if (res && res.ok) broadcast('skin:changed');
|
if (res && res.ok) { armformCache.clear(); broadcast('skin:changed'); }
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
catch (err) { return { ok: false, message: 'Skin konnte nicht gesetzt werden: ' + err.message }; }
|
catch (err) { return { ok: false, message: 'Skin konnte nicht gesetzt werden: ' + err.message }; }
|
||||||
@@ -1775,11 +1830,248 @@ ipcMain.handle('skin:reset', async () => {
|
|||||||
if (!token) return { ok: false, message: 'Kein angemeldetes Konto.' };
|
if (!token) return { ok: false, message: 'Kein angemeldetes Konto.' };
|
||||||
try {
|
try {
|
||||||
const res = await skins.setzeSkinZurueck(token);
|
const res = await skins.setzeSkinZurueck(token);
|
||||||
if (res && res.ok) broadcast('skin:changed');
|
if (res && res.ok) { armformCache.clear(); broadcast('skin:changed'); }
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
catch (err) { return { ok: false, message: 'Zurücksetzen fehlgeschlagen: ' + err.message }; }
|
catch (err) { return { ok: false, message: 'Zurücksetzen fehlgeschlagen: ' + err.message }; }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------- Skin-Editor ----------
|
||||||
|
// Der Editor malt in eine Canvas – fremde Bildadressen (textures.minecraft.net)
|
||||||
|
// würden sie "verunreinigen" und das Auslesen der Pixel verbieten. Deshalb holt
|
||||||
|
// der Hauptprozess die Datei und reicht sie als data:-Adresse weiter.
|
||||||
|
const PNG_KENNUNG = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
||||||
|
function skinPngAusBase64(base64) {
|
||||||
|
const daten = Buffer.from(String(base64 || ''), 'base64');
|
||||||
|
if (daten.length < 33 || !daten.subarray(0, 8).equals(PNG_KENNUNG)) return null;
|
||||||
|
if (daten.readUInt32BE(16) !== 64 || daten.readUInt32BE(20) !== 64) return null;
|
||||||
|
return daten;
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.handle('skin:currentData', async () => {
|
||||||
|
const token = await aktivesToken();
|
||||||
|
if (!token) return { ok: false, message: 'Kein angemeldetes Konto.' };
|
||||||
|
try {
|
||||||
|
const p = await skins.holeProfil(token);
|
||||||
|
if (!p.ok) return p;
|
||||||
|
if (!p.skinUrl) return { ok: false, standard: true, variante: p.variante, message: 'Das Konto nutzt den Standard-Skin.' };
|
||||||
|
const res = await fetch(p.skinUrl);
|
||||||
|
if (!res.ok) return { ok: false, message: 'Skin-Datei nicht abrufbar (HTTP ' + res.status + ').' };
|
||||||
|
const daten = Buffer.from(await res.arrayBuffer());
|
||||||
|
return { ok: true, name: p.name, variante: p.variante, dataUri: 'data:image/png;base64,' + daten.toString('base64') };
|
||||||
|
} catch (err) {
|
||||||
|
return { ok: false, message: 'Skin konnte nicht geladen werden: ' + err.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------- Umhänge (offiziell von Mojang + eigene AeroMC-Umhänge) ----------
|
||||||
|
// Bilder immer als data:-Adresse in die Oberfläche (fremde Server schicken
|
||||||
|
// keine CORS-Kopfzeilen – die 3D-Ansicht dürfte sie sonst nicht laden)
|
||||||
|
async function bildAlsDataUri(url) {
|
||||||
|
const res = await fetch(url, { headers: { 'User-Agent': 'AeroMC-Launcher' } });
|
||||||
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||||
|
const daten = Buffer.from(await res.arrayBuffer());
|
||||||
|
if (daten.length > 6 * 1024 * 1024) throw new Error('Bild zu groß');
|
||||||
|
return 'data:image/png;base64,' + daten.toString('base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
// PNG mit Seitenverhältnis 2:1 (64×32 … 1024×512) – wie der Server es verlangt
|
||||||
|
function umhangPngAusBase64(base64) {
|
||||||
|
const daten = Buffer.from(String(base64 || ''), 'base64');
|
||||||
|
if (daten.length < 33 || !daten.subarray(0, 8).equals(PNG_KENNUNG)) return null;
|
||||||
|
const breite = daten.readUInt32BE(16);
|
||||||
|
const hoehe = daten.readUInt32BE(20);
|
||||||
|
if (![64, 128, 256, 512, 1024].includes(breite) || breite !== 2 * hoehe) return null;
|
||||||
|
return daten;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Umhang eines beliebigen Spielers fürs Anzeigen: eigener AeroMC-Umhang hat
|
||||||
|
// Vorrang, sonst der offizielle aus Mojangs Profil. 5 Minuten gemerkt.
|
||||||
|
const umhangCache = new Map(); // uuid -> { dataUri, quelle, zeit }
|
||||||
|
async function umhangFuer(uuid) {
|
||||||
|
const id = String(uuid || '').replace(/-/g, '').toLowerCase();
|
||||||
|
if (!/^[0-9a-f]{32}$/.test(id)) return null;
|
||||||
|
const gemerkt = umhangCache.get(id);
|
||||||
|
if (gemerkt && Date.now() - gemerkt.zeit < 5 * 60 * 1000) return gemerkt;
|
||||||
|
let ergebnis = { dataUri: null, quelle: null, zeit: Date.now() };
|
||||||
|
try {
|
||||||
|
const aero = await friends.getCapes([id]);
|
||||||
|
const eintrag = aero.ok && aero.capes && aero.capes[id];
|
||||||
|
if (eintrag && eintrag.url) ergebnis = { dataUri: await bildAlsDataUri(eintrag.url), quelle: 'aeromc', zeit: Date.now() };
|
||||||
|
} catch { /* weiter mit Mojang */ }
|
||||||
|
if (!ergebnis.dataUri) {
|
||||||
|
try {
|
||||||
|
const res = await fetch('https://sessionserver.mojang.com/session/minecraft/profile/' + id, { headers: { 'User-Agent': 'AeroMC-Launcher' } });
|
||||||
|
if (res.ok) {
|
||||||
|
const profil = await res.json();
|
||||||
|
const eig = (profil.properties || []).find((p) => p.name === 'textures');
|
||||||
|
const tex = eig ? JSON.parse(Buffer.from(eig.value, 'base64').toString('utf8')) : {};
|
||||||
|
const cape = tex.textures && tex.textures.CAPE;
|
||||||
|
if (cape && cape.url) ergebnis = { dataUri: await bildAlsDataUri(skins.aufHttps(cape.url)), quelle: 'mojang', zeit: Date.now() };
|
||||||
|
}
|
||||||
|
} catch { /* kein Umhang */ }
|
||||||
|
}
|
||||||
|
umhangCache.set(id, ergebnis);
|
||||||
|
return ergebnis;
|
||||||
|
}
|
||||||
|
ipcMain.handle('cape:forUuid', async (_e, uuid) => {
|
||||||
|
const r = await umhangFuer(uuid);
|
||||||
|
return r && r.dataUri ? { dataUri: r.dataUri, quelle: r.quelle } : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Übersicht fürs Skin-Fenster: offizielle Umhänge + eigener AeroMC-Umhang
|
||||||
|
ipcMain.handle('cape:info', async () => {
|
||||||
|
const konto = aktivesFreundeKonto();
|
||||||
|
const token = await aktivesToken();
|
||||||
|
if (!konto || !token) return { ok: false, message: 'Kein angemeldetes Konto.' };
|
||||||
|
const ergebnis = { ok: true, name: konto.username, offiziell: [], aeromc: null, aeromcFehler: '' };
|
||||||
|
try {
|
||||||
|
const p = await skins.holeProfil(token);
|
||||||
|
if (p.ok) ergebnis.offiziell = p.capes || [];
|
||||||
|
} catch { /* ohne offizielle Liste weiter */ }
|
||||||
|
const aero = await friends.getCapes([konto.uuid]);
|
||||||
|
if (aero.ok) ergebnis.aeromc = aero.capes[String(konto.uuid).replace(/-/g, '').toLowerCase()] || null;
|
||||||
|
else { ergebnis.aeromcFehler = aero.message; ergebnis.aeromcFehlt = !!aero.fehlt; }
|
||||||
|
return ergebnis;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('cape:setOfficial', async (_e, capeId) => {
|
||||||
|
const token = await aktivesToken();
|
||||||
|
if (!token) return { ok: false, message: 'Kein angemeldetes Konto.' };
|
||||||
|
try {
|
||||||
|
const res = await skins.setzeUmhang(token, capeId || null);
|
||||||
|
if (res.ok) { umhangCache.clear(); broadcast('skin:changed', { quelle: 'umhang' }); }
|
||||||
|
return res;
|
||||||
|
} catch (err) { return { ok: false, message: 'Umhang konnte nicht gesetzt werden: ' + err.message }; }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Der Umhang-Agent muss als echte Datei vorliegen (Java liest nicht aus app.asar):
|
||||||
|
// einmal nach userData kopieren, Dateiname mit Größe, damit ein Update ihn ersetzt.
|
||||||
|
// Rückgabe: "pfad=serveradresse" für -javaagent, oder '' wenn es nicht klappt.
|
||||||
|
function umhangAgent() {
|
||||||
|
try {
|
||||||
|
const quelle = path.join(__dirname, 'assets', 'agent', 'aeromc-capes.jar');
|
||||||
|
const groesse = fs.statSync(quelle).size;
|
||||||
|
const ordner = path.join(app.getPath('userData'), 'agent');
|
||||||
|
const ziel = path.join(ordner, `aeromc-capes-${groesse}.jar`);
|
||||||
|
if (!fs.existsSync(ziel) || fs.statSync(ziel).size !== groesse) {
|
||||||
|
fs.mkdirSync(ordner, { recursive: true });
|
||||||
|
fs.writeFileSync(ziel, fs.readFileSync(quelle));
|
||||||
|
for (const alt of fs.readdirSync(ordner)) {
|
||||||
|
if (alt !== path.basename(ziel) && /^aeromc-capes-\d+\.jar$/.test(alt)) { try { fs.unlinkSync(path.join(ordner, alt)); } catch { /* noch in Benutzung */ } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `${ziel}=${friends.BASIS_URL}`;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Umhang-Agent nicht verfügbar:', err.message);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// verständliche Meldung, wenn der Server das neue Plugin noch nicht hat
|
||||||
|
function umhangFehler(res) {
|
||||||
|
if (res && res.code === 'rest_no_route') return { ok: false, message: 'Der Server kennt noch keine Umhänge (Plugin „AeroMC Freunde“ 1.5.0 nötig).' };
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.handle('cape:upload', async (_e, base64) => {
|
||||||
|
const konto = aktivesFreundeKonto();
|
||||||
|
if (!konto) return { ok: false, message: 'Kein angemeldetes Konto.' };
|
||||||
|
if (!umhangPngAusBase64(base64)) return { ok: false, message: 'Ungültiger Umhang (PNG mit 64×32 bis 1024×512 Pixeln nötig).' };
|
||||||
|
const res = await friends.uploadCape(konto.uuid, base64);
|
||||||
|
if (res.ok) { umhangCache.clear(); broadcast('skin:changed', { quelle: 'umhang' }); }
|
||||||
|
return res.ok ? { ok: true, cape: res.data && res.data.cape } : umhangFehler(res);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('cape:remove', async () => {
|
||||||
|
const konto = aktivesFreundeKonto();
|
||||||
|
if (!konto) return { ok: false, message: 'Kein angemeldetes Konto.' };
|
||||||
|
const res = await friends.removeCape(konto.uuid);
|
||||||
|
if (res.ok) { umhangCache.clear(); broadcast('skin:changed', { quelle: 'umhang' }); }
|
||||||
|
return res.ok ? res : umhangFehler(res);
|
||||||
|
});
|
||||||
|
|
||||||
|
// für den Editor: eigener Umhang, sonst der getragene offizielle
|
||||||
|
ipcMain.handle('cape:currentData', async () => {
|
||||||
|
const konto = aktivesFreundeKonto();
|
||||||
|
if (!konto) return { ok: false, message: 'Kein angemeldetes Konto.' };
|
||||||
|
umhangCache.delete(String(konto.uuid).replace(/-/g, '').toLowerCase());
|
||||||
|
const r = await umhangFuer(konto.uuid);
|
||||||
|
if (!r || !r.dataUri) return { ok: false, keiner: true, message: 'Dein Konto hat noch keinen Umhang.' };
|
||||||
|
return { ok: true, dataUri: r.dataUri, quelle: r.quelle };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bild wählen: art 'umhang' = fertige Umhang-Datei (PNG), 'bild' = beliebiges Motiv zum Einsetzen
|
||||||
|
ipcMain.handle('cape:chooseFile', async (e, art) => {
|
||||||
|
const motiv = art === 'bild';
|
||||||
|
const res = await dialog.showOpenDialog(BrowserWindow.fromWebContents(e.sender) || mainWindow, {
|
||||||
|
title: motiv ? 'Bild für den Umhang wählen' : 'Umhang-Datei öffnen',
|
||||||
|
properties: ['openFile'],
|
||||||
|
filters: motiv
|
||||||
|
? [{ name: 'Bilder', extensions: ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp'] }]
|
||||||
|
: [{ name: 'PNG-Bild', extensions: ['png'] }],
|
||||||
|
});
|
||||||
|
if (res.canceled || !res.filePaths.length) return { ok: false, canceled: true };
|
||||||
|
const pfad = res.filePaths[0];
|
||||||
|
try {
|
||||||
|
const st = fs.statSync(pfad);
|
||||||
|
if (st.size > 15 * 1024 * 1024) return { ok: false, message: 'Die Datei ist zu groß (über 15 MB).' };
|
||||||
|
const endung = path.extname(pfad).slice(1).toLowerCase().replace('jpg', 'jpeg');
|
||||||
|
return { ok: true, pfad, dataUri: `data:image/${endung || 'png'};base64,` + fs.readFileSync(pfad).toString('base64') };
|
||||||
|
} catch (err) { return { ok: false, message: 'Datei nicht lesbar: ' + err.message }; }
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('cape:saveData', async (e, base64) => {
|
||||||
|
const daten = umhangPngAusBase64(base64);
|
||||||
|
if (!daten) return { ok: false, message: 'Ungültige Umhang-Daten.' };
|
||||||
|
const res = await dialog.showSaveDialog(BrowserWindow.fromWebContents(e.sender) || mainWindow, {
|
||||||
|
title: 'Umhang speichern',
|
||||||
|
defaultPath: path.join(app.getPath('pictures'), 'umhang.png'),
|
||||||
|
filters: [{ name: 'PNG-Bild', extensions: ['png'] }],
|
||||||
|
});
|
||||||
|
if (res.canceled || !res.filePath) return { ok: false, canceled: true };
|
||||||
|
try { fs.writeFileSync(res.filePath, daten); return { ok: true, pfad: res.filePath }; }
|
||||||
|
catch (err) { return { ok: false, message: 'Speichern fehlgeschlagen: ' + err.message }; }
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('skin:readData', (_e, dateiPfad) => {
|
||||||
|
const geprueft = skins.pruefeDatei(dateiPfad);
|
||||||
|
if (!geprueft.ok) return geprueft;
|
||||||
|
try { return { ok: true, dataUri: 'data:image/png;base64,' + fs.readFileSync(dateiPfad).toString('base64') }; }
|
||||||
|
catch (err) { return { ok: false, message: 'Datei nicht lesbar: ' + err.message }; }
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('skin:saveData', async (e, base64) => {
|
||||||
|
const daten = skinPngAusBase64(base64);
|
||||||
|
if (!daten) return { ok: false, message: 'Ungültige Skin-Daten.' };
|
||||||
|
const res = await dialog.showSaveDialog(BrowserWindow.fromWebContents(e.sender) || mainWindow, {
|
||||||
|
title: 'Skin speichern',
|
||||||
|
defaultPath: path.join(app.getPath('pictures'), 'skin.png'),
|
||||||
|
filters: [{ name: 'PNG-Bild', extensions: ['png'] }],
|
||||||
|
});
|
||||||
|
if (res.canceled || !res.filePath) return { ok: false, canceled: true };
|
||||||
|
try { fs.writeFileSync(res.filePath, daten); return { ok: true, pfad: res.filePath }; }
|
||||||
|
catch (err) { return { ok: false, message: 'Speichern fehlgeschlagen: ' + err.message }; }
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('skin:applyData', async (_e, base64, variante) => {
|
||||||
|
const daten = skinPngAusBase64(base64);
|
||||||
|
if (!daten) return { ok: false, message: 'Ungültige Skin-Daten.' };
|
||||||
|
const token = await aktivesToken();
|
||||||
|
if (!token) return { ok: false, message: 'Kein angemeldetes Konto.' };
|
||||||
|
const tmp = path.join(app.getPath('temp'), 'aeromc-skin-editor.png');
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(tmp, daten);
|
||||||
|
const res = await skins.setzeSkin(token, tmp, variante);
|
||||||
|
if (res && res.ok) { armformCache.clear(); broadcast('skin:changed', { quelle: 'editor' }); }
|
||||||
|
return res;
|
||||||
|
} catch (err) {
|
||||||
|
return { ok: false, message: 'Skin konnte nicht gesetzt werden: ' + err.message };
|
||||||
|
} finally {
|
||||||
|
try { fs.unlinkSync(tmp); } catch { /* egal */ }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('auth:remove', (_e, id) => auth.removeAccount(id));
|
ipcMain.handle('auth:remove', (_e, id) => auth.removeAccount(id));
|
||||||
ipcMain.handle('auth:logout', () => { auth.clearAccount(); return true; });
|
ipcMain.handle('auth:logout', () => { auth.clearAccount(); return true; });
|
||||||
|
|
||||||
@@ -3480,6 +3772,7 @@ ipcMain.handle('instances:launch', async (_e, id, accountId, launchOpts) => {
|
|||||||
if (settings.autoModUpdates) modUpdates = await modUpdatesVorDemStart(id).catch(() => null);
|
if (settings.autoModUpdates) modUpdates = await modUpdatesVorDemStart(id).catch(() => null);
|
||||||
|
|
||||||
const opts = {
|
const opts = {
|
||||||
|
javaAgent: settings.aeromcCapes === false ? '' : umhangAgent(),
|
||||||
versionId: inst.minecraft.version,
|
versionId: inst.minecraft.version,
|
||||||
loader: inst.minecraft.loader || 'vanilla',
|
loader: inst.minecraft.loader || 'vanilla',
|
||||||
loaderVersion: inst.minecraft.loaderVersion || '',
|
loaderVersion: inst.minecraft.loaderVersion || '',
|
||||||
@@ -3732,6 +4025,87 @@ ipcMain.handle('cf:import', (_e, entries, copyData) => {
|
|||||||
return { ok: true, imported: done, failed };
|
return { ok: true, imported: done, failed };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------- Modrinth App ----------
|
||||||
|
ipcMain.handle('mr:pickFolder', async (e) => {
|
||||||
|
const res = await dialog.showOpenDialog(BrowserWindow.fromWebContents(e.sender) || mainWindow, {
|
||||||
|
title: 'Datenordner der Modrinth App wählen (meist %APPDATA%\\ModrinthApp)',
|
||||||
|
defaultPath: mrimport.standardOrdner(),
|
||||||
|
properties: ['openDirectory'],
|
||||||
|
});
|
||||||
|
return res.canceled ? null : res.filePaths[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('mr:scan', async (_e, folder) => {
|
||||||
|
try { return await mrimport.scan(folder); }
|
||||||
|
catch (err) { return { ok: false, reason: err.message }; }
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* War die Instanz in der Modrinth App mit einem Modrinth-Modpack verknüpft,
|
||||||
|
* übernehmen wir die Verknüpfung – samt der Liste der Dateien, die zum Paket
|
||||||
|
* gehören. Ohne diese Liste würde "Modpack aktualisieren" alte Mods liegen
|
||||||
|
* lassen; deshalb wird die Paketdatei einmal geladen und nur ihr Inhaltsverzeichnis
|
||||||
|
* gelesen. Klappt das nicht (offline …), bleibt die Instanz ohne Verknüpfung.
|
||||||
|
*/
|
||||||
|
async function verknuepfeModrinthModpack(instId, link) {
|
||||||
|
const kopf = { headers: { 'User-Agent': 'AeroMC-Launcher', Accept: 'application/json' } };
|
||||||
|
const vres = await fetch(`https://api.modrinth.com/v2/version/${encodeURIComponent(link.versionId)}`, kopf);
|
||||||
|
if (!vres.ok) return false;
|
||||||
|
const version = await vres.json();
|
||||||
|
const datei = (version.files || []).find((f) => f.primary) || (version.files || [])[0];
|
||||||
|
if (!datei || !datei.url) return false;
|
||||||
|
const dl = await fetch(datei.url, { headers: { 'User-Agent': 'AeroMC-Launcher' } });
|
||||||
|
if (!dl.ok) return false;
|
||||||
|
const buf = Buffer.from(await dl.arrayBuffer());
|
||||||
|
const zip = forge.readZipIndex(buf);
|
||||||
|
if (!zip['modrinth.index.json']) return false;
|
||||||
|
const index = JSON.parse(forge.readZipEntry(buf, zip['modrinth.index.json']).toString('utf8').replace(/^\uFEFF/, ''));
|
||||||
|
const dateien = (index.files || [])
|
||||||
|
.filter((f) => f.path && !(f.env && f.env.client === 'unsupported'))
|
||||||
|
.map((f) => String(f.path).replace(/\\/g, '/'));
|
||||||
|
for (const prefix of ['overrides/', 'client-overrides/']) {
|
||||||
|
for (const name of Object.keys(zip)) if (name.startsWith(prefix) && !name.endsWith('/')) dateien.push(name.slice(prefix.length));
|
||||||
|
}
|
||||||
|
let titel = index.name || '';
|
||||||
|
try {
|
||||||
|
const pres = await fetch(`https://api.modrinth.com/v2/project/${encodeURIComponent(link.projectId)}`, kopf);
|
||||||
|
if (pres.ok) titel = (await pres.json()).title || titel;
|
||||||
|
} catch { /* Titel ist nur Zierde */ }
|
||||||
|
store.updateInstance(instId, {
|
||||||
|
modpack: {
|
||||||
|
quelle: 'modrinth',
|
||||||
|
name: index.name || titel,
|
||||||
|
title: titel,
|
||||||
|
projectId: link.projectId,
|
||||||
|
versionId: link.versionId,
|
||||||
|
versionNummer: version.version_number || '',
|
||||||
|
dateien,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.handle('mr:import', async (_e, entries, copyData) => {
|
||||||
|
const done = [];
|
||||||
|
const failed = [];
|
||||||
|
const liste = entries || [];
|
||||||
|
for (const entry of liste) {
|
||||||
|
try {
|
||||||
|
const imported = mrimport.importOne(store, entry, copyData !== false);
|
||||||
|
if (entry.modpack && entry.modpack.projectId && entry.modpack.versionId) {
|
||||||
|
broadcast('mr:progress', { current: done.length + failed.length, total: liste.length, detail: 'Modpack-Verknüpfung für ' + entry.name });
|
||||||
|
try { imported.modpack = await verknuepfeModrinthModpack(imported.id, entry.modpack); }
|
||||||
|
catch { imported.modpack = false; }
|
||||||
|
}
|
||||||
|
done.push(imported);
|
||||||
|
}
|
||||||
|
catch (err) { failed.push({ name: entry.name, message: err.message }); }
|
||||||
|
broadcast('mr:progress', { current: done.length + failed.length, total: liste.length });
|
||||||
|
}
|
||||||
|
if (done.length) broadcast('instances:changed');
|
||||||
|
return { ok: true, imported: done, failed };
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('mc:pickFolder', async (e) => {
|
ipcMain.handle('mc:pickFolder', async (e) => {
|
||||||
const res = await dialog.showOpenDialog(BrowserWindow.fromWebContents(e.sender) || mainWindow, {
|
const res = await dialog.showOpenDialog(BrowserWindow.fromWebContents(e.sender) || mainWindow, {
|
||||||
title: 'Minecraft-Launcher-Ordner wählen (meist .minecraft)',
|
title: 'Minecraft-Launcher-Ordner wählen (meist .minecraft)',
|
||||||
|
|||||||
@@ -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 };
|
||||||
@@ -154,6 +154,19 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
skinChoose: () => ipcRenderer.invoke('skin:choose'),
|
skinChoose: () => ipcRenderer.invoke('skin:choose'),
|
||||||
skinApply: (pfad, variante) => ipcRenderer.invoke('skin:apply', pfad, variante),
|
skinApply: (pfad, variante) => ipcRenderer.invoke('skin:apply', pfad, variante),
|
||||||
skinReset: () => ipcRenderer.invoke('skin:reset'),
|
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),
|
||||||
|
capeInfo: () => ipcRenderer.invoke('cape:info'),
|
||||||
|
capeForUuid: (uuid) => ipcRenderer.invoke('cape:forUuid', uuid),
|
||||||
|
capeSetOfficial: (id) => ipcRenderer.invoke('cape:setOfficial', id),
|
||||||
|
capeUpload: (base64) => ipcRenderer.invoke('cape:upload', base64),
|
||||||
|
capeRemove: () => ipcRenderer.invoke('cape:remove'),
|
||||||
|
capeCurrentData: () => ipcRenderer.invoke('cape:currentData'),
|
||||||
|
capeChooseFile: (art) => ipcRenderer.invoke('cape:chooseFile', art),
|
||||||
|
capeSaveData: (base64) => ipcRenderer.invoke('cape:saveData', base64),
|
||||||
authRemove: (id) => ipcRenderer.invoke('auth:remove', id),
|
authRemove: (id) => ipcRenderer.invoke('auth:remove', id),
|
||||||
authLogin: () => ipcRenderer.invoke('auth:login'),
|
authLogin: () => ipcRenderer.invoke('auth:login'),
|
||||||
authLogout: () => ipcRenderer.invoke('auth:logout'),
|
authLogout: () => ipcRenderer.invoke('auth:logout'),
|
||||||
@@ -192,6 +205,14 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
return () => ipcRenderer.removeListener('mmc:progress', l);
|
return () => ipcRenderer.removeListener('mmc:progress', l);
|
||||||
},
|
},
|
||||||
cfPickFolder: () => ipcRenderer.invoke('cf:pickFolder'),
|
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),
|
cfScan: (folder) => ipcRenderer.invoke('cf:scan', folder),
|
||||||
cfImport: (entries, copyData) => ipcRenderer.invoke('cf:import', entries, copyData),
|
cfImport: (entries, copyData) => ipcRenderer.invoke('cf:import', entries, copyData),
|
||||||
onCfProgress: (cb) => {
|
onCfProgress: (cb) => {
|
||||||
@@ -269,6 +290,7 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
// Einstellungen/Konsole als eigenes, frei verschiebbares Fenster
|
// Einstellungen/Konsole als eigenes, frei verschiebbares Fenster
|
||||||
popoutOpen: (modalId, params) => ipcRenderer.send('popout:open', modalId, params),
|
popoutOpen: (modalId, params) => ipcRenderer.send('popout:open', modalId, params),
|
||||||
popoutHide: () => ipcRenderer.send('popout:hide'),
|
popoutHide: () => ipcRenderer.send('popout:hide'),
|
||||||
|
setTitleBarDesign: (design) => ipcRenderer.send('window:titleBar', design),
|
||||||
onPopoutReopen: (cb) => {
|
onPopoutReopen: (cb) => {
|
||||||
const l = (_e, params) => cb(params);
|
const l = (_e, params) => cb(params);
|
||||||
ipcRenderer.on('popout:reopen', l);
|
ipcRenderer.on('popout:reopen', l);
|
||||||
@@ -285,7 +307,7 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
return () => ipcRenderer.removeListener('instances:changed', l);
|
return () => ipcRenderer.removeListener('instances:changed', l);
|
||||||
},
|
},
|
||||||
onSkinChanged: (cb) => {
|
onSkinChanged: (cb) => {
|
||||||
const l = () => cb();
|
const l = (_e, info) => cb(info || {});
|
||||||
ipcRenderer.on('skin:changed', l);
|
ipcRenderer.on('skin:changed', l);
|
||||||
return () => ipcRenderer.removeListener('skin:changed', l);
|
return () => ipcRenderer.removeListener('skin:changed', l);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -537,13 +537,23 @@ function escapeHtml(s) {
|
|||||||
}
|
}
|
||||||
function numOrNull(v) { const n = parseInt(v, 10); return Number.isFinite(n) ? n : null; }
|
function numOrNull(v) { const n = parseInt(v, 10); return Number.isFinite(n) ? n : null; }
|
||||||
|
|
||||||
|
// Designs: 'dark'/'light' = Klassisch (MultiMC-Look), 'aero' = wie die Webseite,
|
||||||
|
// 'lila' = Aero in dunklem Lila, 'minecraft' = Block-Optik. Alle außer Klassisch
|
||||||
|
// bauen auf den dunklen Regeln auf (data-theme="dark") und überschreiben sie über data-design.
|
||||||
|
const DESIGNS = ['dark', 'light', 'aero', 'lila', 'minecraft'];
|
||||||
function applyTheme(theme) {
|
function applyTheme(theme) {
|
||||||
document.documentElement.setAttribute('data-theme', theme === 'light' ? 'light' : 'dark');
|
const t = DESIGNS.includes(theme) ? theme : 'dark';
|
||||||
|
const root = document.documentElement;
|
||||||
|
root.setAttribute('data-theme', t === 'light' ? 'light' : 'dark');
|
||||||
|
root.setAttribute('data-design', ['aero', 'lila', 'minecraft'].includes(t) ? t : 'klassisch');
|
||||||
|
if (!POPOUT && window.api.setTitleBarDesign) window.api.setTitleBarDesign(t);
|
||||||
|
document.querySelectorAll('.design-wahl').forEach((b) => b.classList.toggle('on', b.dataset.design === t));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Instanzen aus MultiMC übernehmen ----------
|
// ---------- Instanzen aus MultiMC übernehmen ----------
|
||||||
let mmcFound = [];
|
let mmcFound = [];
|
||||||
let cfFound = [];
|
let cfFound = [];
|
||||||
|
let mrFound = [];
|
||||||
let mcFound = [];
|
let mcFound = [];
|
||||||
let technicFound = [];
|
let technicFound = [];
|
||||||
let importKind = 'zip';
|
let importKind = 'zip';
|
||||||
@@ -568,8 +578,8 @@ async function ensureDefaultMcScan() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setImportKind(kind) {
|
function setImportKind(kind) {
|
||||||
importKind = ['zip', 'modpack', 'mc', 'cf', 'mmc', 'technic', 'code'].includes(kind) ? kind : 'zip';
|
importKind = ['zip', 'modpack', 'mc', 'cf', 'mr', 'mmc', 'technic', 'code'].includes(kind) ? kind : 'zip';
|
||||||
['zip', 'modpack', 'mc', 'cf', 'mmc', 'technic', 'code'].forEach((entryKind) => {
|
['zip', 'modpack', 'mc', 'cf', 'mr', 'mmc', 'technic', 'code'].forEach((entryKind) => {
|
||||||
el(`import-tab-${entryKind}`).classList.toggle('on', entryKind === importKind);
|
el(`import-tab-${entryKind}`).classList.toggle('on', entryKind === importKind);
|
||||||
el(`import-pane-${entryKind}`).classList.toggle('hidden', entryKind !== importKind);
|
el(`import-pane-${entryKind}`).classList.toggle('hidden', entryKind !== importKind);
|
||||||
});
|
});
|
||||||
@@ -577,12 +587,14 @@ function setImportKind(kind) {
|
|||||||
el('import-modpack-do').classList.toggle('hidden', importKind !== 'modpack');
|
el('import-modpack-do').classList.toggle('hidden', importKind !== 'modpack');
|
||||||
el('mc-do').classList.toggle('hidden', importKind !== 'mc');
|
el('mc-do').classList.toggle('hidden', importKind !== 'mc');
|
||||||
el('cf-do').classList.toggle('hidden', importKind !== 'cf');
|
el('cf-do').classList.toggle('hidden', importKind !== 'cf');
|
||||||
|
el('mr-do').classList.toggle('hidden', importKind !== 'mr');
|
||||||
el('mmc-do').classList.toggle('hidden', importKind !== 'mmc');
|
el('mmc-do').classList.toggle('hidden', importKind !== 'mmc');
|
||||||
el('technic-do').classList.toggle('hidden', importKind !== 'technic');
|
el('technic-do').classList.toggle('hidden', importKind !== 'technic');
|
||||||
el('code-do').classList.toggle('hidden', importKind !== 'code');
|
el('code-do').classList.toggle('hidden', importKind !== 'code');
|
||||||
el('code-progress').classList.toggle('hidden', importKind !== 'code' || !el('code-progress').textContent);
|
el('code-progress').classList.toggle('hidden', importKind !== 'code' || !el('code-progress').textContent);
|
||||||
el('mc-progress').classList.toggle('hidden', importKind !== 'mc' || !el('mc-progress').textContent);
|
el('mc-progress').classList.toggle('hidden', importKind !== 'mc' || !el('mc-progress').textContent);
|
||||||
el('cf-progress').classList.toggle('hidden', importKind !== 'cf' || !el('cf-progress').textContent);
|
el('cf-progress').classList.toggle('hidden', importKind !== 'cf' || !el('cf-progress').textContent);
|
||||||
|
el('mr-progress').classList.toggle('hidden', importKind !== 'mr' || !el('mr-progress').textContent);
|
||||||
el('mmc-progress').classList.toggle('hidden', importKind !== 'mmc' || !el('mmc-progress').textContent);
|
el('mmc-progress').classList.toggle('hidden', importKind !== 'mmc' || !el('mmc-progress').textContent);
|
||||||
el('technic-progress').classList.toggle('hidden', importKind !== 'technic' || !el('technic-progress').textContent);
|
el('technic-progress').classList.toggle('hidden', importKind !== 'technic' || !el('technic-progress').textContent);
|
||||||
}
|
}
|
||||||
@@ -601,6 +613,12 @@ function openImportDialog(kind = 'zip', vorauswahl = null) {
|
|||||||
el('cf-progress').textContent = '';
|
el('cf-progress').textContent = '';
|
||||||
el('cf-progress').classList.add('hidden');
|
el('cf-progress').classList.add('hidden');
|
||||||
el('cf-status').textContent = 'Standardpfad unter Windows: C:\\Users\\DeinName\\curseforge\\minecraft\\Instances';
|
el('cf-status').textContent = 'Standardpfad unter Windows: C:\\Users\\DeinName\\curseforge\\minecraft\\Instances';
|
||||||
|
mrFound = [];
|
||||||
|
el('mr-result').classList.add('hidden');
|
||||||
|
el('mr-do').disabled = true;
|
||||||
|
el('mr-progress').textContent = '';
|
||||||
|
el('mr-progress').classList.add('hidden');
|
||||||
|
el('mr-status').textContent = 'Leer lassen nutzt automatisch den Standardort der Modrinth App.';
|
||||||
el('mmc-result').classList.add('hidden');
|
el('mmc-result').classList.add('hidden');
|
||||||
el('mmc-do').disabled = true;
|
el('mmc-do').disabled = true;
|
||||||
el('mmc-progress').textContent = '';
|
el('mmc-progress').textContent = '';
|
||||||
@@ -613,6 +631,8 @@ function openImportDialog(kind = 'zip', vorauswahl = null) {
|
|||||||
ensureDefaultMcScan();
|
ensureDefaultMcScan();
|
||||||
}
|
}
|
||||||
if (importKind === 'cf') el('cf-path').focus();
|
if (importKind === 'cf') el('cf-path').focus();
|
||||||
|
// Modrinth App: Standardort gleich durchsuchen, wie beim Minecraft Launcher
|
||||||
|
if (importKind === 'mr' && !(Array.isArray(vorauswahl) && vorauswahl.length)) scanMr();
|
||||||
if (importKind === 'mmc') el('mmc-path').focus();
|
if (importKind === 'mmc') el('mmc-path').focus();
|
||||||
if (Array.isArray(vorauswahl) && vorauswahl.length) uebernehmeSetupVorauswahl(vorauswahl);
|
if (Array.isArray(vorauswahl) && vorauswahl.length) uebernehmeSetupVorauswahl(vorauswahl);
|
||||||
}
|
}
|
||||||
@@ -622,7 +642,7 @@ function openImportDialog(kind = 'zip', vorauswahl = null) {
|
|||||||
* eintragen und durchsuchen, damit man beim ersten Start nur noch auswählt.
|
* eintragen und durchsuchen, damit man beim ersten Start nur noch auswählt.
|
||||||
*/
|
*/
|
||||||
function uebernehmeSetupVorauswahl(quellen) {
|
function uebernehmeSetupVorauswahl(quellen) {
|
||||||
const scanner = { mc: scanMc, cf: scanCf, mmc: scanMmc, technic: scanTechnic };
|
const scanner = { mc: scanMc, cf: scanCf, mr: scanMr, mmc: scanMmc, technic: scanTechnic };
|
||||||
importWarteschlange = quellen.map((q) => q && q.kind).filter((k) => scanner[k]);
|
importWarteschlange = quellen.map((q) => q && q.kind).filter((k) => scanner[k]);
|
||||||
for (const q of quellen) {
|
for (const q of quellen) {
|
||||||
if (!q || !scanner[q.kind]) continue;
|
if (!q || !scanner[q.kind]) continue;
|
||||||
@@ -923,6 +943,82 @@ async function runCfImport() {
|
|||||||
if (instances.length) await selectInstance(instances[0].id);
|
if (instances.length) await selectInstance(instances[0].id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function scanMr() {
|
||||||
|
const folder = el('mr-path').value.trim();
|
||||||
|
el('mr-status').textContent = 'Suche Instanzen der Modrinth App …';
|
||||||
|
const res = await window.api.mrScan(folder);
|
||||||
|
if (!res.ok) {
|
||||||
|
el('mr-status').textContent = folder
|
||||||
|
? '✖ Dort wurden keine Daten der Modrinth App gefunden. Gemeint ist der Ordner mit „app.db“ und „profiles“.'
|
||||||
|
: '✖ Die Modrinth App wurde am Standardort nicht gefunden. Ist sie installiert? Sonst den Datenordner wählen.';
|
||||||
|
el('mr-result').classList.add('hidden');
|
||||||
|
el('mr-do').disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mrFound = res.instances;
|
||||||
|
el('mr-status').textContent = `${res.count} Instanz${res.count === 1 ? '' : 'en'} gefunden (${res.basis}).`
|
||||||
|
+ (res.hinweis ? ' ⚠ ' + res.hinweis : '');
|
||||||
|
el('mr-result').classList.remove('hidden');
|
||||||
|
el('mr-all').checked = true;
|
||||||
|
renderMrList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMrList() {
|
||||||
|
const box = el('mr-list');
|
||||||
|
box.innerHTML = '';
|
||||||
|
if (!mrFound.length) { box.innerHTML = '<div class="mods-empty">Keine Instanzen gefunden.</div>'; return; }
|
||||||
|
mrFound.forEach((inst, idx) => {
|
||||||
|
const row = document.createElement('label');
|
||||||
|
row.className = 'mmc-item';
|
||||||
|
const loaderTxt = inst.loader === 'vanilla'
|
||||||
|
? 'Vanilla'
|
||||||
|
: (LOADER_LABEL[inst.loader] || inst.loader) + (inst.loaderVersion ? ' ' + inst.loaderVersion : '');
|
||||||
|
const meta = [inst.version || 'ohne Version', loaderTxt, inst.group ? 'Gruppe: ' + inst.group : '',
|
||||||
|
inst.modpack ? 'Modrinth-Modpack' : '', inst.playSeconds ? fmtDuration(inst.playSeconds) + ' gespielt' : '',
|
||||||
|
inst.installiert === false ? 'nicht fertig installiert' : '']
|
||||||
|
.filter(Boolean).join(' · ');
|
||||||
|
row.innerHTML =
|
||||||
|
`<input type="checkbox" data-idx="${idx}" checked />
|
||||||
|
<div class="mod-info">
|
||||||
|
<div class="mmc-name">${escapeHtml(inst.name)}</div>
|
||||||
|
<div class="mmc-meta">${escapeHtml(meta)}</div>
|
||||||
|
</div>`;
|
||||||
|
box.appendChild(row);
|
||||||
|
});
|
||||||
|
updateMrButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectedMr() {
|
||||||
|
return [...document.querySelectorAll('#mr-list input[type=checkbox]')]
|
||||||
|
.filter((c) => c.checked)
|
||||||
|
.map((c) => mrFound[Number(c.dataset.idx)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMrButton() {
|
||||||
|
const n = selectedMr().length;
|
||||||
|
el('mr-do').disabled = n === 0;
|
||||||
|
el('mr-do').textContent = n ? `${n} übernehmen` : 'Übernehmen';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runMrImport() {
|
||||||
|
const entries = selectedMr();
|
||||||
|
if (!entries.length) return;
|
||||||
|
const copy = el('mr-copy').checked;
|
||||||
|
el('mr-do').disabled = true;
|
||||||
|
el('mr-progress').textContent = 'Übernehme …';
|
||||||
|
el('mr-progress').classList.remove('hidden');
|
||||||
|
|
||||||
|
const res = await window.api.mrImport(entries, copy);
|
||||||
|
await reload();
|
||||||
|
importAbgeschlossen('mr');
|
||||||
|
const n = res.imported.length;
|
||||||
|
const f = res.failed.length;
|
||||||
|
const ohneLink = res.imported.filter((i) => i.modpack === false).length;
|
||||||
|
toast(`${n} Instanz${n === 1 ? '' : 'en'} aus der Modrinth App übernommen${f ? ` · ${f} fehlgeschlagen` : ''}`
|
||||||
|
+ (ohneLink ? ` · ${ohneLink}× Modpack-Verknüpfung nicht möglich (offline?)` : '') + '.', f > 0);
|
||||||
|
if (instances.length) await selectInstance(instances[0].id);
|
||||||
|
}
|
||||||
|
|
||||||
async function scanMmc() {
|
async function scanMmc() {
|
||||||
const folder = el('mmc-path').value.trim();
|
const folder = el('mmc-path').value.trim();
|
||||||
if (!folder) { el('mmc-status').textContent = 'Bitte zuerst einen Ordner angeben.'; return; }
|
if (!folder) { el('mmc-status').textContent = 'Bitte zuerst einen Ordner angeben.'; return; }
|
||||||
@@ -1464,7 +1560,13 @@ async function applySkinBackground(uuidOverride) {
|
|||||||
// Jede Pose hat auch eine Entsprechung beim 3D-Dienst (Kamerawinkel/Ansicht),
|
// Jede Pose hat auch eine Entsprechung beim 3D-Dienst (Kamerawinkel/Ansicht),
|
||||||
// damit die Auswahl immer sichtbar wirkt – auch wenn der Pose-Dienst offline ist.
|
// damit die Auswahl immer sichtbar wirkt – auch wenn der Pose-Dienst offline ist.
|
||||||
// 1) Skin lokal in 3D rendern – unabhängig von Render-Diensten
|
// 1) Skin lokal in 3D rendern – unabhängig von Render-Diensten
|
||||||
const local = await renderSkin3D(`https://crafthead.net/skin/${uuid}`, s.bgSkinPose || 'walking');
|
// Armform: fest gewählt oder (Standard) die des Kontos – sonst sieht ein
|
||||||
|
// schmaler Alex-Skin mit breiten Armen falsch aus (grauer Streifen am Arm)
|
||||||
|
let modell = s.bgSkinModel === 'slim' || s.bgSkinModel === 'classic' ? s.bgSkinModel : '';
|
||||||
|
if (!modell) { try { modell = await window.api.skinModelFor(uuid); } catch { modell = 'classic'; } }
|
||||||
|
let umhang = null;
|
||||||
|
try { const r = await window.api.capeForUuid(uuid); umhang = r ? r.dataUri : null; } catch { /* ohne Umhang */ }
|
||||||
|
const local = await renderSkin3D(`https://crafthead.net/skin/${uuid}`, s.bgSkinPose || 'walking', { modell, umhang });
|
||||||
if (local) { root.style.setProperty('--bg-skin', `url("${local}")`); return; }
|
if (local) { root.style.setProperty('--bg-skin', `url("${local}")`); return; }
|
||||||
|
|
||||||
// 2) Rückfall: externe Render-Dienste
|
// 2) Rückfall: externe Render-Dienste
|
||||||
@@ -1611,6 +1713,7 @@ async function renderSkin3D(quelle, poseKey, opts = {}) {
|
|||||||
viewer.autoRotate = false;
|
viewer.autoRotate = false;
|
||||||
viewer.animation = null;
|
viewer.animation = null;
|
||||||
pose.apply(viewer.playerObject);
|
pose.apply(viewer.playerObject);
|
||||||
|
if (opts.umhang) { try { await viewer.loadCape(opts.umhang); } catch { /* ohne Umhang */ } }
|
||||||
|
|
||||||
viewer.render();
|
viewer.render();
|
||||||
const data = canvas.toDataURL('image/png');
|
const data = canvas.toDataURL('image/png');
|
||||||
@@ -1630,7 +1733,7 @@ const ICONS_SVG = {
|
|||||||
terminal: '<path d="M4 5h16v14H4zM7 9l3 3-3 3M13 16h4"/>',
|
terminal: '<path d="M4 5h16v14H4zM7 9l3 3-3 3M13 16h4"/>',
|
||||||
user: '<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2M12 11a4 4 0 100-8 4 4 0 000 8z"/>',
|
user: '<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2M12 11a4 4 0 100-8 4 4 0 000 8z"/>',
|
||||||
users: '<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2M9 11a4 4 0 100-8 4 4 0 000 8zM23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75"/>',
|
users: '<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2M9 11a4 4 0 100-8 4 4 0 000 8zM23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75"/>',
|
||||||
settings: '<circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3M5 5l2 2M17 17l2 2M19 5l-2 2M7 17l-2 2"/>',
|
settings: '<path d="M19.1 13.1 21.5 14.5 20.5 16.9 17.8 16.3 16.3 17.8 16.9 20.5 14.5 21.5 13.1 19.1 10.9 19.1 9.5 21.5 7.1 20.5 7.7 17.8 6.2 16.3 3.5 16.9 2.5 14.5 4.9 13.1 4.9 10.9 2.5 9.5 3.5 7.1 6.2 7.7 7.7 6.2 7.1 3.5 9.5 2.5 10.9 4.9 13.1 4.9 14.5 2.5 16.9 3.5 16.3 6.2 17.8 7.7 20.5 7.1 21.5 9.5 19.1 10.9Z"/><circle cx="12" cy="12" r="3"/>', // Zahnrad (8 Zähne)
|
||||||
play: '<path d="M7 5l12 7-12 7z"/>',
|
play: '<path d="M7 5l12 7-12 7z"/>',
|
||||||
blocks: '<path d="M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z"/>',
|
blocks: '<path d="M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z"/>',
|
||||||
globe: '<circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a15 15 0 010 18M12 3a15 15 0 000 18"/>',
|
globe: '<circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a15 15 0 010 18M12 3a15 15 0 000 18"/>',
|
||||||
@@ -1640,6 +1743,7 @@ const ICONS_SVG = {
|
|||||||
export: '<path d="M12 21V9m0 0l-4 4m4-4l4 4M4 4h16"/>',
|
export: '<path d="M12 21V9m0 0l-4 4m4-4l4 4M4 4h16"/>',
|
||||||
trash: '<path d="M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13"/>',
|
trash: '<path d="M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13"/>',
|
||||||
chart: '<path d="M4 20V10M10 20V4M16 20v-7M22 20H2"/>',
|
chart: '<path d="M4 20V10M10 20V4M16 20v-7M22 20H2"/>',
|
||||||
|
search: '<circle cx="11" cy="11" r="7"/><path d="M20 20l-4.2-4.2"/>',
|
||||||
};
|
};
|
||||||
function svgIcon(name) {
|
function svgIcon(name) {
|
||||||
const p = ICONS_SVG[name];
|
const p = ICONS_SVG[name];
|
||||||
@@ -1809,6 +1913,17 @@ function buildCard(inst) {
|
|||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Suchfeld in der Werkzeugleiste: standardmäßig nur eine Lupe
|
||||||
|
function sucheOeffnen() {
|
||||||
|
el('search-wrap').classList.add('offen');
|
||||||
|
el('search').focus();
|
||||||
|
el('search').select();
|
||||||
|
}
|
||||||
|
function sucheSchliessen() {
|
||||||
|
el('search-wrap').classList.remove('offen');
|
||||||
|
if (document.activeElement === el('search')) el('search').blur();
|
||||||
|
}
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
const q = el('search').value.trim().toLowerCase();
|
const q = el('search').value.trim().toLowerCase();
|
||||||
const grid = el('grid');
|
const grid = el('grid');
|
||||||
@@ -2507,7 +2622,7 @@ function schliesseSkinViewer(bildId) {
|
|||||||
* Mausrad zoomt -- kommt von skinview3d selbst). Klappt das Rendern nicht,
|
* Mausrad zoomt -- kommt von skinview3d selbst). Klappt das Rendern nicht,
|
||||||
* wird stattdessen ein Hinweistext gezeigt.
|
* wird stattdessen ein Hinweistext gezeigt.
|
||||||
*/
|
*/
|
||||||
async function zeigeSkinFigur(bildId, leerId, textur, leerText) {
|
async function zeigeSkinFigur(bildId, leerId, textur, leerText, umhang) {
|
||||||
schliesseSkinViewer(bildId);
|
schliesseSkinViewer(bildId);
|
||||||
const canvas = el(bildId);
|
const canvas = el(bildId);
|
||||||
const leer = el(leerId);
|
const leer = el(leerId);
|
||||||
@@ -2544,6 +2659,7 @@ async function zeigeSkinFigur(bildId, leerId, textur, leerText) {
|
|||||||
viewer.autoRotate = false;
|
viewer.autoRotate = false;
|
||||||
viewer.animation = null;
|
viewer.animation = null;
|
||||||
pose.apply(viewer.playerObject);
|
pose.apply(viewer.playerObject);
|
||||||
|
if (umhang) { try { await viewer.loadCape(umhang); } catch { /* ohne Umhang weiter */ } }
|
||||||
viewer.controls.enableRotate = true;
|
viewer.controls.enableRotate = true;
|
||||||
viewer.controls.enableZoom = true;
|
viewer.controls.enableZoom = true;
|
||||||
viewer.controls.enablePan = false;
|
viewer.controls.enablePan = false;
|
||||||
@@ -2579,14 +2695,78 @@ async function openSkin() {
|
|||||||
// Armform zuerst setzen – davon hängt ab, wie die Figur gerendert wird
|
// Armform zuerst setzen – davon hängt ab, wie die Figur gerendert wird
|
||||||
if (p.variante) el('skin-variante').value = p.variante === 'slim' ? 'slim' : 'classic';
|
if (p.variante) el('skin-variante').value = p.variante === 'slim' ? 'slim' : 'classic';
|
||||||
skinTexturAktuell = p.skinUrl || '';
|
skinTexturAktuell = p.skinUrl || '';
|
||||||
|
await ladeUmhangInfo();
|
||||||
await zeigeSkinFigur('skin-aktuell', 'skin-aktuell-leer', skinTexturAktuell,
|
await zeigeSkinFigur('skin-aktuell', 'skin-aktuell-leer', skinTexturAktuell,
|
||||||
'Standard-Skin (keine eigene Datei hinterlegt)');
|
'Standard-Skin (keine eigene Datei hinterlegt)', skinUmhang);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Umhänge im Skin-Fenster ----------
|
||||||
|
let skinUmhang = null; // data:-Adresse des angezeigten Umhangs (AeroMC vor offiziell)
|
||||||
|
|
||||||
|
async function ladeUmhangInfo() {
|
||||||
|
const sel = el('skin-cape-offiziell');
|
||||||
|
const info = await window.api.capeInfo();
|
||||||
|
sel.innerHTML = '<option value="">Kein Umhang</option>';
|
||||||
|
if (!info.ok) {
|
||||||
|
el('skin-cape-aero').textContent = info.message || '–';
|
||||||
|
el('skin-cape-remove').classList.add('hidden');
|
||||||
|
skinUmhang = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const c of info.offiziell) {
|
||||||
|
const o = document.createElement('option');
|
||||||
|
o.value = c.id;
|
||||||
|
o.textContent = c.name;
|
||||||
|
if (c.aktiv) o.selected = true;
|
||||||
|
sel.appendChild(o);
|
||||||
|
}
|
||||||
|
sel.disabled = !info.offiziell.length;
|
||||||
|
sel.title = info.offiziell.length ? 'Nur Umhänge, die dein Konto bei Mojang besitzt' : 'Dein Konto besitzt keinen offiziellen Umhang';
|
||||||
|
if (info.aeromc) {
|
||||||
|
el('skin-cape-aero').textContent = `Hochgeladen (${info.aeromc.width}×${info.aeromc.height})`;
|
||||||
|
el('skin-cape-remove').classList.remove('hidden');
|
||||||
|
el('skin-cape-edit').textContent = 'Bearbeiten…';
|
||||||
|
} else {
|
||||||
|
el('skin-cape-aero').textContent = info.aeromcFehlt ? 'Server noch nicht bereit' : info.aeromcFehler ? 'Umhang-Server nicht erreichbar' : 'Noch keiner';
|
||||||
|
el('skin-cape-aero').title = info.aeromcFehler || '';
|
||||||
|
el('skin-cape-remove').classList.add('hidden');
|
||||||
|
el('skin-cape-edit').textContent = 'Gestalten…';
|
||||||
|
}
|
||||||
|
const konto = await window.api.authStatus();
|
||||||
|
const r = konto && konto.profile ? await window.api.capeForUuid(konto.profile.id) : null;
|
||||||
|
skinUmhang = r ? r.dataUri : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waehleOffiziellenUmhang() {
|
||||||
|
const sel = el('skin-cape-offiziell');
|
||||||
|
sel.disabled = true;
|
||||||
|
el('skin-cape-status').textContent = 'Umhang wird gesetzt …';
|
||||||
|
const res = await window.api.capeSetOfficial(sel.value || null);
|
||||||
|
sel.disabled = false;
|
||||||
|
if (res.ok) {
|
||||||
|
el('skin-cape-status').textContent = sel.value ? '✔ Offizieller Umhang gesetzt.' : '✔ Kein offizieller Umhang mehr.';
|
||||||
|
await ladeUmhangInfo();
|
||||||
|
await zeigeSkinFigur('skin-aktuell', 'skin-aktuell-leer', skinTexturAktuell, 'Standard-Skin (keine eigene Datei hinterlegt)', skinUmhang);
|
||||||
|
} else {
|
||||||
|
el('skin-cape-status').textContent = '✖ ' + (res.message || 'Fehlgeschlagen.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function entferneAeroUmhang() {
|
||||||
|
askConfirm('AeroMC-Umhang entfernen?', 'Dein eigener Umhang wird vom AeroMC-Umhang-Server gelöscht. Hast du ihn nicht als PNG gespeichert, ist er danach weg.', 'Entfernen', async () => {
|
||||||
|
const res = await window.api.capeRemove();
|
||||||
|
el('skin-cape-status').textContent = res.ok ? '✔ AeroMC-Umhang entfernt.' : '✖ ' + (res.message || 'Fehlgeschlagen.');
|
||||||
|
if (res.ok) {
|
||||||
|
await ladeUmhangInfo();
|
||||||
|
await zeigeSkinFigur('skin-aktuell', 'skin-aktuell-leer', skinTexturAktuell, 'Standard-Skin (keine eigene Datei hinterlegt)', skinUmhang);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Armform gewechselt -> beide Figuren neu zeichnen
|
// Armform gewechselt -> beide Figuren neu zeichnen
|
||||||
async function skinVarianteGewechselt() {
|
async function skinVarianteGewechselt() {
|
||||||
if (skinTexturAktuell) await zeigeSkinFigur('skin-aktuell', 'skin-aktuell-leer', skinTexturAktuell, '');
|
if (skinTexturAktuell) await zeigeSkinFigur('skin-aktuell', 'skin-aktuell-leer', skinTexturAktuell, '', skinUmhang);
|
||||||
if (skinTexturNeu) await zeigeSkinFigur('skin-neu', 'skin-neu-leer', skinTexturNeu, '');
|
if (skinTexturNeu) await zeigeSkinFigur('skin-neu', 'skin-neu-leer', skinTexturNeu, '', skinUmhang);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waehleSkinDatei() {
|
async function waehleSkinDatei() {
|
||||||
@@ -2597,7 +2777,7 @@ async function waehleSkinDatei() {
|
|||||||
gewaehlterSkin = res.pfad;
|
gewaehlterSkin = res.pfad;
|
||||||
el('skin-pfad').value = res.pfad;
|
el('skin-pfad').value = res.pfad;
|
||||||
skinTexturNeu = res.dataUri || '';
|
skinTexturNeu = res.dataUri || '';
|
||||||
await zeigeSkinFigur('skin-neu', 'skin-neu-leer', skinTexturNeu, 'Vorschau nicht verfügbar');
|
await zeigeSkinFigur('skin-neu', 'skin-neu-leer', skinTexturNeu, 'Vorschau nicht verfügbar', skinUmhang);
|
||||||
el('skin-status').textContent = `${res.breite}×${res.hoehe} Pixel – sieht gut aus.`;
|
el('skin-status').textContent = `${res.breite}×${res.hoehe} Pixel – sieht gut aus.`;
|
||||||
el('skin-apply').disabled = false;
|
el('skin-apply').disabled = false;
|
||||||
}
|
}
|
||||||
@@ -4790,7 +4970,8 @@ async function openSettings(pane) {
|
|||||||
window.api.updateLastCheck().then(renderUpdateStatus).catch(() => {});
|
window.api.updateLastCheck().then(renderUpdateStatus).catch(() => {});
|
||||||
|
|
||||||
const s = await window.api.getSettings();
|
const s = await window.api.getSettings();
|
||||||
el('s-theme').value = s.theme || 'dark';
|
el('s-theme').value = DESIGNS.includes(s.theme) ? s.theme : 'dark';
|
||||||
|
applyTheme(el('s-theme').value);
|
||||||
el('s-global-pre').value = s.globalPreLaunch || '';
|
el('s-global-pre').value = s.globalPreLaunch || '';
|
||||||
el('s-global-post').value = s.globalPostExit || '';
|
el('s-global-post').value = s.globalPostExit || '';
|
||||||
el('s-wrapper').value = s.wrapperCommand || '';
|
el('s-wrapper').value = s.wrapperCommand || '';
|
||||||
@@ -4806,6 +4987,7 @@ async function openSettings(pane) {
|
|||||||
}
|
}
|
||||||
sel.value = s.bgSkin || '';
|
sel.value = s.bgSkin || '';
|
||||||
el('s-bgskin-pose').value = s.bgSkinPose || 'walking';
|
el('s-bgskin-pose').value = s.bgSkinPose || 'walking';
|
||||||
|
el('s-bgskin-model').value = ['classic', 'slim'].includes(s.bgSkinModel) ? s.bgSkinModel : 'auto';
|
||||||
el('s-bgskin-cycle').checked = !!s.bgSkinCycle;
|
el('s-bgskin-cycle').checked = !!s.bgSkinCycle;
|
||||||
el('s-bgskin-cycle-seconds').value = s.bgSkinCycleSeconds || 30;
|
el('s-bgskin-cycle-seconds').value = s.bgSkinCycleSeconds || 30;
|
||||||
sel.disabled = !!s.bgSkinCycle;
|
sel.disabled = !!s.bgSkinCycle;
|
||||||
@@ -4816,6 +4998,7 @@ async function openSettings(pane) {
|
|||||||
el('s-friends').checked = s.friendsEnabled !== false;
|
el('s-friends').checked = s.friendsEnabled !== false;
|
||||||
el('s-friends-server').checked = s.friendsShareServer !== false;
|
el('s-friends-server').checked = s.friendsShareServer !== false;
|
||||||
el('s-auto-mod-updates').checked = !!s.autoModUpdates;
|
el('s-auto-mod-updates').checked = !!s.autoModUpdates;
|
||||||
|
el('s-aeromc-capes').checked = s.aeromcCapes !== false;
|
||||||
el('s-autostart').checked = !!s.autostart;
|
el('s-autostart').checked = !!s.autostart;
|
||||||
el('s-auto-java').checked = s.autoJava !== false;
|
el('s-auto-java').checked = s.autoJava !== false;
|
||||||
el('s-autobackup').checked = !!s.autoBackupEnabled;
|
el('s-autobackup').checked = !!s.autoBackupEnabled;
|
||||||
@@ -4939,6 +5122,7 @@ async function saveSettings() {
|
|||||||
globalPostExit: el('s-global-post').value.trim(),
|
globalPostExit: el('s-global-post').value.trim(),
|
||||||
bgSkin: el('s-bgskin').value,
|
bgSkin: el('s-bgskin').value,
|
||||||
bgSkinPose: el('s-bgskin-pose').value,
|
bgSkinPose: el('s-bgskin-pose').value,
|
||||||
|
bgSkinModel: el('s-bgskin-model').value,
|
||||||
bgSkinCycle: el('s-bgskin-cycle').checked,
|
bgSkinCycle: el('s-bgskin-cycle').checked,
|
||||||
bgSkinCycleSeconds: Math.max(5, numOrNull(el('s-bgskin-cycle-seconds').value) || 30),
|
bgSkinCycleSeconds: Math.max(5, numOrNull(el('s-bgskin-cycle-seconds').value) || 30),
|
||||||
sortBy: el('s-sortby').value,
|
sortBy: el('s-sortby').value,
|
||||||
@@ -4948,6 +5132,7 @@ async function saveSettings() {
|
|||||||
friendsEnabled: el('s-friends').checked,
|
friendsEnabled: el('s-friends').checked,
|
||||||
friendsShareServer: el('s-friends-server').checked,
|
friendsShareServer: el('s-friends-server').checked,
|
||||||
autoModUpdates: el('s-auto-mod-updates').checked,
|
autoModUpdates: el('s-auto-mod-updates').checked,
|
||||||
|
aeromcCapes: el('s-aeromc-capes').checked,
|
||||||
autostart: el('s-autostart').checked,
|
autostart: el('s-autostart').checked,
|
||||||
autoJava: el('s-auto-java').checked,
|
autoJava: el('s-auto-java').checked,
|
||||||
autoBackupEnabled: el('s-autobackup').checked,
|
autoBackupEnabled: el('s-autobackup').checked,
|
||||||
@@ -5424,6 +5609,18 @@ function wire() {
|
|||||||
// ohne Wrapper käme das Klick-Ereignis als Bereichsname an
|
// ohne Wrapper käme das Klick-Ereignis als Bereichsname an
|
||||||
el('btn-settings').addEventListener('click', () => openSettings());
|
el('btn-settings').addEventListener('click', () => openSettings());
|
||||||
el('search').addEventListener('input', render);
|
el('search').addEventListener('input', render);
|
||||||
|
el('btn-search').addEventListener('click', () => sucheOeffnen());
|
||||||
|
el('search').addEventListener('keydown', (e) => {
|
||||||
|
if (e.key !== 'Escape') return;
|
||||||
|
// Esc gehört hier der Suche, nicht dem globalen Dialog-Schließen
|
||||||
|
e.stopPropagation();
|
||||||
|
el('search').value = '';
|
||||||
|
render();
|
||||||
|
sucheSchliessen();
|
||||||
|
});
|
||||||
|
// leer verlassen = wieder zur Lupe; mit Suchbegriff bleibt das Feld offen,
|
||||||
|
// damit man sieht, dass die Liste gerade gefiltert ist
|
||||||
|
el('search').addEventListener('blur', () => { if (!el('search').value.trim()) sucheSchliessen(); });
|
||||||
|
|
||||||
el('btn-open-dir').addEventListener('click', () => {
|
el('btn-open-dir').addEventListener('click', () => {
|
||||||
if (currentDetailId) window.api.openInstanceFolder(currentDetailId);
|
if (currentDetailId) window.api.openInstanceFolder(currentDetailId);
|
||||||
@@ -5471,6 +5668,9 @@ function wire() {
|
|||||||
});
|
});
|
||||||
document.addEventListener('scroll', closeContextMenu, true);
|
document.addEventListener('scroll', closeContextMenu, true);
|
||||||
window.addEventListener('blur', closeContextMenu);
|
window.addEventListener('blur', closeContextMenu);
|
||||||
|
// Titelleiste wie bei Windows abblenden, solange das Fenster nicht aktiv ist
|
||||||
|
window.addEventListener('blur', () => document.body.classList.add('fenster-inaktiv'));
|
||||||
|
window.addEventListener('focus', () => document.body.classList.remove('fenster-inaktiv'));
|
||||||
|
|
||||||
// Warnung anklicken -> sofort erneut prüfen
|
// Warnung anklicken -> sofort erneut prüfen
|
||||||
el('status-auth').addEventListener('click', async () => {
|
el('status-auth').addEventListener('click', async () => {
|
||||||
@@ -5530,6 +5730,18 @@ function wire() {
|
|||||||
});
|
});
|
||||||
el('cf-list').addEventListener('change', updateCfButton);
|
el('cf-list').addEventListener('change', updateCfButton);
|
||||||
el('cf-do').addEventListener('click', runCfImport);
|
el('cf-do').addEventListener('click', runCfImport);
|
||||||
|
el('mr-pick').addEventListener('click', async () => {
|
||||||
|
const p = await window.api.mrPickFolder();
|
||||||
|
if (p) { el('mr-path').value = p; await scanMr(); }
|
||||||
|
});
|
||||||
|
el('mr-scan').addEventListener('click', scanMr);
|
||||||
|
el('mr-path').addEventListener('keydown', (e) => { if (e.key === 'Enter') scanMr(); });
|
||||||
|
el('mr-all').addEventListener('change', (e) => {
|
||||||
|
document.querySelectorAll('#mr-list input[type=checkbox]').forEach((c) => { c.checked = e.target.checked; });
|
||||||
|
updateMrButton();
|
||||||
|
});
|
||||||
|
el('mr-list').addEventListener('change', updateMrButton);
|
||||||
|
el('mr-do').addEventListener('click', runMrImport);
|
||||||
el('mmc-pick').addEventListener('click', async () => {
|
el('mmc-pick').addEventListener('click', async () => {
|
||||||
const p = await window.api.mmcPickFolder();
|
const p = await window.api.mmcPickFolder();
|
||||||
if (p) { el('mmc-path').value = p; await scanMmc(); }
|
if (p) { el('mmc-path').value = p; await scanMmc(); }
|
||||||
@@ -5567,6 +5779,10 @@ function wire() {
|
|||||||
el('cf-progress').textContent = `Übernehme … ${p.current}/${p.total}`;
|
el('cf-progress').textContent = `Übernehme … ${p.current}/${p.total}`;
|
||||||
el('cf-progress').classList.remove('hidden');
|
el('cf-progress').classList.remove('hidden');
|
||||||
});
|
});
|
||||||
|
window.api.onMrProgress((p) => {
|
||||||
|
el('mr-progress').textContent = p.detail ? `${p.detail} …` : `Übernehme … ${p.current}/${p.total}`;
|
||||||
|
el('mr-progress').classList.remove('hidden');
|
||||||
|
});
|
||||||
window.api.onMmcProgress((p) => {
|
window.api.onMmcProgress((p) => {
|
||||||
el('mmc-progress').textContent = `Übernehme … ${p.current}/${p.total}`;
|
el('mmc-progress').textContent = `Übernehme … ${p.current}/${p.total}`;
|
||||||
el('mmc-progress').classList.remove('hidden');
|
el('mmc-progress').classList.remove('hidden');
|
||||||
@@ -5803,7 +6019,14 @@ function wire() {
|
|||||||
el('login-skin').addEventListener('click', openSkin);
|
el('login-skin').addEventListener('click', openSkin);
|
||||||
el('skin-pick').addEventListener('click', waehleSkinDatei);
|
el('skin-pick').addEventListener('click', waehleSkinDatei);
|
||||||
el('skin-variante').addEventListener('change', skinVarianteGewechselt);
|
el('skin-variante').addEventListener('change', skinVarianteGewechselt);
|
||||||
|
el('skin-cape-offiziell').addEventListener('change', waehleOffiziellenUmhang);
|
||||||
|
el('skin-cape-edit').addEventListener('click', () => window.openSkinEditor({ modus: 'umhang', quelle: 'aktuell' }));
|
||||||
|
el('skin-cape-remove').addEventListener('click', entferneAeroUmhang);
|
||||||
el('skin-apply').addEventListener('click', setzeSkin);
|
el('skin-apply').addEventListener('click', setzeSkin);
|
||||||
|
// gewählte Datei (sonst der aktuelle Skin des Kontos) im Editor öffnen
|
||||||
|
el('skin-editor').addEventListener('click', () => window.openSkinEditor(gewaehlterSkin
|
||||||
|
? { pfad: gewaehlterSkin, variante: el('skin-variante').value }
|
||||||
|
: { quelle: 'aktuell' }));
|
||||||
el('skin-reset').addEventListener('click', setzeSkinZurueck);
|
el('skin-reset').addEventListener('click', setzeSkinZurueck);
|
||||||
el('logs-select').addEventListener('change', (e) => ladeLog(e.target.value));
|
el('logs-select').addEventListener('change', (e) => ladeLog(e.target.value));
|
||||||
el('logs-folder').addEventListener('click', async () => {
|
el('logs-folder').addEventListener('click', async () => {
|
||||||
@@ -5986,6 +6209,11 @@ function wire() {
|
|||||||
if (p) { el('s-java').value = p; await refreshJavaStatus(p); }
|
if (p) { el('s-java').value = p; await refreshJavaStatus(p); }
|
||||||
});
|
});
|
||||||
el('s-theme').addEventListener('change', (e) => applyTheme(e.target.value));
|
el('s-theme').addEventListener('change', (e) => applyTheme(e.target.value));
|
||||||
|
// Design-Kacheln setzen die (versteckte) Auswahl und zeigen es sofort an
|
||||||
|
document.querySelectorAll('.design-wahl').forEach((b) => b.addEventListener('click', () => {
|
||||||
|
el('s-theme').value = b.dataset.design;
|
||||||
|
applyTheme(b.dataset.design);
|
||||||
|
}));
|
||||||
el('s-bgskin-opacity').addEventListener('input', (e) => {
|
el('s-bgskin-opacity').addEventListener('input', (e) => {
|
||||||
el('s-bgskin-val').textContent = e.target.value;
|
el('s-bgskin-val').textContent = e.target.value;
|
||||||
document.documentElement.style.setProperty('--bg-skin-opacity', String(e.target.value / 100));
|
document.documentElement.style.setProperty('--bg-skin-opacity', String(e.target.value / 100));
|
||||||
@@ -5995,11 +6223,13 @@ function wire() {
|
|||||||
await window.api.saveSettings({
|
await window.api.saveSettings({
|
||||||
bgSkin: el('s-bgskin').value,
|
bgSkin: el('s-bgskin').value,
|
||||||
bgSkinPose: el('s-bgskin-pose').value,
|
bgSkinPose: el('s-bgskin-pose').value,
|
||||||
|
bgSkinModel: el('s-bgskin-model').value,
|
||||||
});
|
});
|
||||||
await refreshSkinBackground();
|
await refreshSkinBackground();
|
||||||
};
|
};
|
||||||
el('s-bgskin').addEventListener('change', previewSkin);
|
el('s-bgskin').addEventListener('change', previewSkin);
|
||||||
el('s-bgskin-pose').addEventListener('change', previewSkin);
|
el('s-bgskin-pose').addEventListener('change', previewSkin);
|
||||||
|
el('s-bgskin-model').addEventListener('change', previewSkin);
|
||||||
const previewSkinCycle = async () => {
|
const previewSkinCycle = async () => {
|
||||||
el('s-bgskin').disabled = el('s-bgskin-cycle').checked;
|
el('s-bgskin').disabled = el('s-bgskin-cycle').checked;
|
||||||
await window.api.saveSettings({
|
await window.api.saveSettings({
|
||||||
@@ -6141,8 +6371,7 @@ function wire() {
|
|||||||
// der eine eigene Suche hat (Mods, Modpack-Katalog, Import …)
|
// der eine eigene Suche hat (Mods, Modpack-Katalog, Import …)
|
||||||
if (strg && e.key.toLowerCase() === 'f' && !modalOffen) {
|
if (strg && e.key.toLowerCase() === 'f' && !modalOffen) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
el('search').focus();
|
sucheOeffnen();
|
||||||
el('search').select();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Strg+1 … Strg+5: Schnellstart-Instanz starten (nur im Hauptfenster)
|
// Strg+1 … Strg+5: Schnellstart-Instanz starten (nur im Hauptfenster)
|
||||||
@@ -6240,6 +6469,7 @@ const POPOUT_OPENERS = {
|
|||||||
screens: () => openScreens(),
|
screens: () => openScreens(),
|
||||||
servers: () => openServers(),
|
servers: () => openServers(),
|
||||||
skin: () => openSkin(),
|
skin: () => openSkin(),
|
||||||
|
skineditor: (p) => window.openSkinEditor(p),
|
||||||
bedrock: () => openBedrock(),
|
bedrock: () => openBedrock(),
|
||||||
login: (p) => openLogin({ addAccount: p.addAccount === true || p.addAccount === 'true' }),
|
login: (p) => openLogin({ addAccount: p.addAccount === true || p.addAccount === 'true' }),
|
||||||
import: (p) => {
|
import: (p) => {
|
||||||
@@ -6398,7 +6628,11 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|||||||
// Ein Skin-Wechsel oder eine neue/aktualisierte Instanz aus einem anderen
|
// Ein Skin-Wechsel oder eine neue/aktualisierte Instanz aus einem anderen
|
||||||
// Fenster (Skin-/Import-Popup) betrifft ebenfalls nur die Anzeige im
|
// Fenster (Skin-/Import-Popup) betrifft ebenfalls nur die Anzeige im
|
||||||
// Hauptfenster (Skin-Hintergrund, Seitenleiste) – Popups zeigen beides nicht an
|
// Hauptfenster (Skin-Hintergrund, Seitenleiste) – Popups zeigen beides nicht an
|
||||||
window.api.onSkinChanged(() => { if (!POPOUT) refreshSkinBackground(); });
|
window.api.onSkinChanged((info) => {
|
||||||
|
if (!POPOUT) refreshSkinBackground();
|
||||||
|
// im Editor gesetzt -> offenes Skin-Fenster zeigt sonst noch den alten Stand
|
||||||
|
if (POPOUT === 'skin' && (info.quelle === 'editor' || info.quelle === 'umhang') && !el('modal-skin').classList.contains('hidden')) openSkin();
|
||||||
|
});
|
||||||
window.api.onInstancesChanged(() => { if (!POPOUT) reload(); });
|
window.api.onInstancesChanged(() => { if (!POPOUT) reload(); });
|
||||||
try { showAuthHealth(await window.api.authHealth()); } catch { /* noch keine Prüfung */ }
|
try { showAuthHealth(await window.api.authHealth()); } catch { /* noch keine Prüfung */ }
|
||||||
await loadMcIcons(); // echte Minecraft-Icons bereitstellen
|
await loadMcIcons(); // echte Minecraft-Icons bereitstellen
|
||||||
|
|||||||
@@ -115,7 +115,30 @@ async function holeProfil(accessToken) {
|
|||||||
id: daten.id,
|
id: daten.id,
|
||||||
skinUrl: aktiv ? aufHttps(aktiv.url) : '',
|
skinUrl: aktiv ? aufHttps(aktiv.url) : '',
|
||||||
variante: aktiv ? String(aktiv.variant || '').toLowerCase() : '',
|
variante: aktiv ? String(aktiv.variant || '').toLowerCase() : '',
|
||||||
|
// offizielle Umhänge, die dieses Konto besitzt (Migrator, MINECON …)
|
||||||
|
capes: (daten.capes || []).map((c) => ({
|
||||||
|
id: c.id,
|
||||||
|
name: c.alias || 'Umhang',
|
||||||
|
url: aufHttps(c.url),
|
||||||
|
aktiv: c.state === 'ACTIVE',
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { setzeSkin, setzeSkinZurueck, holeProfil, pruefeDatei, aufHttps };
|
/*
|
||||||
|
* Offiziellen Umhang wählen (nur einer, den das Konto besitzt) bzw. mit
|
||||||
|
* capeId = null keinen tragen. Gilt im Spiel für alle Spieler.
|
||||||
|
*/
|
||||||
|
async function setzeUmhang(accessToken, capeId) {
|
||||||
|
const res = await fetch(API + '/minecraft/profile/capes/active', capeId
|
||||||
|
? {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { Authorization: 'Bearer ' + accessToken, 'User-Agent': UA, 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ capeId }),
|
||||||
|
}
|
||||||
|
: { method: 'DELETE', headers: { Authorization: 'Bearer ' + accessToken, 'User-Agent': UA } });
|
||||||
|
if (!res.ok) return { ok: false, message: fehlerText(res.status) };
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { setzeSkin, setzeSkinZurueck, holeProfil, pruefeDatei, aufHttps, setzeUmhang };
|
||||||
|
|||||||
@@ -78,7 +78,8 @@ const DEFAULT_SETTINGS = {
|
|||||||
desktopNotifications: true, // Windows-Meldung bei fertigem Hintergrund-Download/-Backup
|
desktopNotifications: true, // Windows-Meldung bei fertigem Hintergrund-Download/-Backup
|
||||||
friendsEnabled: true,
|
friendsEnabled: true,
|
||||||
friendsShareServer: true, // Freunde sehen, auf welchem Server man spielt (für "Beitreten")
|
friendsShareServer: true, // Freunde sehen, auf welchem Server man spielt (für "Beitreten")
|
||||||
autoModUpdates: false, // vor jedem Start Mods über Modrinth aktualisieren
|
autoModUpdates: false,
|
||||||
|
aeromcCapes: true, // AeroMC-Umhänge im Spiel anzeigen (Umhang-Agent beim Start) // vor jedem Start Mods über Modrinth aktualisieren
|
||||||
schnellstart: {}, // Strg+1 … Strg+5 -> Instanz-ID ({ "1": "Survival", … })
|
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
|
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
|
friendNicknames: {}, // rein lokal: uuid -> eigener Spitzname für einen Freund
|
||||||
@@ -87,6 +88,7 @@ const DEFAULT_SETTINGS = {
|
|||||||
bgSkin: '', // '' = aus, 'active' = aktives Konto, sonst Konto-UUID
|
bgSkin: '', // '' = aus, 'active' = aktives Konto, sonst Konto-UUID
|
||||||
bgSkinOpacity: 55, // Deckkraft des Hintergrund-Skins in %
|
bgSkinOpacity: 55, // Deckkraft des Hintergrund-Skins in %
|
||||||
bgSkinPose: 'walking', // Pose des 3D-Renders (falls der Pose-Dienst erreichbar ist)
|
bgSkinPose: 'walking', // Pose des 3D-Renders (falls der Pose-Dienst erreichbar ist)
|
||||||
|
bgSkinModel: 'auto', // Armform der Figur: 'auto' (vom Konto), 'classic' (Steve), 'slim' (Alex)
|
||||||
bgSkinCycle: false, // bei 2+ angemeldeten Konten automatisch durchwechseln
|
bgSkinCycle: false, // bei 2+ angemeldeten Konten automatisch durchwechseln
|
||||||
bgSkinCycleSeconds: 30, // Sekunden zwischen zwei Kontowechseln
|
bgSkinCycleSeconds: 30, // Sekunden zwischen zwei Kontowechseln
|
||||||
wrapperCommand: '', // Externe Programme: Startprogramm, das Java umschließt
|
wrapperCommand: '', // Externe Programme: Startprogramm, das Java umschließt
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
}
|
}
|
||||||
*{box-sizing:border-box}
|
*{box-sizing:border-box}
|
||||||
html,body{margin:0;height:100%;font-family:var(--font);font-size:12px;color:var(--text);background:var(--win)}
|
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}
|
.hidden{display:none!important}
|
||||||
.mono{font-family:var(--mono);font-size:12px}
|
.mono{font-family:var(--mono);font-size:12px}
|
||||||
.spacer{flex:1}
|
.spacer{flex:1}
|
||||||
@@ -65,7 +65,7 @@ body{display:grid;grid-template-rows:auto 1fr auto;height:100vh;overflow:hidden}
|
|||||||
:root[data-theme="dark"] .empty h2,
|
:root[data-theme="dark"] .empty h2,
|
||||||
:root[data-theme="dark"] .mods-col h4,
|
:root[data-theme="dark"] .mods-col h4,
|
||||||
:root[data-theme="dark"] .field>span{color:var(--text)}
|
:root[data-theme="dark"] .field>span{color:var(--text)}
|
||||||
:root[data-theme="dark"] #btn-console svg{color:#c8c8cc}
|
:root[data-theme="dark"] #btn-console svg,:root[data-theme="dark"] #btn-search svg{color:#c8c8cc}
|
||||||
:root[data-theme="dark"] .acct-av svg{color:#c8c8cc}
|
:root[data-theme="dark"] .acct-av svg{color:#c8c8cc}
|
||||||
:root[data-theme="dark"] .mods-filters select,
|
:root[data-theme="dark"] .mods-filters select,
|
||||||
:root[data-theme="dark"] .mods-search select,
|
:root[data-theme="dark"] .mods-search select,
|
||||||
@@ -75,9 +75,19 @@ body{display:grid;grid-template-rows:auto 1fr auto;height:100vh;overflow:hidden}
|
|||||||
:root[data-theme="dark"] .modal-close:hover{color:#fff}
|
:root[data-theme="dark"] .modal-close:hover{color:#fff}
|
||||||
:root[data-theme="dark"] ::-webkit-scrollbar-thumb{background:#4a4a52;border-color:var(--area)}
|
: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 ---------- */
|
||||||
.toolbar{display:flex;align-items:center;gap:2px;padding:5px 8px;background:var(--win);border-bottom:1px solid var(--border)}
|
.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;padding:6px 10px;border:1px solid transparent;border-radius:3px;background:none;color:var(--text);cursor:pointer;font-family:inherit;font-size:12px}
|
.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:hover{background:#e2e6ec;border-color:var(--border2)}
|
||||||
.tbtn:active{background:#d7dde5}
|
.tbtn:active{background:#d7dde5}
|
||||||
.tbtn svg{width:18px;height:18px;color:#4b5563}
|
.tbtn svg{width:18px;height:18px;color:#4b5563}
|
||||||
@@ -93,9 +103,17 @@ body{display:grid;grid-template-rows:auto 1fr auto;height:100vh;overflow:hidden}
|
|||||||
.tbgrow{flex:1}
|
.tbgrow{flex:1}
|
||||||
.search-wrap input{padding:5px 9px;border:1px solid var(--border);border-radius:3px;background:#fff;color:var(--text);font-size:12px;width:150px}
|
.search-wrap input{padding:5px 9px;border:1px solid var(--border);border-radius:3px;background:#fff;color:var(--text);font-size:12px;width:150px}
|
||||||
.search-wrap input:focus{outline:none;border-color:var(--accent)}
|
.search-wrap input:focus{outline:none;border-color:var(--accent)}
|
||||||
|
/* eingeklappt nur die Lupe, aufgeklappt nur das Feld */
|
||||||
|
.search-wrap{display:flex;align-items:center}
|
||||||
|
.search-wrap:not(.offen) input{display:none}
|
||||||
|
.search-wrap.offen .tb-lupe{display:none}
|
||||||
|
.search-wrap.offen input{animation:suche-auf .16s ease-out}
|
||||||
|
@keyframes suche-auf{from{width:0;opacity:0}}
|
||||||
|
.tbtn.tb-lupe{padding:6px 8px}
|
||||||
|
#btn-search svg{color:#5a6470}
|
||||||
|
|
||||||
/* ---------- Arbeitsbereich ---------- */
|
/* ---------- 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}
|
.list-area{position:relative;overflow:hidden;min-height:0}
|
||||||
/* Skin-Hintergrund (wie MultiMCs Katze) – liegt hinter den Instanzen */
|
/* Skin-Hintergrund (wie MultiMCs Katze) – liegt hinter den Instanzen */
|
||||||
.list-area::after{
|
.list-area::after{
|
||||||
@@ -160,7 +178,7 @@ body.file-drop-active::after{
|
|||||||
.empty p{max-width:340px;margin:0 0 8px}
|
.empty p{max-width:340px;margin:0 0 8px}
|
||||||
|
|
||||||
/* ---------- Statusleiste (zwei Zeilen) ---------- */
|
/* ---------- 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-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}
|
.st-row2{background:#e4e4e4;border-top:1px solid var(--border2);color:#444}
|
||||||
.auth-warn{
|
.auth-warn{
|
||||||
@@ -327,6 +345,11 @@ body.is-popout>header.toolbar,body.is-popout>main.workspace,body.is-popout>foote
|
|||||||
.skin-bild img{max-width:100%;max-height:100%;image-rendering:pixelated}
|
.skin-bild img{max-width:100%;max-height:100%;image-rendering:pixelated}
|
||||||
.skin-bild canvas{max-width:100%;max-height:100%;cursor:grab}
|
.skin-bild canvas{max-width:100%;max-height:100%;cursor:grab}
|
||||||
.skin-bild canvas:active{cursor:grabbing}
|
.skin-bild canvas:active{cursor:grabbing}
|
||||||
|
.popout-host .skin-bild{height:240px}
|
||||||
|
.skin-umhang{margin-top:12px;padding-top:10px;border-top:1px solid var(--border2)}
|
||||||
|
.skin-umhang .field-row{margin-top:6px}
|
||||||
|
.skin-umhang .field{margin-bottom:6px}
|
||||||
|
.skin-umhang-aero{display:flex;align-items:center;gap:8px;min-height:30px;flex-wrap:wrap}
|
||||||
.crash-details{margin-top:12px}
|
.crash-details{margin-top:12px}
|
||||||
.crash-details summary{cursor:pointer;font-size:12px;color:var(--dim)}
|
.crash-details summary{cursor:pointer;font-size:12px;color:var(--dim)}
|
||||||
.crash-voll{max-height:320px;overflow:auto;margin-top:8px;padding:10px;background:rgba(0,0,0,.25);border-radius:4px;font-family:var(--mono);font-size:11px;line-height:1.45;white-space:pre-wrap;word-break:break-word}
|
.crash-voll{max-height:320px;overflow:auto;margin-top:8px;padding:10px;background:rgba(0,0,0,.25);border-radius:4px;font-family:var(--mono);font-size:11px;line-height:1.45;white-space:pre-wrap;word-break:break-word}
|
||||||
@@ -640,3 +663,60 @@ img.account-av{object-fit:cover;image-rendering:auto}
|
|||||||
::-webkit-scrollbar{width:14px;height:14px}
|
::-webkit-scrollbar{width:14px;height:14px}
|
||||||
::-webkit-scrollbar-thumb{background:#c1c1c1;border:3px solid var(--area);border-radius:7px}
|
::-webkit-scrollbar-thumb{background:#c1c1c1;border:3px solid var(--area);border-radius:7px}
|
||||||
::-webkit-scrollbar-thumb:hover{background:#a8a8a8}
|
::-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-ablage{display:none}
|
||||||
|
.se-u2d canvas{cursor:crosshair}
|
||||||
|
.se-u2d[data-werkzeug="pipette"] canvas{cursor:copy}
|
||||||
|
.se-fuss{align-items:center}
|
||||||
|
.se-status{flex:1;min-width:0;margin-left:6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Plugin Name: AeroMC Freunde
|
* Plugin Name: AeroMC Freunde
|
||||||
* Description: Backend für das Freundesystem des AeroMC Launchers - meldet, wer gerade online/im Spiel ist, 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.
|
* 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 sowie eigene Umhänge (auch HD) aus dem Skin-Editor. Anmeldung über Mojangs Sitzungsprüfung (wie bei Minecraft-Servern) - ohne eigenes Passwort.
|
||||||
* Version: 1.4.0
|
* Version: 1.5.0
|
||||||
* Author: M_Viper
|
* Author: M_Viper
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ if (!defined('ABSPATH')) {
|
|||||||
exit; // kein direkter Aufruf der Datei
|
exit; // kein direkter Aufruf der Datei
|
||||||
}
|
}
|
||||||
|
|
||||||
define('AEROMC_FRIENDS_VERSION', '1.4.0');
|
define('AEROMC_FRIENDS_VERSION', '1.5.0');
|
||||||
define('AEROMC_FRIENDS_PRESENCE_TABLE', 'aeromc_presence');
|
define('AEROMC_FRIENDS_PRESENCE_TABLE', 'aeromc_presence');
|
||||||
define('AEROMC_FRIENDS_FRIENDS_TABLE', 'aeromc_friends');
|
define('AEROMC_FRIENDS_FRIENDS_TABLE', 'aeromc_friends');
|
||||||
define('AEROMC_FRIENDS_INVITES_TABLE', 'aeromc_invites');
|
define('AEROMC_FRIENDS_INVITES_TABLE', 'aeromc_invites');
|
||||||
@@ -18,6 +18,7 @@ define('AEROMC_FRIENDS_SESSIONS_TABLE', 'aeromc_sessions');
|
|||||||
define('AEROMC_FRIENDS_SHARES_TABLE', 'aeromc_shares');
|
define('AEROMC_FRIENDS_SHARES_TABLE', 'aeromc_shares');
|
||||||
define('AEROMC_FRIENDS_MESSAGES_TABLE', 'aeromc_messages');
|
define('AEROMC_FRIENDS_MESSAGES_TABLE', 'aeromc_messages');
|
||||||
define('AEROMC_FRIENDS_REPORTS_TABLE', 'aeromc_reports');
|
define('AEROMC_FRIENDS_REPORTS_TABLE', 'aeromc_reports');
|
||||||
|
define('AEROMC_FRIENDS_CAPES_TABLE', 'aeromc_capes');
|
||||||
|
|
||||||
// Übergangsmodus: false = Anfragen ohne Anmeldung werden noch angenommen, aber
|
// Übergangsmodus: false = Anfragen ohne Anmeldung werden noch angenommen, aber
|
||||||
// nur für Spieler, die sich noch NIE über die neue Anmeldung gemeldet haben
|
// nur für Spieler, die sich noch NIE über die neue Anmeldung gemeldet haben
|
||||||
@@ -143,6 +144,22 @@ function aeromc_friends_install() {
|
|||||||
) $charset_collate;";
|
) $charset_collate;";
|
||||||
dbDelta($sql_reports);
|
dbDelta($sql_reports);
|
||||||
|
|
||||||
|
// eigene Umhänge (Bild liegt unter wp-content/uploads/aeromc-umhaenge)
|
||||||
|
$capes_table = $wpdb->prefix . AEROMC_FRIENDS_CAPES_TABLE;
|
||||||
|
$sql_capes = "CREATE TABLE $capes_table (
|
||||||
|
uuid varchar(32) NOT NULL,
|
||||||
|
username varchar(32) NOT NULL,
|
||||||
|
datei varchar(64) NOT NULL DEFAULT '',
|
||||||
|
breite smallint(5) unsigned NOT NULL DEFAULT 0,
|
||||||
|
hoehe smallint(5) unsigned NOT NULL DEFAULT 0,
|
||||||
|
hash varchar(40) NOT NULL DEFAULT '',
|
||||||
|
status varchar(16) NOT NULL DEFAULT 'aktiv',
|
||||||
|
updated_at datetime NOT NULL,
|
||||||
|
PRIMARY KEY (uuid),
|
||||||
|
KEY status (status)
|
||||||
|
) $charset_collate;";
|
||||||
|
dbDelta($sql_capes);
|
||||||
|
|
||||||
update_option('aeromc_friends_db_version', AEROMC_FRIENDS_VERSION);
|
update_option('aeromc_friends_db_version', AEROMC_FRIENDS_VERSION);
|
||||||
}
|
}
|
||||||
register_activation_hook(__FILE__, 'aeromc_friends_install');
|
register_activation_hook(__FILE__, 'aeromc_friends_install');
|
||||||
@@ -157,6 +174,7 @@ add_action('plugins_loaded', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
require_once plugin_dir_path(__FILE__) . 'includes/routes.php';
|
require_once plugin_dir_path(__FILE__) . 'includes/routes.php';
|
||||||
|
require_once plugin_dir_path(__FILE__) . 'includes/capes.php';
|
||||||
if (is_admin()) {
|
if (is_admin()) {
|
||||||
require_once plugin_dir_path(__FILE__) . 'includes/admin.php';
|
require_once plugin_dir_path(__FILE__) . 'includes/admin.php';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,306 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AeroMC-Umhänge: eigene (auch HD-)Umhänge, die Spieler im Launcher gestalten.
|
||||||
|
*
|
||||||
|
* POST /aeromc/v1/cape { uuid, image } Umhang hochladen/ersetzen (Anmeldung Pflicht)
|
||||||
|
* POST /aeromc/v1/cape/remove { uuid } eigenen Umhang entfernen (Anmeldung Pflicht)
|
||||||
|
* GET /aeromc/v1/cape?uuid= Umhang eines Spielers (öffentlich)
|
||||||
|
* GET /aeromc/v1/capes?uuids=a,b,c mehrere auf einmal, für Launcher und Mod (öffentlich)
|
||||||
|
*
|
||||||
|
* Bilder werden streng geprüft (echtes PNG, Seitenverhältnis 2:1, 64×32 bis
|
||||||
|
* 1024×512) und – falls GD verfügbar ist – neu kodiert, damit keine fremden
|
||||||
|
* Daten im Bild mitreisen. Der Dateiname enthält einen Teil der Prüfsumme, so
|
||||||
|
* ändert sich die Adresse bei jedem neuen Umhang (kein veralteter Zwischenspeicher).
|
||||||
|
* Admins können Umhänge unter "AeroMC Fehler → Umhänge" löschen oder sperren.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
define('AEROMC_FRIENDS_CAPE_DIR', 'aeromc-umhaenge');
|
||||||
|
define('AEROMC_FRIENDS_CAPE_MAX_BYTES', 3000000);
|
||||||
|
define('AEROMC_FRIENDS_CAPE_MIN_GAP', 15); // Sekunden zwischen zwei Uploads
|
||||||
|
define('AEROMC_FRIENDS_CAPE_MAX_UUIDS', 100); // je Sammelabfrage
|
||||||
|
|
||||||
|
function aeromc_friends_capes_table() {
|
||||||
|
global $wpdb;
|
||||||
|
return $wpdb->prefix . AEROMC_FRIENDS_CAPES_TABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
function aeromc_friends_cape_dir() {
|
||||||
|
$uploads = wp_upload_dir();
|
||||||
|
return [
|
||||||
|
'pfad' => trailingslashit($uploads['basedir']) . AEROMC_FRIENDS_CAPE_DIR,
|
||||||
|
'url' => trailingslashit($uploads['baseurl']) . AEROMC_FRIENDS_CAPE_DIR,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Datei eines Umhangs löschen (nur Namen in unserem Schema). */
|
||||||
|
function aeromc_friends_cape_delete_file($datei) {
|
||||||
|
if (!$datei || !preg_match('/^[a-f0-9]{32}-[a-f0-9]{8}\.png$/', (string) $datei)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$ort = aeromc_friends_cape_dir();
|
||||||
|
$voll = $ort['pfad'] . '/' . $datei;
|
||||||
|
if (file_exists($voll)) {
|
||||||
|
@unlink($voll);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Zeile -> öffentliche Angaben */
|
||||||
|
function aeromc_friends_cape_info($r) {
|
||||||
|
$ort = aeromc_friends_cape_dir();
|
||||||
|
return [
|
||||||
|
'url' => $ort['url'] . '/' . $r->datei,
|
||||||
|
'width' => (int) $r->breite,
|
||||||
|
'height' => (int) $r->hoehe,
|
||||||
|
'hash' => $r->hash,
|
||||||
|
'updated' => $r->updated_at,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft ein Umhang-Bild und gibt [daten, breite, hoehe] zurück oder WP_Error.
|
||||||
|
*/
|
||||||
|
function aeromc_friends_cape_check_image($base64) {
|
||||||
|
$daten = base64_decode(preg_replace('/^data:image\/png;base64,/', '', (string) $base64), true);
|
||||||
|
if ($daten === false || strlen($daten) < 60) {
|
||||||
|
return new WP_Error('aeromc_invalid', 'Ungültiges Bild.', ['status' => 400]);
|
||||||
|
}
|
||||||
|
if (strlen($daten) > AEROMC_FRIENDS_CAPE_MAX_BYTES) {
|
||||||
|
return new WP_Error('aeromc_too_big', 'Der Umhang ist zu groß (Datei).', ['status' => 413]);
|
||||||
|
}
|
||||||
|
if (strncmp($daten, "\x89PNG\r\n\x1A\n", 8) !== 0 || substr($daten, 12, 4) !== 'IHDR') {
|
||||||
|
return new WP_Error('aeromc_invalid', 'Nur PNG-Bilder.', ['status' => 400]);
|
||||||
|
}
|
||||||
|
$kopf = unpack('Nbreite/Nhoehe', substr($daten, 16, 8));
|
||||||
|
$breite = (int) $kopf['breite'];
|
||||||
|
$hoehe = (int) $kopf['hoehe'];
|
||||||
|
if (!in_array($breite, [64, 128, 256, 512, 1024], true) || $breite !== 2 * $hoehe) {
|
||||||
|
return new WP_Error('aeromc_invalid', 'Ein Umhang muss 64×32, 128×64, 256×128, 512×256 oder 1024×512 Pixel groß sein.', ['status' => 400]);
|
||||||
|
}
|
||||||
|
// neu kodieren: entfernt alles außer den Bildpunkten
|
||||||
|
if (function_exists('imagecreatefromstring')) {
|
||||||
|
$bild = @imagecreatefromstring($daten);
|
||||||
|
if (!$bild || imagesx($bild) !== $breite || imagesy($bild) !== $hoehe) {
|
||||||
|
return new WP_Error('aeromc_invalid', 'Das Bild ist beschädigt.', ['status' => 400]);
|
||||||
|
}
|
||||||
|
imagealphablending($bild, false);
|
||||||
|
imagesavealpha($bild, true);
|
||||||
|
ob_start();
|
||||||
|
imagepng($bild, null, 9);
|
||||||
|
$daten = ob_get_clean();
|
||||||
|
imagedestroy($bild);
|
||||||
|
}
|
||||||
|
return [$daten, $breite, $hoehe];
|
||||||
|
}
|
||||||
|
|
||||||
|
add_action('rest_api_init', function () {
|
||||||
|
register_rest_route('aeromc/v1', '/cape', [
|
||||||
|
[
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => 'aeromc_friends_route_cape_upload',
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => 'aeromc_friends_route_cape_get',
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
register_rest_route('aeromc/v1', '/cape/remove', [
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => 'aeromc_friends_route_cape_remove',
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
]);
|
||||||
|
register_rest_route('aeromc/v1', '/capes', [
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => 'aeromc_friends_route_capes',
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Anmeldung ist hier immer Pflicht – auch im Übergangsmodus (öffentlich sichtbare Bilder). */
|
||||||
|
function aeromc_friends_cape_auth($request, $uuid) {
|
||||||
|
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||||||
|
if (is_wp_error($sitzung)) {
|
||||||
|
return $sitzung;
|
||||||
|
}
|
||||||
|
if (!$sitzung) {
|
||||||
|
return new WP_Error('aeromc_auth', 'Anmeldung erforderlich.', ['status' => 401]);
|
||||||
|
}
|
||||||
|
return $sitzung;
|
||||||
|
}
|
||||||
|
|
||||||
|
function aeromc_friends_route_cape_upload($request) {
|
||||||
|
global $wpdb;
|
||||||
|
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
||||||
|
if (!$uuid) {
|
||||||
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid.', ['status' => 400]);
|
||||||
|
}
|
||||||
|
$sitzung = aeromc_friends_cape_auth($request, $uuid);
|
||||||
|
if (is_wp_error($sitzung)) {
|
||||||
|
return $sitzung;
|
||||||
|
}
|
||||||
|
$table = aeromc_friends_capes_table();
|
||||||
|
$alt = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE uuid = %s", $uuid));
|
||||||
|
if ($alt && $alt->status === 'gesperrt') {
|
||||||
|
return new WP_Error('aeromc_blocked', 'Dein Umhang wurde vom Betreiber gesperrt.', ['status' => 403]);
|
||||||
|
}
|
||||||
|
if ($alt && time() - strtotime($alt->updated_at . ' UTC') < AEROMC_FRIENDS_CAPE_MIN_GAP) {
|
||||||
|
return new WP_Error('aeromc_too_fast', 'Bitte einen Moment warten, bevor du den Umhang erneut hochlädst.', ['status' => 429]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$geprueft = aeromc_friends_cape_check_image($request->get_param('image'));
|
||||||
|
if (is_wp_error($geprueft)) {
|
||||||
|
return $geprueft;
|
||||||
|
}
|
||||||
|
list($daten, $breite, $hoehe) = $geprueft;
|
||||||
|
$hash = sha1($daten);
|
||||||
|
|
||||||
|
$ort = aeromc_friends_cape_dir();
|
||||||
|
if (!wp_mkdir_p($ort['pfad'])) {
|
||||||
|
return new WP_Error('aeromc_error', 'Umhang konnte nicht gespeichert werden.', ['status' => 500]);
|
||||||
|
}
|
||||||
|
if (!file_exists($ort['pfad'] . '/index.html')) {
|
||||||
|
file_put_contents($ort['pfad'] . '/index.html', '');
|
||||||
|
}
|
||||||
|
$datei = $uuid . '-' . substr($hash, 0, 8) . '.png';
|
||||||
|
if (file_put_contents($ort['pfad'] . '/' . $datei, $daten) === false) {
|
||||||
|
return new WP_Error('aeromc_error', 'Umhang konnte nicht gespeichert werden.', ['status' => 500]);
|
||||||
|
}
|
||||||
|
if ($alt && $alt->datei !== $datei) {
|
||||||
|
aeromc_friends_cape_delete_file($alt->datei);
|
||||||
|
}
|
||||||
|
$zeile = [
|
||||||
|
'uuid' => $uuid,
|
||||||
|
'username' => $sitzung->username,
|
||||||
|
'datei' => $datei,
|
||||||
|
'breite' => $breite,
|
||||||
|
'hoehe' => $hoehe,
|
||||||
|
'hash' => $hash,
|
||||||
|
'status' => 'aktiv',
|
||||||
|
'updated_at' => current_time('mysql', true),
|
||||||
|
];
|
||||||
|
$wpdb->replace($table, $zeile);
|
||||||
|
return ['ok' => true, 'cape' => aeromc_friends_cape_info((object) $zeile)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function aeromc_friends_route_cape_remove($request) {
|
||||||
|
global $wpdb;
|
||||||
|
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
||||||
|
if (!$uuid) {
|
||||||
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid.', ['status' => 400]);
|
||||||
|
}
|
||||||
|
$sitzung = aeromc_friends_cape_auth($request, $uuid);
|
||||||
|
if (is_wp_error($sitzung)) {
|
||||||
|
return $sitzung;
|
||||||
|
}
|
||||||
|
$table = aeromc_friends_capes_table();
|
||||||
|
$alt = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE uuid = %s", $uuid));
|
||||||
|
if ($alt) {
|
||||||
|
aeromc_friends_cape_delete_file($alt->datei);
|
||||||
|
if ($alt->status === 'gesperrt') {
|
||||||
|
// Sperre bleibt bestehen, nur das Bild ist weg
|
||||||
|
$wpdb->update($table, ['datei' => '', 'hash' => ''], ['uuid' => $uuid]);
|
||||||
|
} else {
|
||||||
|
$wpdb->delete($table, ['uuid' => $uuid]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ['ok' => true];
|
||||||
|
}
|
||||||
|
|
||||||
|
function aeromc_friends_route_cape_get($request) {
|
||||||
|
global $wpdb;
|
||||||
|
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
||||||
|
if (!$uuid) {
|
||||||
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid.', ['status' => 400]);
|
||||||
|
}
|
||||||
|
$table = aeromc_friends_capes_table();
|
||||||
|
$r = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE uuid = %s AND status = 'aktiv'", $uuid));
|
||||||
|
return ['ok' => true, 'cape' => $r ? aeromc_friends_cape_info($r) : null];
|
||||||
|
}
|
||||||
|
|
||||||
|
function aeromc_friends_route_capes($request) {
|
||||||
|
global $wpdb;
|
||||||
|
$liste = array_filter(array_map('aeromc_friends_normalize_uuid', explode(',', (string) $request->get_param('uuids'))));
|
||||||
|
$liste = array_slice(array_values(array_unique($liste)), 0, AEROMC_FRIENDS_CAPE_MAX_UUIDS);
|
||||||
|
$ergebnis = [];
|
||||||
|
if ($liste) {
|
||||||
|
$table = aeromc_friends_capes_table();
|
||||||
|
$platz = implode(',', array_fill(0, count($liste), '%s'));
|
||||||
|
$rows = $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT * FROM $table WHERE status = 'aktiv' AND uuid IN ($platz)", $liste
|
||||||
|
));
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$ergebnis[$r->uuid] = aeromc_friends_cape_info($r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$antwort = new WP_REST_Response(['ok' => true, 'capes' => (object) $ergebnis]);
|
||||||
|
// kurz zwischenspeichern lassen – der Mod fragt beim Betreten eines Servers viele auf einmal ab
|
||||||
|
$antwort->header('Cache-Control', 'public, max-age=60');
|
||||||
|
return $antwort;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Admin: Umhänge prüfen, löschen, sperren ----------
|
||||||
|
if (is_admin()) {
|
||||||
|
add_action('admin_menu', function () {
|
||||||
|
add_submenu_page('aeromc-reports', 'AeroMC Umhänge', 'Umhänge', 'manage_options', 'aeromc-capes', 'aeromc_friends_capes_page');
|
||||||
|
}, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
function aeromc_friends_capes_page() {
|
||||||
|
global $wpdb;
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$table = aeromc_friends_capes_table();
|
||||||
|
if (isset($_POST['aeromc_cape'], $_POST['aeromc_aktion']) && check_admin_referer('aeromc_capes')) {
|
||||||
|
$uuid = aeromc_friends_normalize_uuid($_POST['aeromc_cape']);
|
||||||
|
$r = $uuid ? $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE uuid = %s", $uuid)) : null;
|
||||||
|
if ($r) {
|
||||||
|
$aktion = (string) $_POST['aeromc_aktion'];
|
||||||
|
if ($aktion === 'loeschen') {
|
||||||
|
aeromc_friends_cape_delete_file($r->datei);
|
||||||
|
$wpdb->delete($table, ['uuid' => $uuid]);
|
||||||
|
echo '<div class="notice notice-success"><p>Umhang von ' . esc_html($r->username) . ' gelöscht.</p></div>';
|
||||||
|
} elseif ($aktion === 'sperren') {
|
||||||
|
aeromc_friends_cape_delete_file($r->datei);
|
||||||
|
$wpdb->update($table, ['status' => 'gesperrt', 'datei' => '', 'hash' => ''], ['uuid' => $uuid]);
|
||||||
|
echo '<div class="notice notice-success"><p>' . esc_html($r->username) . ' gesperrt – der Umhang ist entfernt, neue Uploads werden abgelehnt.</p></div>';
|
||||||
|
} elseif ($aktion === 'entsperren') {
|
||||||
|
$wpdb->delete($table, ['uuid' => $uuid]);
|
||||||
|
echo '<div class="notice notice-success"><p>Sperre für ' . esc_html($r->username) . ' aufgehoben.</p></div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$ort = aeromc_friends_cape_dir();
|
||||||
|
$rows = $wpdb->get_results("SELECT * FROM $table ORDER BY updated_at DESC LIMIT 300");
|
||||||
|
echo '<div class="wrap"><h1>AeroMC Umhänge</h1>';
|
||||||
|
echo '<p>Eigene Umhänge, die Spieler im AeroMC-Skin-Editor hochgeladen haben. „Sperren“ entfernt den Umhang und verhindert neue Uploads dieses Spielers.</p>';
|
||||||
|
if (!$rows) {
|
||||||
|
echo '<p>Noch keine Umhänge hochgeladen.</p></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
echo '<table class="widefat striped"><thead><tr><th style="width:150px">Umhang</th><th>Spieler</th><th>Größe</th><th>Geändert (UTC)</th><th>Status</th><th></th></tr></thead><tbody>';
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$bild = $r->datei
|
||||||
|
? '<img src="' . esc_url($ort['url'] . '/' . $r->datei) . '" alt="" style="width:128px;height:64px;image-rendering:pixelated;background:#ddd;border:1px solid #ccc" />'
|
||||||
|
: '–';
|
||||||
|
echo '<tr><td>' . $bild . '</td><td>' . esc_html($r->username) . '<br /><small>' . esc_html($r->uuid) . '</small></td>'
|
||||||
|
. '<td>' . esc_html($r->breite . '×' . $r->hoehe) . '</td><td>' . esc_html($r->updated_at) . '</td><td>' . esc_html($r->status) . '</td><td>';
|
||||||
|
echo '<form method="post" style="display:flex;gap:6px">';
|
||||||
|
wp_nonce_field('aeromc_capes');
|
||||||
|
echo '<input type="hidden" name="aeromc_cape" value="' . esc_attr($r->uuid) . '" />';
|
||||||
|
if ($r->status === 'gesperrt') {
|
||||||
|
echo '<button class="button" name="aeromc_aktion" value="entsperren">Sperre aufheben</button>';
|
||||||
|
} else {
|
||||||
|
echo '<button class="button" name="aeromc_aktion" value="loeschen" onclick="return confirm(\'Umhang löschen?\')">Löschen</button>';
|
||||||
|
echo '<button class="button button-link-delete" name="aeromc_aktion" value="sperren" onclick="return confirm(\'Spieler sperren? Der Umhang wird entfernt und neue Uploads abgelehnt.\')">Sperren</button>';
|
||||||
|
}
|
||||||
|
echo '</form></td></tr>';
|
||||||
|
}
|
||||||
|
echo '</tbody></table></div>';
|
||||||
|
}
|
||||||