3 Commits
Author SHA1 Message Date
M_Viper e5ccf0b4cc Upload via GUI (123 Dateien) 2026-10-03 07:09:09 +00:00
M_Viper 10ee45cbc2 Upload via GUI (123 Dateien) 2026-10-03 07:09:08 +00:00
M_Viper bc68395437 Upload via GUI (123 Dateien) 2026-10-03 07:09:06 +00:00
47 changed files with 4849 additions and 1000 deletions
+22
View File
@@ -2,6 +2,28 @@
Alle nennenswerten Änderungen an AeroMC werden hier festgehalten.
## [1.1.6] - 2026-10-02
### Neu
- Skin-Editor: Skins direkt im Launcher pixeln – erreichbar im Skin-Fenster über „Im Editor bearbeiten…“
- Zwei Schichten: Grundschicht und 2. Schicht getrennt bemalen – die 2. Schicht liegt wie eine Hülle über der Figur und lässt Haare, Kapuzen oder Ärmel plastisch wirken
- Wahlweise auf der aufgeklappten 2D-Vorlage oder direkt auf der drehbaren 3D-Figur malen – die andere Ansicht läuft als Live-Vorschau mit und ist ebenfalls bemalbar
- Werkzeuge: Stift, Radierer (nur 2. Schicht), Pipette (auch Alt+Klick), Füllen, Farbwähler mit den zuletzt benutzten Farben
- Rückgängig/Wiederholen mit Strg+Z und Strg+Y
- Spiegeln: malt gleichzeitig auf der anderen Körperseite (linker ↔ rechter Arm/Bein)
- Ebenen-Werkzeuge: 2. Schicht ein-/ausblenden, Grundschicht durchscheinen lassen, 2. Schicht leeren
- Aktuellen Skin laden, PNG öffnen oder speichern und das Ergebnis mit einem Klick als Skin übernehmen – klassische und schmale Arme, alte 64×32-Skins werden automatisch umgewandelt
- Skin-Hintergrund: Armform wählbar (Optionen → Darstellung) – „Automatisch“ übernimmt die Armform des Kontos, alternativ fest „Normal“ oder „Schmal“
- Designs zur Auswahl (Optionen → Darstellung → Design, mit Vorschau-Kacheln): „Klassisch“ hell oder dunkel wie bisher, „AeroMC“ im Look der Webseite (Nachtblau, Cyan-Verläufe, runde Knöpfe), „Nachtlila“ (wie AeroMC, nur in dunklem Lila) und „Minecraft“ mit Block-Optik, Stein-Knöpfen, grünem Starten-Knopf und eigener Pixel-Schrift
- Ein gewähltes Design gilt sofort in allen offenen Fenstern
- Eigene Titelleiste im Design statt der weißen Windows-Leiste – Minimieren, Maximieren und Schließen bleiben echte Windows-Knöpfe (Andocken, Fensterlayouts) und werden passend eingefärbt
- Import aus der Modrinth App (Importieren → „Modrinth App“, auch im Installer): übernimmt Name, Gruppe, Symbol, Minecraft-Version, Loader, Arbeitsspeicher, Java-Einstellungen, Spielzeit sowie Mods, Welten und Einstellungen – auch bei verlegtem Datenordner und während die Modrinth App läuft
- Aus der Modrinth App übernommene Modrinth-Modpacks bleiben mit ihrem Paket verknüpft und lassen sich in AeroMC aktualisieren
### Behoben
- Das Symbol für „Optionen“ sah aus wie eine Sonne – jetzt ein Zahnrad
- Der Skin-Hintergrund zeigte schmale (Alex-)Skins mit breiten Armen – dadurch erschienen falsche Streifen an den Armen
## [1.1.5] - 2026-10-02
### Neu
+48 -11
View File
@@ -6,7 +6,7 @@
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.6
<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
**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
- Von Freunden geteilte Instanzen per Code einlösen
@@ -64,7 +64,8 @@ Der Launcher konzentriert sich auf den Alltag: Instanz anlegen, Konto auswählen
**Konten, Welten und Backups**
- Microsoft-Login mit mehreren Konten
- 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
- Skin-Hintergrund des aktiven Kontos (mehrere Posen, passende Armform, einstellbare Deckkraft, optional wechselnd)
- Welten, Ressourcenpakete, Datenpakete, Screenshots, Serverliste und Logdateien verwalten
- Serverliste mit Live-Status und Desktop-Verknüpfung pro Server (startet die Instanz und verbindet direkt)
- Welten mit einem Klick in eine andere Instanz kopieren
@@ -80,7 +81,22 @@ Der Launcher konzentriert sich auf den Alltag: Instanz anlegen, Konto auswählen
- Update-Suche sofort beim Start, Updates installieren sich ohne Rückfragen
- Optionaler Autostart mit Windows (im Infobereich statt als Fenster)
- Rechtsklick-Menü in allen Textfeldern (Ausschneiden, Kopieren, Einfügen)
- 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
@@ -105,7 +121,7 @@ Automatische Updates bleiben getrennt: Setup erhält nur Setup-Updates, Portable
## Erste Schritte
1. AeroMC installieren oder die portable Datei starten.
2. Über **Konten** ein Microsoft-Konto hinzufügen.
2. Über den Konto-Knopf oben rechts (oder **Optionen → Konten**) ein Microsoft-Konto hinzufügen.
3. Eine neue Instanz anlegen, Minecraft-Version und optional einen Loader wählen – bei Fabric, Quilt oder NeoForge auf Wunsch gleich mit Performance-Paket.
4. Optional Mods/Ressourcenpakete/Shader hinzufügen oder bestehende Setups über **Importieren** übernehmen.
5. Instanz starten (online oder offline).
@@ -172,6 +188,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`) |
| Minecraft Launcher | Profile aus dem offiziellen Launcher |
| CurseForge | Instanzen aus dem CurseForge-Ordner |
| Modrinth App | Instanzen aus der Modrinth App (`%APPDATA%\ModrinthApp`) inkl. Einstellungen, Spielzeit und Modpack-Verknüpfung |
| MultiMC / Prism | Vorhandene MultiMC- oder Prism-Instanzen |
| Technic | Technic-Katalog direkt durchsuchen und installieren, oder eine vorhandene Technic-Installation importieren (`%APPDATA%\.technic\modpacks`) |
| Per Code | Von einem Freund geteilte Instanz einlösen (siehe **Instanzen → Instanz teilen**) |
@@ -180,7 +197,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.
**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.
@@ -222,7 +241,6 @@ Ressourcenpakete und Shader funktionieren auch in Vanilla-Instanzen. Mods benöt
- Meldung im Spiel mit 3D-Spielerkopf, sobald ein Freund online kommt
- **Beitreten** startet eine passende eigene Instanz (gleiche Minecraft-Version, bevorzugt mit dem Server in der Liste) und verbindet direkt; gibt es keine, wird eine Vanilla-Instanz angelegt
- **Einladen** schickt Freunden den eigenen aktuellen Server. Im Launcher erscheint die Einladung mit „Beitreten“, im Spiel als Meldung – dort kopiert **Strg+Alt+J** die Adresse für „Direkt verbinden“
- **Kurznachrichten**: 💬 beim Freund öffnet eine kleine Unterhaltung mit Schnellantworten („Bin in 10 Min. online“, „Komm online!“ …) oder eigenem Text bis 200 Zeichen. Neue Nachrichten erscheinen als Hinweis im Launcher und – beim Spielen – als Meldung im Spiel; der Zähler am „Freunde“-Knopf zählt offene Anfragen und ungelesene Nachrichten. Nachrichten werden nach 7 Tagen gelöscht.
- **Screenshots verschicken**: In **Bildschirmfotos verwalten** schickt 📤 ein Bild (verkleinert auf höchstens 1600 Pixel Breite) samt optionalem Text an einen Freund; er sieht es im Chat.
@@ -234,15 +252,34 @@ Das Freundesystem funktioniert nur zwischen Spielern, die AeroMC nutzen und mind
## 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
- Live-Vorschau am 3D-Modell vor dem Anwenden
- 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.
### Skin-Editor
**Im Editor bearbeiten…** öffnet einen eigenen Pixel-Editor – mit der gewählten PNG-Datei oder dem aktuellen Skin des Kontos:
- **Zwei Schichten:** Grundschicht und 2. Schicht (die Hülle über der Figur für Haare, Jacken, Ärmel …) getrennt bemalen; 2. Schicht ein-/ausblenden, Grundschicht darunter durchscheinen lassen, 2. Schicht leeren
- **2D oder 3D:** auf der aufgeklappten Vorlage oder direkt auf der drehbaren Figur malen – die andere Ansicht läuft als Live-Vorschau mit und ist ebenfalls bemalbar
- **Werkzeuge:** Stift, Radierer (nur 2. Schicht), Pipette (auch Alt+Klick), Füllen, Farbwähler mit zuletzt benutzten Farben
- **Spiegeln:** malt gleichzeitig auf der anderen Körperseite
- Rückgängig/Wiederholen, klassische oder schmale Arme, alte 64×32-Skins werden automatisch umgewandelt
- Neu beginnen, aktuellen Skin laden oder eine PNG-Datei öffnen
- **Als Skin übernehmen** (direkt hochladen) oder **Als PNG speichern…**
| Taste | Wirkung |
|---|---|
| B / E / I / G | Stift / Radierer / Pipette / Füllen |
| M | Spiegeln ein/aus |
| L | Schicht wechseln |
| Strg+Z / Strg+Y | Rückgängig / Wiederholen |
## Backup und Wiederherstellung
Unter **Optionen → Backup**:
@@ -261,7 +298,6 @@ Speicherort: `Dokumente\AeroMC Launcher\Backups`.
## Einstellungen
### Allgemein
- Design (Dunkel / Hell)
- Instanzen sortieren (nach letztem Start / nach Name)
- Instanz-Verzeichnis
- Bereiche: Bedrock-Bereich anzeigen
@@ -270,8 +306,9 @@ 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.
### Darstellung
- Design: Klassisch Dunkel, Klassisch Hell, AeroMC, Nachtlila oder Minecraft – mit Vorschau-Kacheln, gilt nach dem Speichern in allen Fenstern
- Block-Symbole räumlich (3D)
- Skin-Hintergrund: Quelle, Pose, Deckkraft, 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
- Standard-RAM (min./max.)
Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

+19 -1
View File
@@ -5,7 +5,7 @@ using System.Web.Script.Serialization;
namespace AeroMCSetup;
/// <summary>Ein gefundener anderer Launcher, dessen Instanzen AeroMC übernehmen kann.</summary>
/// <param name="kind">Kennung wie im Import-Dialog des Launchers: mc, cf, mmc, technic</param>
/// <param name="kind">Kennung wie im Import-Dialog des Launchers: mc, cf, mr, mmc, technic</param>
public sealed class FoundLauncher(string kind, string name, string path, int count, string unit)
{
public string Kind { get; } = kind;
@@ -37,6 +37,7 @@ public static class LauncherImport
}
Add(DetectMinecraftLauncher);
Add(DetectCurseForge);
Add(DetectModrinthApp);
Add(() => DetectMultiMc("Prism Launcher", System.IO.Path.Combine(AppData, "PrismLauncher")));
Add(() => DetectMultiMc("PolyMC", System.IO.Path.Combine(AppData, "PolyMC")));
// MultiMC (und portable Prism/PolyMC) liegen irgendwo – über ihre Verknüpfungen finden
@@ -92,6 +93,23 @@ public static class LauncherImport
return new FoundLauncher("cf", "CurseForge", dir, count, count == 1 ? "Instanz" : "Instanzen");
}
// ---------- Modrinth App ----------
private static FoundLauncher? DetectModrinthApp()
{
// neuere Fassungen: ModrinthApp, sehr alte: com.modrinth.theseus (siehe src/mrimport.js)
var root = new[]
{
System.IO.Path.Combine(AppData, "ModrinthApp"),
System.IO.Path.Combine(AppData, "com.modrinth.theseus"),
}.FirstOrDefault(d => Directory.Exists(System.IO.Path.Combine(d, "profiles")));
if (root is null) return null;
string[] markers = ["profile.json", "mods", "config", "resourcepacks", "shaderpacks", "saves", "options.txt"];
int count = Directory.GetDirectories(System.IO.Path.Combine(root, "profiles")).Count(d => markers.Any(m => Exists(d, m)));
return new FoundLauncher("mr", "Modrinth App", root, count, count == 1 ? "Instanz" : "Instanzen");
}
// ---------- Prism Launcher / PolyMC (MultiMC-Format) ----------
private static FoundLauncher? DetectMultiMc(string name, string root)
@@ -5,7 +5,7 @@
},
"projects": {
"C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj": {
"version": "1.1.5",
"version": "1.1.6",
"restore": {
"projectUniqueName": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj",
"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;
Binary file not shown.
Binary file not shown.
+77
View File
@@ -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();
}
}
}
Binary file not shown.
+842
View File
@@ -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;
}
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -14,11 +14,11 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
[assembly: System.Reflection.AssemblyCopyrightAttribute("Copyright (c) M_Viper. Alle Rechte vorbehalten.")]
[assembly: System.Reflection.AssemblyDescriptionAttribute("Installiert AeroMC")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.1.5.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.1.5")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.1.6.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.1.6")]
[assembly: System.Reflection.AssemblyProductAttribute("AeroMC Setup")]
[assembly: System.Reflection.AssemblyTitleAttribute("AeroMC Setup")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.1.5.0")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.1.6.0")]
[assembly: System.Resources.NeutralResourcesLanguageAttribute("de-DE")]
// Von der MSBuild WriteCodeFragment-Klasse generiert.
@@ -1 +1 @@
6fa3eb63bf9e7c9a77cca67d608dff49330e3a93e70e18548b24192d4ef82422
5b6f28fb41c3f0e5fe29f9489cfc618a7d6593efb71d420030e4a1d4fdb4066e
+1 -1
View File
@@ -418,7 +418,7 @@
"C:\\Users\\Viper\\.nuget\\packages\\": {}
},
"project": {
"version": "1.1.5",
"version": "1.1.6",
"restore": {
"projectUniqueName": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj",
"projectName": "AeroMC-Setup",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"version": 2,
"dgSpecHash": "za8GIOA75zI=",
"dgSpecHash": "lJiGEWCFg6M=",
"success": true,
"projectFilePath": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj",
"expectedPackageFiles": [
+9 -2
View File
@@ -1,12 +1,12 @@
{
"name": "aeromc",
"version": "1.1.5",
"version": "1.1.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "aeromc",
"version": "1.1.5",
"version": "1.1.6",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@xhayper/discord-rpc": "^1.3.4",
@@ -15,6 +15,7 @@
"loopback-capture": "^3.0.2",
"node-window-manager": "^2.2.4",
"skinview3d": "^3.4.2",
"sql.js": "^1.14.2",
"undici": "^6.28.0",
"unzipper": "^0.12.5"
},
@@ -4346,6 +4347,12 @@
"license": "BSD-3-Clause",
"optional": true
},
"node_modules/sql.js": {
"version": "1.14.2",
"resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.14.2.tgz",
"integrity": "sha512-3ZGPovObMFrdw79zrUHbfdE/DLIsy8jdNdssmMSQuRAymedU6q84asPt0kgiqrdMYlPegDItiIMfmIXzZnYFcw==",
"license": "MIT"
},
"node_modules/stat-mode": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz",
+2 -1
View File
@@ -1,7 +1,7 @@
{
"name": "aeromc",
"productName": "AeroMC",
"version": "1.1.5",
"version": "1.1.6",
"description": "AeroMC – Instanz-basierter Minecraft-Launcher (à la MultiMC/Prism)",
"homepage": "https://m-viper.de",
"launcherWebsite": "https://aeromc.viper.ipv64.net",
@@ -47,6 +47,7 @@
"loopback-capture": "^3.0.2",
"node-window-manager": "^2.2.4",
"skinview3d": "^3.4.2",
"sql.js": "^1.14.2",
"undici": "^6.28.0",
"unzipper": "^0.12.5"
}
+86
View File
@@ -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')
+270
View File
@@ -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`);
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 484 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 B

+424
View File
@@ -0,0 +1,424 @@
/*
* designs.css – wählbare Designs (Optionen → Darstellung → Design)
* ---------------------------------------------------------------
* Klassisch (hell/dunkel) steht in styles.css. Die Designs hier bauen
* auf den dunklen Regeln auf (data-theme="dark") und überschreiben sie über
* data-design – gleiche Spezifität, deshalb muss diese Datei NACH styles.css
* geladen werden.
*/
/* ===================== Design-Auswahl in den Optionen ===================== */
.design-raster{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}
.settings-card-wide .design-raster{grid-template-columns:repeat(5,minmax(0,1fr))}
@media (max-width:900px){.settings-card-wide .design-raster{grid-template-columns:repeat(2,minmax(0,1fr))}}
.design-wahl{display:flex;flex-direction:column;gap:5px;padding:6px;border:1px solid var(--border);border-radius:8px;background:none;color:var(--text);font-family:inherit;font-size:12px;cursor:pointer;text-align:left}
.design-wahl:hover{border-color:var(--sel-br)}
.design-wahl.on{border-color:var(--sel-br);box-shadow:0 0 0 1px var(--sel-br);background:var(--sel)}
.design-wahl small{font-size:12px;font-weight:600;padding:0 2px}
.dv{position:relative;display:block;width:100%;align-self:stretch;height:58px;border-radius:5px;overflow:hidden;border:1px solid rgba(0,0,0,.25)}
.dv i,.dv b,.dv em{position:absolute;display:block}
.dv i{left:0;right:0;top:0;height:11px}
.dv em{top:11px;right:0;bottom:0;width:24%}
.dv b{left:10px;top:20px;width:22px;height:22px;border-radius:3px}
.dv-dark{background:#242426}.dv-dark i{background:#333337}.dv-dark em{background:#2d2d30;border-left:1px solid #3f3f46}.dv-dark b{background:#0e639c;border:1px solid #1177bb}
.dv-light{background:#e2e2e2}.dv-light i{background:#f0f0f0;border-bottom:1px solid #c4c7cc}.dv-light em{background:#f0f0f0;border-left:1px solid #c4c7cc}.dv-light b{background:#dce8f6;border:1px solid #3a6ea5}
.dv-aero{background:radial-gradient(60px 30px at 35% 10px,rgba(64,126,232,.55),transparent 70%),linear-gradient(to right,rgba(148,175,255,.09) 1px,transparent 1px) 0 0/9px 9px,linear-gradient(to bottom,rgba(148,175,255,.09) 1px,transparent 1px) 0 0/9px 9px,#05070d}
.dv-aero i{background:rgba(10,14,26,.95);border-bottom:1px solid rgba(148,175,255,.18)}
.dv-aero em{background:#0a0e1a;border-left:1px solid rgba(148,175,255,.18)}
.dv-aero b{border-radius:7px;background:linear-gradient(135deg,#8fd8ff,#407ee8 60%,#2658be);box-shadow:0 4px 12px -3px rgba(64,126,232,.9)}
.dv-lila{background:radial-gradient(60px 30px at 35% 10px,rgba(139,92,246,.6),transparent 70%),linear-gradient(to right,rgba(196,165,255,.09) 1px,transparent 1px) 0 0/9px 9px,linear-gradient(to bottom,rgba(196,165,255,.09) 1px,transparent 1px) 0 0/9px 9px,#08050f}
.dv-lila i{background:rgba(15,10,26,.95);border-bottom:1px solid rgba(196,165,255,.18)}
.dv-lila em{background:#0f0a1a;border-left:1px solid rgba(196,165,255,.18)}
.dv-lila b{border-radius:7px;background:linear-gradient(135deg,#e8beff,#8b5cf6 60%,#6d28d9);box-shadow:0 4px 12px -3px rgba(139,92,246,.9)}
.dv-mc{background:url(assets/design/erde-dunkel.png) 0 0/24px 24px;image-rendering:pixelated}
.dv-mc i{height:12px;background:url(assets/design/gras.png) 0 0/16px 4px repeat-x,url(assets/design/schiefer.png) 0 0/24px 24px;border-bottom:1px solid #000}
.dv-mc em{background:url(assets/design/schiefer.png) 0 0/24px 24px;border-left:1px solid #000}
.dv-mc b{border-radius:0;background:#3c8527;border:1px solid #000;box-shadow:inset 1px 1px 0 #6cc349,inset -1px -1px 0 #1d4d13}
/* ===================================================================== */
/* ============ Designs: AeroMC (Blau, wie die Webseite) & Nachtlila ===== */
/* ===================================================================== */
/* Beide Designs teilen sich alle Regeln unten; sie unterscheiden sich nur in
der Palette. --p-* ohne "#" sind RGB-Tripel, damit rgba(var(--p-…),.5) geht. */
:root[data-design="aero"]{
--p-akzent:64,126,232; --p-tief:38,88,190; --p-akzent2:104,196,250; --p-hell:143,216,255;
--p-linie:148,175,255; --p-win:10,14,26; --p-surface:13,20,40; --p-surface2:16,26,53; --p-schatten:2,4,10;
--p-akzent-hover:#4f8df0; --p-tief-hover:#2c63cc; --p-aktiv:#0b1226; --p-konsole:#03050a;
--win:#0a0e1a; --area:#05070d; --bar:#0d1428; --surface:#0d1428; --surface2:#101a35; --input:#070b16;
--text:#e8ecfb; --dim:#93a0c4; --link:#8fd8ff;
}
:root[data-design="lila"]{
--p-akzent:139,92,246; --p-tief:109,40,217; --p-akzent2:192,132,252; --p-hell:232,190,255;
--p-linie:196,165,255; --p-win:15,10,26; --p-surface:24,15,41; --p-surface2:32,20,56; --p-schatten:6,3,12;
--p-akzent-hover:#9d72ff; --p-tief-hover:#7c3aed; --p-aktiv:#140c24; --p-konsole:#050309;
--win:#0f0a1a; --area:#08050f; --bar:#180f29; --surface:#180f29; --surface2:#201438; --input:#0b0715;
--text:#f0eafb; --dim:#a99cc6; --link:#e8beff;
}
:root:is([data-design="aero"],[data-design="lila"]){
--border:rgba(var(--p-linie),.14); --border2:rgba(var(--p-linie),.09);
--sel:rgba(var(--p-akzent),.20); --sel-br:var(--p-akzent-hover); --accent:rgb(var(--p-akzent));
--danger:#ff7a8a; --play:#4ade80;
--cyan:rgb(var(--p-hell)); --cyan-soft:rgb(var(--p-akzent2));
--stat-bar:rgb(var(--p-akzent2));
--titel-bg:var(--win); --titel-fg:var(--text);
}
:root:is([data-design="aero"],[data-design="lila"]) ::selection{background:rgb(var(--p-akzent));color:#fff}
:root:is([data-design="aero"],[data-design="lila"]) .titel-text{font-weight:600;background:linear-gradient(120deg,rgb(var(--p-hell)),rgb(var(--p-akzent)) 70%);-webkit-background-clip:text;background-clip:text;color:transparent}
:root:is([data-design="aero"],[data-design="lila"]) body{background:var(--area)}
/* Werkzeugleiste */
:root:is([data-design="aero"],[data-design="lila"]) .toolbar{background:rgba(var(--p-win),.94);border-bottom:1px solid var(--border);padding:7px 8px;gap:2px}
:root:is([data-design="aero"],[data-design="lila"]) .tbtn{border-radius:999px;padding:6px 10px;color:var(--text)}
:root:is([data-design="aero"],[data-design="lila"]) .tbtn:hover{background:var(--surface);border-color:var(--border)}
:root:is([data-design="aero"],[data-design="lila"]) .tbtn:active{background:var(--surface2)}
:root:is([data-design="aero"],[data-design="lila"]) .tbtn svg,
:root:is([data-design="aero"],[data-design="lila"]) #btn-new svg,:root:is([data-design="aero"],[data-design="lila"]) #btn-import svg,:root:is([data-design="aero"],[data-design="lila"]) #btn-open-dir svg,
:root:is([data-design="aero"],[data-design="lila"]) #btn-console svg,:root:is([data-design="aero"],[data-design="lila"]) #btn-account svg,:root:is([data-design="aero"],[data-design="lila"]) #btn-settings svg,
:root:is([data-design="aero"],[data-design="lila"]) .acct-av svg{color:var(--cyan-soft)}
:root:is([data-design="aero"],[data-design="lila"]) .tbsep{background:var(--border)}
:root:is([data-design="aero"],[data-design="lila"]) .search-wrap input{border-radius:999px;padding:5px 12px;width:150px}
/* Instanzliste: Raster und blaues Leuchten wie im Kopf der Webseite */
:root:is([data-design="aero"],[data-design="lila"]) .workspace{background:var(--area)}
:root:is([data-design="aero"],[data-design="lila"]) .list-area{background:
radial-gradient(520px 260px at 32% -60px,rgba(var(--p-akzent),.30),transparent 70%),
linear-gradient(to right,rgba(var(--p-linie),.05) 1px,transparent 1px) 0 0/44px 44px,
linear-gradient(to bottom,rgba(var(--p-linie),.05) 1px,transparent 1px) 0 0/44px 44px,
var(--area)}
:root:is([data-design="aero"],[data-design="lila"]) .group-header{color:var(--text);padding:8px 4px 4px}
:root:is([data-design="aero"],[data-design="lila"]) .group-header .chev{color:var(--cyan-soft)}
:root:is([data-design="aero"],[data-design="lila"]) .group-header::after{background:linear-gradient(to right,rgba(var(--p-linie),.22),rgba(var(--p-linie),.04))}
:root:is([data-design="aero"],[data-design="lila"]) .group-grid{gap:6px}
:root:is([data-design="aero"],[data-design="lila"]) .card{border-radius:14px;padding:8px 4px 6px;transition:background .15s,border-color .15s,box-shadow .15s}
:root:is([data-design="aero"],[data-design="lila"]) .card:hover{background:rgba(var(--p-surface),.85);border-color:var(--border)}
:root:is([data-design="aero"],[data-design="lila"]) .card.sel{background:linear-gradient(180deg,rgba(var(--p-akzent),.24),rgba(var(--p-tief),.14));border-color:rgba(var(--p-akzent2),.55);box-shadow:0 10px 28px -14px rgba(var(--p-akzent),.95),0 0 0 1px rgba(var(--p-hell),.08) inset}
:root:is([data-design="aero"],[data-design="lila"]) .card.sel .card-name{background:linear-gradient(120deg,rgb(var(--p-akzent2)),rgb(var(--p-akzent)) 60%,rgb(var(--p-tief)));color:#fff;border-radius:999px}
:root:is([data-design="aero"],[data-design="lila"]) .card-icon-img{border-radius:10px}
:root:is([data-design="aero"],[data-design="lila"]) .card-taste{border-radius:999px;background:linear-gradient(120deg,rgb(var(--p-akzent2)),rgb(var(--p-akzent)))}
:root:is([data-design="aero"],[data-design="lila"]) .empty h2{background:linear-gradient(120deg,rgb(var(--p-hell)),rgb(var(--p-akzent)) 60%,rgb(var(--p-tief)));-webkit-background-clip:text;background-clip:text;color:transparent;font-size:26px}
/* Seitenleiste */
:root:is([data-design="aero"],[data-design="lila"]) .side{background:var(--win);border-left:1px solid var(--border);padding:10px 6px}
:root:is([data-design="aero"],[data-design="lila"]) .side-ico img{border-radius:12px}
:root:is([data-design="aero"],[data-design="lila"]) .side-grp{color:var(--cyan-soft)}
:root:is([data-design="aero"],[data-design="lila"]) .hr{background:var(--border);margin:8px 4px}
:root:is([data-design="aero"],[data-design="lila"]) .sbtn{border-radius:999px;margin-bottom:1px;padding:5px 2px}
:root:is([data-design="aero"],[data-design="lila"]) .sbtn:hover{background:var(--surface);border-color:var(--border)}
:root:is([data-design="aero"],[data-design="lila"]) .sbtn.play{color:#fff;background:linear-gradient(180deg,rgb(var(--p-akzent)),rgb(var(--p-tief)));border-color:transparent;box-shadow:0 0 0 1px rgba(255,255,255,.08) inset,0 10px 24px -12px rgba(var(--p-akzent),.95);padding:7px 4px}
:root:is([data-design="aero"],[data-design="lila"]) .sbtn.play:hover{background:linear-gradient(180deg,var(--p-akzent-hover),var(--p-tief-hover))}
:root:is([data-design="aero"],[data-design="lila"]) .sbtn.del:hover{color:var(--danger);border-color:rgba(255,122,138,.35)}
/* Statusleiste */
:root:is([data-design="aero"],[data-design="lila"]) .statusbar{background:var(--win);border-top:1px solid var(--border)}
:root:is([data-design="aero"],[data-design="lila"]) .st-row{color:var(--dim)}
:root:is([data-design="aero"],[data-design="lila"]) .st-row2{background:var(--input);border-top-color:var(--border2)}
/* Dialoge */
:root:is([data-design="aero"],[data-design="lila"]) .modal{background:rgba(var(--p-schatten),.68);backdrop-filter:blur(3px)}
:root:is([data-design="aero"],[data-design="lila"]) .modal-card{background:var(--win);border:1px solid var(--border);border-radius:16px;box-shadow:0 30px 80px -20px rgba(0,0,0,.85),0 0 0 1px rgba(var(--p-hell),.04)}
:root:is([data-design="aero"],[data-design="lila"]) .modal-head{background:linear-gradient(180deg,rgba(var(--p-surface2),.9),rgba(var(--p-win),.9));border-bottom:1px solid var(--border);padding:12px 16px}
:root:is([data-design="aero"],[data-design="lila"]) .modal-body{background:var(--win)}
:root:is([data-design="aero"],[data-design="lila"]) .modal-foot{background:var(--win);border-top:1px solid var(--border);padding:12px 16px}
:root:is([data-design="aero"],[data-design="lila"]) .modal-close{color:var(--dim)}
:root:is([data-design="aero"],[data-design="lila"]) .modal-close:hover{color:var(--cyan)}
/* Eingabefelder */
:root:is([data-design="aero"],[data-design="lila"]) .search-wrap input,
:root:is([data-design="aero"],[data-design="lila"]) .field input,
:root:is([data-design="aero"],[data-design="lila"]) .field select,
:root:is([data-design="aero"],[data-design="lila"]) .field textarea,
:root:is([data-design="aero"],[data-design="lila"]) .mods-search input,
:root:is([data-design="aero"],[data-design="lila"]) .mods-search select,
:root:is([data-design="aero"],[data-design="lila"]) .mods-filters select,
:root:is([data-design="aero"],[data-design="lila"]) .mmc-bar select,
:root:is([data-design="aero"],[data-design="lila"]) .clip-length select,
:root:is([data-design="aero"],[data-design="lila"]) .path-row input,
:root:is([data-design="aero"],[data-design="lila"]) .se-farbzeile input[type=text],
:root:is([data-design="aero"],[data-design="lila"]) .se-feld select{background:var(--input);color:var(--text);border-color:var(--border);border-radius:10px}
:root:is([data-design="aero"],[data-design="lila"]) .search-wrap input{border-radius:999px}
:root:is([data-design="aero"],[data-design="lila"]) .search-wrap input:focus,
:root:is([data-design="aero"],[data-design="lila"]) .field input:focus,
:root:is([data-design="aero"],[data-design="lila"]) .field select:focus,
:root:is([data-design="aero"],[data-design="lila"]) .field textarea:focus,
:root:is([data-design="aero"],[data-design="lila"]) .mods-search input:focus,
:root:is([data-design="aero"],[data-design="lila"]) .path-row input:focus{border-color:var(--cyan-soft);box-shadow:0 0 0 3px rgba(var(--p-akzent2),.15)}
:root:is([data-design="aero"],[data-design="lila"]) input[type=checkbox],:root:is([data-design="aero"],[data-design="lila"]) input[type=radio],:root:is([data-design="aero"],[data-design="lila"]) input[type=range]{accent-color:rgb(var(--p-akzent))}
:root:is([data-design="aero"],[data-design="lila"]) .login-usercode{background:var(--input);color:var(--text);border-color:var(--border);border-radius:12px}
:root:is([data-design="aero"],[data-design="lila"]) .upd-notes{background:var(--input);border-radius:10px}
/* Knöpfe – rund wie auf der Webseite */
:root:is([data-design="aero"],[data-design="lila"]) .btn{border-radius:999px;padding:7px 15px;background:var(--surface);border-color:var(--border);color:var(--text);transition:background .15s,border-color .15s,transform .15s}
:root:is([data-design="aero"],[data-design="lila"]) .btn:hover{background:var(--surface2);border-color:rgba(var(--p-linie),.32)}
:root:is([data-design="aero"],[data-design="lila"]) .btn:active{background:var(--p-aktiv)}
:root:is([data-design="aero"],[data-design="lila"]) .btn-primary{background:linear-gradient(180deg,rgb(var(--p-akzent)),rgb(var(--p-tief)));border-color:transparent;color:#fff;box-shadow:0 0 0 1px rgba(255,255,255,.08) inset,0 10px 28px -12px rgba(var(--p-akzent),.9)}
:root:is([data-design="aero"],[data-design="lila"]) .btn-primary:hover{background:linear-gradient(180deg,var(--p-akzent-hover),var(--p-tief-hover));transform:translateY(-1px)}
:root:is([data-design="aero"],[data-design="lila"]) .btn-primary:disabled{transform:none}
:root:is([data-design="aero"],[data-design="lila"]) .btn-danger{color:var(--danger);border-color:rgba(255,122,138,.35)}
:root:is([data-design="aero"],[data-design="lila"]) .btn-danger:hover{background:rgba(255,122,138,.10);border-color:rgba(255,122,138,.6)}
:root:is([data-design="aero"],[data-design="lila"]) .btn-mini{padding:4px 10px}
:root:is([data-design="aero"],[data-design="lila"]) .badge{background:var(--surface2);color:var(--dim);border-color:var(--border);border-radius:999px}
/* Listen, Kacheln, Tabs */
:root:is([data-design="aero"],[data-design="lila"]) .mods-list,
:root:is([data-design="aero"],[data-design="lila"]) .account-item,
:root:is([data-design="aero"],[data-design="lila"]) .screen-thumb{background:var(--input);border-color:var(--border);border-radius:12px}
:root:is([data-design="aero"],[data-design="lila"]) .mod-icon,
:root:is([data-design="aero"],[data-design="lila"]) .account-av,
:root:is([data-design="aero"],[data-design="lila"]) .icon-preview{background:var(--surface2);border-radius:8px}
:root:is([data-design="aero"],[data-design="lila"]) .mod-item{border-radius:10px}
:root:is([data-design="aero"],[data-design="lila"]) .mod-item+.mod-item{border-color:var(--border2)}
:root:is([data-design="aero"],[data-design="lila"]) .mod-item:hover{background:var(--surface)}
:root:is([data-design="aero"],[data-design="lila"]) .mtab{border-radius:10px 10px 0 0}
:root:is([data-design="aero"],[data-design="lila"]) .mtab.on{background:var(--surface);border-color:var(--border);color:var(--cyan)}
:root:is([data-design="aero"],[data-design="lila"]) .advanced{background:var(--surface);border-color:var(--border);border-radius:12px}
:root:is([data-design="aero"],[data-design="lila"]) .console-out{background:var(--p-konsole);border-color:var(--border);border-radius:12px}
:root:is([data-design="aero"],[data-design="lila"]) .stat-tile,
:root:is([data-design="aero"],[data-design="lila"]) .crash-fund,
:root:is([data-design="aero"],[data-design="lila"]) .share-code-box,
:root:is([data-design="aero"],[data-design="lila"]) .share-preview,
:root:is([data-design="aero"],[data-design="lila"]) .clip-bar,
:root:is([data-design="aero"],[data-design="lila"]) .wahl-option{border-radius:12px;background:var(--surface)}
:root:is([data-design="aero"],[data-design="lila"]) .chat-verlauf{background:var(--input);border-radius:12px}
:root:is([data-design="aero"],[data-design="lila"]) .chat-blase{background:var(--surface2);border-radius:14px}
:root:is([data-design="aero"],[data-design="lila"]) .chat-blase.ich{background:linear-gradient(180deg,rgb(var(--p-akzent)),rgb(var(--p-tief)));border-color:transparent}
:root:is([data-design="aero"],[data-design="lila"]) .stat-bar{border-radius:0 6px 6px 0;background:linear-gradient(90deg,rgb(var(--p-akzent)),rgb(var(--p-akzent2)))}
/* Einstellungen */
:root:is([data-design="aero"],[data-design="lila"]) .settings-body{background:var(--area)}
:root:is([data-design="aero"],[data-design="lila"]) .settings-nav{background:var(--win);border-right-color:var(--border)}
:root:is([data-design="aero"],[data-design="lila"]) .snav{border-radius:10px}
:root:is([data-design="aero"],[data-design="lila"]) .snav:hover{background:var(--surface)}
:root:is([data-design="aero"],[data-design="lila"]) .snav.on{background:linear-gradient(120deg,rgba(var(--p-akzent),.30),rgba(var(--p-tief),.18));border-color:rgba(var(--p-akzent2),.40);box-shadow:none}
:root:is([data-design="aero"],[data-design="lila"]) .settings-card{background:var(--surface);border-color:var(--border);border-radius:16px}
:root:is([data-design="aero"],[data-design="lila"]) .settings-kv-row,
:root:is([data-design="aero"],[data-design="lila"]) .about-brand{border-bottom-color:var(--border2)}
:root:is([data-design="aero"],[data-design="lila"]) .appinfo-item{background:rgba(var(--p-linie),.04);border-color:var(--border2);border-radius:12px}
:root:is([data-design="aero"],[data-design="lila"]) .info-meta-link{color:var(--cyan)}
:root:is([data-design="aero"],[data-design="lila"]) .settings-hero h4{background:linear-gradient(120deg,rgb(var(--p-hell)),rgb(var(--p-akzent)) 70%);-webkit-background-clip:text;background-clip:text;color:transparent}
/* Kontextmenü, Hinweis, Bildlaufleisten */
:root:is([data-design="aero"],[data-design="lila"]) .ctx{background:var(--surface);border-color:var(--border);border-radius:12px;box-shadow:0 18px 40px -12px rgba(0,0,0,.8)}
:root:is([data-design="aero"],[data-design="lila"]) .ctx-item{border-radius:8px}
:root:is([data-design="aero"],[data-design="lila"]) .ctx-item:hover{background:rgba(var(--p-akzent),.22)}
:root:is([data-design="aero"],[data-design="lila"]) .toast{background:var(--surface2);border:1px solid var(--border);border-radius:999px;padding:10px 18px}
:root:is([data-design="aero"],[data-design="lila"]) ::-webkit-scrollbar-thumb{background:rgba(var(--p-linie),.18);border-color:var(--area)}
:root:is([data-design="aero"],[data-design="lila"]) ::-webkit-scrollbar-thumb:hover{background:rgba(var(--p-linie),.3)}
/* Skin-Editor */
:root:is([data-design="aero"],[data-design="lila"]) .se-modus{border-radius:999px;background:var(--surface)}
:root:is([data-design="aero"],[data-design="lila"]) .se-modus-btn.on{background:linear-gradient(180deg,rgb(var(--p-akzent)),rgb(var(--p-tief)))}
:root:is([data-design="aero"],[data-design="lila"]) .se-tool,:root:is([data-design="aero"],[data-design="lila"]) .se-ebene{background:var(--surface)}
:root:is([data-design="aero"],[data-design="lila"]) .se-tool{border-radius:10px}
:root:is([data-design="aero"],[data-design="lila"]) .se-ebenen{border-radius:10px}
:root:is([data-design="aero"],[data-design="lila"]) .se-buehne,:root:is([data-design="aero"],[data-design="lila"]) .se-mini{border-radius:14px;background:var(--input)}
:root:is([data-design="aero"],[data-design="lila"]) .design-wahl{border-radius:12px}
/* ===================================================================== */
/* ===================== Design: Minecraft ============================= */
/* ===================================================================== */
@font-face{font-family:"AeroPixel";src:url(assets/aeropixel.ttf) format("truetype");font-display:block}
:root[data-design="minecraft"]{
--win:#313233; --area:#1e1e1f; --bar:#48494a;
--border:#000; --border2:#1a1a1b;
--text:#ffffff; --dim:#b8b9bc;
--sel:#2f6b1f; --sel-br:#6cc349; --link:#86d75e; --accent:#3c8527;
--danger:#ff5555; --play:#6cc349; --input:#000;
--stat-bar:#6cc349;
--pixel:"AeroPixel","Segoe UI",sans-serif;
--mc-schatten:2px 2px 0 #3f3f3f;
}
:root[data-design="minecraft"] body{background:var(--area)}
:root[data-design="minecraft"] ::selection{background:#3c8527;color:#fff}
:root[data-design="minecraft"]{--titel-bg:#1e1e1f;--titel-fg:#ffffff}
:root[data-design="minecraft"] .titel-text{font-family:var(--pixel);font-size:16px;text-shadow:var(--mc-schatten);padding-bottom:2px}
/* Wiederkehrender Block-Knopf: grauer Stein mit hellem/dunklem Rand */
:root[data-design="minecraft"] .btn,
:root[data-design="minecraft"] .sbtn,
:root[data-design="minecraft"] .snav,
:root[data-design="minecraft"] .se-tool,
:root[data-design="minecraft"] .wahl-option{
background:#58595b;color:#fff;border:2px solid #000;border-radius:0;
box-shadow:inset 2px 2px 0 #8b8c8f,inset -2px -2px 0 #36373a;
text-shadow:1px 1px 0 #2a2a2a}
:root[data-design="minecraft"] .btn:hover,
:root[data-design="minecraft"] .sbtn:hover,
:root[data-design="minecraft"] .snav:hover,
:root[data-design="minecraft"] .se-tool:hover:not(:disabled),
:root[data-design="minecraft"] .wahl-option:hover{background:#6a6b6f;border-color:#fff;box-shadow:inset 2px 2px 0 #a3a4a8,inset -2px -2px 0 #3e3f42}
:root[data-design="minecraft"] .btn:active,
:root[data-design="minecraft"] .sbtn:active{background:#48494b;box-shadow:inset -2px -2px 0 #8b8c8f,inset 2px 2px 0 #36373a}
:root[data-design="minecraft"] .btn:disabled,
:root[data-design="minecraft"] .sbtn:disabled{background:#2b2b2c;color:#8b8b8b;box-shadow:none;border-color:#000;text-shadow:none;opacity:1}
:root[data-design="minecraft"] .sbtn:disabled:hover{background:#2b2b2c;border-color:#000}
/* grün wie der Spielen-Knopf */
:root[data-design="minecraft"] .btn-primary,
:root[data-design="minecraft"] .sbtn.play,
:root[data-design="minecraft"] .snav.on,
:root[data-design="minecraft"] .se-tool.on,
:root[data-design="minecraft"] .wahl-option.an{background:#3c8527;color:#fff;box-shadow:inset 2px 2px 0 #6cc349,inset -2px -2px 0 #1d4d13}
:root[data-design="minecraft"] .btn-primary:hover,
:root[data-design="minecraft"] .sbtn.play:hover{background:#4a9a33;box-shadow:inset 2px 2px 0 #82d65c,inset -2px -2px 0 #24601a;border-color:#fff}
:root[data-design="minecraft"] .btn-danger{background:#8c2b2b;box-shadow:inset 2px 2px 0 #c55050,inset -2px -2px 0 #561616}
:root[data-design="minecraft"] .btn-danger:hover{background:#a33434;border-color:#fff;box-shadow:inset 2px 2px 0 #d86262,inset -2px -2px 0 #651b1b}
:root[data-design="minecraft"] .sbtn.del:hover{background:#8c2b2b;color:#fff;box-shadow:inset 2px 2px 0 #c55050,inset -2px -2px 0 #561616}
/* Werkzeugleiste: Schiefer mit Grasnarbe */
:root[data-design="minecraft"] .toolbar{
background:url(assets/design/gras.png) 0 0/32px 8px repeat-x,url(assets/design/schiefer.png) 0 0/64px 64px;
image-rendering:pixelated;border-bottom:2px solid #000;padding:11px 8px 6px}
:root[data-design="minecraft"] .tbtn{color:#fff;border-radius:0;text-shadow:1px 1px 0 #1a1a1a;image-rendering:auto}
:root[data-design="minecraft"] .tbtn:hover{background:rgba(0,0,0,.35);border-color:#fff}
:root[data-design="minecraft"] .tbtn:active{background:rgba(0,0,0,.5)}
:root[data-design="minecraft"] .tbtn svg,:root[data-design="minecraft"] #btn-console svg,:root[data-design="minecraft"] .acct-av svg{color:#e6e6e6}
:root[data-design="minecraft"] #btn-new svg,:root[data-design="minecraft"] #btn-account svg{color:#7fd858}
:root[data-design="minecraft"] #btn-import svg{color:#6db8ff}
:root[data-design="minecraft"] #btn-open-dir svg{color:#f2c94c}
:root[data-design="minecraft"] #btn-settings svg{color:#d0d0d0}
:root[data-design="minecraft"] .tbsep{background:#000;width:2px}
:root[data-design="minecraft"] .tb-badge{border-radius:0;background:#ff5555;box-shadow:inset -1px -1px 0 #a32a2a}
/* Eingabefelder: schwarz mit grauem Rand wie im Spiel */
:root[data-design="minecraft"] .search-wrap input,
:root[data-design="minecraft"] .field input,
:root[data-design="minecraft"] .field select,
:root[data-design="minecraft"] .field textarea,
:root[data-design="minecraft"] .mods-search input,
:root[data-design="minecraft"] .mods-search select,
:root[data-design="minecraft"] .mods-filters select,
:root[data-design="minecraft"] .mmc-bar select,
:root[data-design="minecraft"] .clip-length select,
:root[data-design="minecraft"] .path-row input,
:root[data-design="minecraft"] .login-usercode,
:root[data-design="minecraft"] .upd-notes,
:root[data-design="minecraft"] .se-farbzeile input[type=text],
:root[data-design="minecraft"] .se-feld select{background:#000;color:#fff;border:1px solid #a0a0a0;border-radius:0}
:root[data-design="minecraft"] .search-wrap input:focus,
:root[data-design="minecraft"] .field input:focus,
:root[data-design="minecraft"] .field select:focus,
:root[data-design="minecraft"] .field textarea:focus,
:root[data-design="minecraft"] .mods-search input:focus,
:root[data-design="minecraft"] .path-row input:focus{border-color:#fff;box-shadow:none}
:root[data-design="minecraft"] input[type=checkbox],:root[data-design="minecraft"] input[type=radio],:root[data-design="minecraft"] input[type=range]{accent-color:#3c8527}
/* Instanzliste auf dunkler Erde */
:root[data-design="minecraft"] .workspace{background:var(--area)}
:root[data-design="minecraft"] .list-area{background:url(assets/design/erde-dunkel.png) 0 0/64px 64px;image-rendering:pixelated}
:root[data-design="minecraft"] .list-area::after,
:root[data-design="minecraft"] .list{image-rendering:auto}
:root[data-design="minecraft"] .group-header{font-family:var(--pixel);font-size:16px;font-weight:400;color:#fff;text-shadow:var(--mc-schatten);padding:8px 4px 4px}
:root[data-design="minecraft"] .group-count{color:#b8b9bc}
:root[data-design="minecraft"] .group-header .chev{color:#fff;font-family:"Segoe UI",sans-serif;font-size:10px}
:root[data-design="minecraft"] .group-header::after{height:2px;background:rgba(0,0,0,.55);box-shadow:0 1px 0 rgba(255,255,255,.08)}
:root[data-design="minecraft"] .card{border-radius:0}
:root[data-design="minecraft"] .card-name{color:#fff;text-shadow:1px 1px 0 #1a1a1a}
:root[data-design="minecraft"] .card:hover{background:rgba(0,0,0,.35);border-color:rgba(255,255,255,.35)}
:root[data-design="minecraft"] .card.sel{background:rgba(0,0,0,.6);border-color:#fff;outline:1px solid #fff;outline-offset:-2px}
:root[data-design="minecraft"] .card.sel .card-name{background:none;color:#ffff7f;border-radius:0}
:root[data-design="minecraft"] .card-taste{border-radius:0;background:#3c8527;box-shadow:inset -1px -1px 0 #1d4d13}
:root[data-design="minecraft"] .card-icon-img{border-radius:0}
:root[data-design="minecraft"] .empty h2{font-family:var(--pixel);font-weight:400;font-size:24px;color:#fff;text-shadow:3px 3px 0 #3f3f3f}
:root[data-design="minecraft"] .empty p{color:#d0d0d0}
/* Seitenleiste: Knöpfe gestapelt wie im Pausenmenü */
:root[data-design="minecraft"] .side{background:url(assets/design/schiefer.png) 0 0/64px 64px;image-rendering:pixelated;border-left:2px solid #000;padding:10px 6px}
:root[data-design="minecraft"] .side>*{image-rendering:auto}
:root[data-design="minecraft"] .side-title{font-family:var(--pixel);font-size:16px;font-weight:400;text-shadow:var(--mc-schatten)}
:root[data-design="minecraft"] .side-grp{color:#86d75e}
:root[data-design="minecraft"] .side-ico img{border-radius:0}
:root[data-design="minecraft"] .side-empty{color:#d0d0d0}
:root[data-design="minecraft"] .hr{background:#000;height:2px;margin:6px 0}
:root[data-design="minecraft"] .sbtn{margin-bottom:3px;padding:5px 2px}
:root[data-design="minecraft"] .sbtn.play{font-family:var(--pixel);font-size:16px;font-weight:400;padding:6px 4px 7px;text-shadow:var(--mc-schatten)}
/* Statusleiste */
:root[data-design="minecraft"] .statusbar{background:url(assets/design/schiefer.png) 0 0/64px 64px;image-rendering:pixelated;border-top:2px solid #000}
:root[data-design="minecraft"] .st-row{color:#e6e6e6;text-shadow:1px 1px 0 #1a1a1a}
:root[data-design="minecraft"] .st-row2{background:rgba(0,0,0,.4);border-top:2px solid #000}
/* Dialoge: dunkles Steinfenster mit Holzbalken als Kopf */
:root[data-design="minecraft"] .modal{background:rgba(0,0,0,.62)}
:root[data-design="minecraft"] .modal-card{background:#313233;border:2px solid #000;border-radius:0;box-shadow:inset 2px 2px 0 #5a5b5d,inset -2px -2px 0 #1e1e1f,0 10px 30px rgba(0,0,0,.6)}
:root[data-design="minecraft"] .modal-head{background:url(assets/design/bretter.png) 0 0/64px 64px;image-rendering:pixelated;border-bottom:2px solid #000;margin:2px 2px 0}
:root[data-design="minecraft"] .modal-head h3{font-family:var(--pixel);font-size:16px;font-weight:400;color:#fff;text-shadow:var(--mc-schatten)}
:root[data-design="minecraft"] .modal-head>*{image-rendering:auto}
:root[data-design="minecraft"] .modal-body{background:#313233;margin:0 2px}
:root[data-design="minecraft"] .modal-foot{background:#262627;border-top:2px solid #000;margin:0 2px 2px}
:root[data-design="minecraft"] .modal-close{color:#fff;text-shadow:1px 1px 0 #3f3f3f;font-size:16px}
:root[data-design="minecraft"] .modal-close:hover{color:#ffff7f}
:root[data-design="minecraft"] .popout-host .modal-card{box-shadow:inset 2px 2px 0 #5a5b5d,inset -2px -2px 0 #1e1e1f}
/* Listen, Kacheln, Tabs */
:root[data-design="minecraft"] .mods-list,
:root[data-design="minecraft"] .account-item,
:root[data-design="minecraft"] .screen-thumb,
:root[data-design="minecraft"] .chat-verlauf,
:root[data-design="minecraft"] .console-out{background:#000;border:2px solid #000;border-radius:0;box-shadow:inset 1px 1px 0 #3a3a3c}
:root[data-design="minecraft"] .account-item{background:#1e1e1f}
:root[data-design="minecraft"] .account-item.active{border-color:#6cc349}
:root[data-design="minecraft"] .mod-icon,
:root[data-design="minecraft"] .account-av,
:root[data-design="minecraft"] .icon-preview{background:#48494a;border-radius:0}
:root[data-design="minecraft"] .mod-item{border-radius:0}
:root[data-design="minecraft"] .mod-item+.mod-item{border-color:#2a2a2b}
:root[data-design="minecraft"] .mod-item:hover{background:#262627;outline:1px solid #8b8b8b;outline-offset:-1px}
:root[data-design="minecraft"] .mods-tabs{border-bottom:2px solid #000}
:root[data-design="minecraft"] .mtab{border-radius:0;color:#c8c8c8;margin-bottom:-2px;border-width:2px}
:root[data-design="minecraft"] .mtab.on{background:#48494a;border-color:#000;color:#fff;text-shadow:1px 1px 0 #2a2a2a}
:root[data-design="minecraft"] .advanced,
:root[data-design="minecraft"] .stat-tile,
:root[data-design="minecraft"] .crash-fund,
:root[data-design="minecraft"] .share-code-box,
:root[data-design="minecraft"] .share-preview,
:root[data-design="minecraft"] .clip-bar,
:root[data-design="minecraft"] .crash-unklar{background:#262627;border:2px solid #000;border-radius:0;box-shadow:inset 1px 1px 0 #48494a}
:root[data-design="minecraft"] .crash-fund{border-left:4px solid #3c8527}
:root[data-design="minecraft"] .badge{border-radius:0;background:#48494a;color:#e6e6e6;border-color:#000}
:root[data-design="minecraft"] .chat-blase{border-radius:0;background:#48494a;border:2px solid #000}
:root[data-design="minecraft"] .chat-blase.ich{background:#3c8527;border-color:#000}
:root[data-design="minecraft"] .stat-bar{border-radius:0;box-shadow:inset 0 -2px 0 #3c8527}
:root[data-design="minecraft"] .mod-new{border-radius:0}
:root[data-design="minecraft"] .friend-avatar{border-radius:0}
/* Einstellungen */
:root[data-design="minecraft"] .settings-body{background:url(assets/design/erde-dunkel.png) 0 0/64px 64px;image-rendering:pixelated}
:root[data-design="minecraft"] .settings-body>*{image-rendering:auto}
:root[data-design="minecraft"] .settings-nav{background:rgba(0,0,0,.45);border-right:2px solid #000;gap:4px}
:root[data-design="minecraft"] .settings-nav-label{color:#d0d0d0;text-shadow:1px 1px 0 #1a1a1a}
:root[data-design="minecraft"] .snav{padding:8px 12px}
:root[data-design="minecraft"] .settings-hero h4{font-family:var(--pixel);font-size:24px;font-weight:400;text-shadow:3px 3px 0 #3f3f3f}
:root[data-design="minecraft"] .settings-hero p{color:#d0d0d0}
:root[data-design="minecraft"] .settings-card{background:#313233;border:2px solid #000;border-radius:0;box-shadow:inset 2px 2px 0 #5a5b5d,inset -2px -2px 0 #1e1e1f}
:root[data-design="minecraft"] .settings-card h5{font-family:var(--pixel);font-size:16px;font-weight:400;text-shadow:var(--mc-schatten)}
:root[data-design="minecraft"] .settings-kv-row,
:root[data-design="minecraft"] .about-brand{border-bottom-color:#1a1a1b}
:root[data-design="minecraft"] .appinfo-item{background:#262627;border:2px solid #000;border-radius:0}
:root[data-design="minecraft"] .info-meta-link{color:#86d75e}
:root[data-design="minecraft"] .spane h4{font-family:var(--pixel);font-size:16px;font-weight:400;text-shadow:var(--mc-schatten)}
/* Kontextmenü, Hinweis, Bildlaufleisten */
:root[data-design="minecraft"] .ctx{background:#1e1e1f;border:2px solid #000;border-radius:0;box-shadow:inset 1px 1px 0 #48494a,0 8px 24px rgba(0,0,0,.5)}
:root[data-design="minecraft"] .ctx-item{border-radius:0;text-shadow:1px 1px 0 #000}
:root[data-design="minecraft"] .ctx-item:hover{background:#3c8527}
:root[data-design="minecraft"] .ctx-danger:hover{background:#8c2b2b}
:root[data-design="minecraft"] .ctx-sep{background:#000;height:2px}
:root[data-design="minecraft"] .toast{background:#212122;border:2px solid #000;border-radius:0;box-shadow:inset 2px 2px 0 #4a4a4c,inset -2px -2px 0 #111,0 6px 20px rgba(0,0,0,.5);text-shadow:1px 1px 0 #000}
:root[data-design="minecraft"] .toast.err{background:#5c1a1a}
:root[data-design="minecraft"] ::-webkit-scrollbar-thumb{background:#8b8b8b;border:3px solid var(--area);border-radius:0;box-shadow:inset -2px -2px 0 #555}
:root[data-design="minecraft"] ::-webkit-scrollbar-thumb:hover{background:#a8a8a8}
/* Skin-Editor */
:root[data-design="minecraft"] .se-modus{border:2px solid #000;border-radius:0;background:#2b2b2c}
:root[data-design="minecraft"] .se-modus-btn{color:#d0d0d0;text-shadow:1px 1px 0 #000}
:root[data-design="minecraft"] .se-modus-btn+.se-modus-btn{border-left:2px solid #000}
:root[data-design="minecraft"] .se-modus-btn.on{background:#3c8527;color:#fff;box-shadow:inset 2px 2px 0 #6cc349,inset -2px -2px 0 #1d4d13}
:root[data-design="minecraft"] .se-ebenen{border:2px solid #000;border-radius:0}
:root[data-design="minecraft"] .se-ebene{background:#2b2b2c;color:#d0d0d0}
:root[data-design="minecraft"] .se-ebene+.se-ebene{border-left:2px solid #000}
:root[data-design="minecraft"] .se-ebene.on{background:#3c8527;color:#fff}
:root[data-design="minecraft"] .se-buehne,:root[data-design="minecraft"] .se-mini{border:2px solid #000;border-radius:0;background:#1e1e1f}
:root[data-design="minecraft"] .se-farbfeld{border-radius:0;border-color:#000}
:root[data-design="minecraft"] .se-titel{color:#d0d0d0}
:root[data-design="minecraft"] .design-wahl{border-radius:0;border:2px solid #000;background:#262627}
:root[data-design="minecraft"] .design-wahl.on{border-color:#fff;box-shadow:none;background:#2f6b1f}
:root[data-design="minecraft"] .dv{border-radius:0}
+157 -8
View File
@@ -6,8 +6,15 @@
content="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'; img-src 'self' data: https:; connect-src 'self';" />
<title>AeroMC</title>
<link rel="stylesheet" href="styles.css" />
<link rel="stylesheet" href="designs.css" />
</head>
<body>
<!-- Eigene Titelleiste (Fensterknöpfe zeichnet Windows rechts darüber) -->
<div class="titelleiste">
<img src="assets/logo.png" alt="" class="titel-logo" />
<span class="titel-text">AeroMC</span>
</div>
<!-- Obere Werkzeugleiste (MultiMC-Stil: Icon + Text) -->
<header class="toolbar">
<button id="btn-new" class="tbtn" data-ic="plus" title="Neue Instanz (Strg+N)">Instanz hinzufügen</button>
@@ -250,14 +257,7 @@
</div>
<div class="settings-grid">
<section class="settings-card">
<h5>Oberfläche</h5>
<label class="field">
<span>Design</span>
<select id="s-theme">
<option value="dark">Dunkel</option>
<option value="light">Hell</option>
</select>
</label>
<h5>Instanzliste</h5>
<label class="field">
<span>Instanzen sortieren</span>
<select id="s-sortby">
@@ -323,6 +323,37 @@
<p>Optik der Oberfläche und visuelle Extras für die Instanzansicht.</p>
</div>
<div class="settings-grid">
<section class="settings-card settings-card-wide">
<h5>Design</h5>
<div class="field">
<select id="s-theme" class="hidden">
<option value="dark">Klassisch – Dunkel</option>
<option value="light">Klassisch – Hell</option>
<option value="aero">AeroMC</option>
<option value="lila">Nachtlila</option>
<option value="minecraft">Minecraft</option>
</select>
<div class="design-raster">
<button type="button" class="design-wahl" data-design="dark" title="Der bewährte Launcher-Look in Dunkelgrau">
<span class="dv dv-dark"><i></i><b></b><em></em></span><small>Klassisch Dunkel</small>
</button>
<button type="button" class="design-wahl" data-design="light" title="Der bewährte Launcher-Look in Hell">
<span class="dv dv-light"><i></i><b></b><em></em></span><small>Klassisch Hell</small>
</button>
<button type="button" class="design-wahl" data-design="aero" title="Nachtblau mit Cyan-Akzenten – wie die AeroMC-Webseite">
<span class="dv dv-aero"><i></i><b></b><em></em></span><small>AeroMC</small>
</button>
<button type="button" class="design-wahl" data-design="lila" title="Wie AeroMC, nur in dunklem Lila">
<span class="dv dv-lila"><i></i><b></b><em></em></span><small>Nachtlila</small>
</button>
<button type="button" class="design-wahl" data-design="minecraft" title="Block-Optik wie im Spiel">
<span class="dv dv-mc"><i></i><b></b><em></em></span><small>Minecraft</small>
</button>
</div>
</div>
<small class="hint">Ein Klick zeigt das Design sofort an – mit „Speichern“ gilt es in allen Fenstern.</small>
</section>
<section class="settings-card">
<h5>Icons</h5>
<label class="checkbox-field">
@@ -350,6 +381,14 @@
<option value="spooky">Unheimlich</option>
</select>
</label>
<label class="field">
<span>Armform</span>
<select id="s-bgskin-model" title="Bei „Automatisch“ wird die Armform übernommen, die beim Konto eingestellt ist">
<option value="auto">Automatisch (vom Konto)</option>
<option value="classic">Normal (breite Arme, Steve)</option>
<option value="slim">Schmal (Alex)</option>
</select>
</label>
<label class="field">
<span>Deckkraft: <span id="s-bgskin-val">55</span> %</span>
<input id="s-bgskin-opacity" type="range" min="10" max="100" step="5" />
@@ -1213,6 +1252,7 @@
</div>
<div class="modal-foot">
<button id="skin-reset" class="btn btn-danger">Auf Standard zurücksetzen</button>
<button id="skin-editor" class="btn" title="Skin selbst pixeln – mit Grundschicht und 2. Schicht">Im Editor bearbeiten…</button>
<div class="spacer"></div>
<button class="btn" data-close>Schließen</button>
<button id="skin-apply" class="btn btn-primary" disabled>Skin setzen</button>
@@ -1220,6 +1260,86 @@
</div>
</div>
<!-- ===================== Dialog: Skin-Editor ===================== -->
<div id="modal-skineditor" class="modal hidden">
<div class="modal-card se-karte">
<div class="modal-head">
<h3>Skin-Editor</h3>
<div class="se-modus">
<button class="se-modus-btn on" data-se-modus="2d" title="Auf der aufgeklappten Vorlage malen">2D-Vorlage</button>
<button class="se-modus-btn" data-se-modus="3d" title="Direkt auf der drehbaren Figur malen">3D-Figur</button>
</div>
<button class="modal-close" data-close>✕</button>
</div>
<div class="modal-body se-body">
<aside class="se-werkzeuge">
<div class="se-gruppe">
<div class="se-titel">Werkzeug</div>
<div class="se-tools">
<button class="se-tool on" data-se-tool="stift" title="Stift (B)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20h4L18 10l-4-4L4 16v4zM14 6l4 4"/></svg><span>Stift</span></button>
<button class="se-tool" data-se-tool="radierer" title="Radierer (E)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 20h12M5.5 14.5l8-8a2 2 0 012.8 0l2.2 2.2a2 2 0 010 2.8L11 19H8.5l-3-3a1.1 1.1 0 010-1.5zM9 11l5 5"/></svg><span>Radierer</span></button>
<button class="se-tool" data-se-tool="pipette" title="Pipette (I) – auch Alt+Klick"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 7l3 3M5 19l1-4 8.5-8.5a2.1 2.1 0 013 3L9 18l-4 1zM15.5 5.5l1.5-1.5a2.1 2.1 0 013 3l-1.5 1.5"/></svg><span>Pipette</span></button>
<button class="se-tool" data-se-tool="fuellen" title="Füllen (G) – färbt gleichfarbige Pixel einer Seite"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 11l7-7 7 7-7 7zM5 11h14M20 16s1.5 2 1.5 3a1.5 1.5 0 01-3 0c0-1 1.5-3 1.5-3z"/></svg><span>Füllen</span></button>
</div>
</div>
<div class="se-gruppe">
<div class="se-titel">Farbe</div>
<div class="se-farbzeile">
<input id="se-farbe" type="color" value="#3a6ea5" title="Farbe wählen" />
<input id="se-hex" type="text" maxlength="7" spellcheck="false" value="#3a6ea5" />
</div>
<div id="se-zuletzt" class="se-zuletzt"></div>
</div>
<div class="se-gruppe">
<div class="se-titel">Ebene</div>
<div class="se-ebenen">
<button class="se-ebene on" data-se-schicht="0" title="Die eigentliche Figur (L wechselt)">Grundschicht</button>
<button class="se-ebene" data-se-schicht="1" title="Hülle über der Figur – Haare, Jacke, Ärmel … (L wechselt)">2. Schicht</button>
</div>
<label class="se-check"><input id="se-aussen" type="checkbox" checked /> 2. Schicht zeigen</label>
<label class="se-check"><input id="se-durch" type="checkbox" checked /> Grundschicht durchscheinen</label>
<button id="se-leeren" class="btn btn-mini">2. Schicht leeren</button>
</div>
<div class="se-gruppe">
<div class="se-titel">Optionen</div>
<label class="se-check" title="Malt gleichzeitig auf der anderen Körperseite (M)"><input id="se-spiegeln" type="checkbox" /> Spiegeln (links ↔ rechts)</label>
<label class="se-feld"><span>Armform</span>
<select id="se-variante">
<option value="classic">Klassisch (Steve)</option>
<option value="slim">Schmal (Alex)</option>
</select>
</label>
<div class="se-undo">
<button id="se-undo" class="btn btn-mini" title="Rückgängig (Strg+Z)" disabled>↶ Rückgängig</button>
<button id="se-redo" class="btn btn-mini" title="Wiederholen (Strg+Y)" disabled>↷ Wiederholen</button>
</div>
</div>
</aside>
<section class="se-mitte">
<div id="se-buehne" class="se-buehne">
<div id="se-2d" class="se-ansicht se-2d"><canvas id="se-2d-canvas"></canvas></div>
</div>
<div id="se-info" class="se-info hint"></div>
</section>
<aside class="se-seite">
<div id="se-mini-titel" class="se-titel">3D-Vorschau</div>
<div id="se-mini" class="se-mini">
<div id="se-3d" class="se-ansicht se-3d"><canvas id="se-3d-canvas"></canvas><span id="se-3d-leer" class="hint se-3d-leer">wird geladen …</span></div>
</div>
<p class="hint se-tipp">Beide Ansichten sind bemalbar. Die <b>2. Schicht</b> liegt wie eine Hülle über der Figur – lässt man Pixel darin frei, wirken Haare, Kapuzen oder Ärmel plastisch.</p>
</aside>
</div>
<div class="modal-foot se-fuss">
<button id="se-neu" class="btn" title="Mit einer leeren Vorlage beginnen">Neu</button>
<button id="se-laden" class="btn" title="Den Skin deines angemeldeten Kontos laden">Aktuellen Skin laden</button>
<button id="se-oeffnen" class="btn">PNG öffnen…</button>
<button id="se-speichern" class="btn">Als PNG speichern…</button>
<span id="se-status" class="hint se-status"></span>
<button id="se-uebernehmen" class="btn btn-primary">Als Skin übernehmen</button>
</div>
</div>
</div>
<!-- ===================== Dialog: Logdateien ===================== -->
<div id="modal-logs" class="modal hidden">
<div class="modal-card modal-lg">
@@ -1364,6 +1484,7 @@
<button id="import-tab-modpack" class="mtab" data-kind="modpack">Modpack</button>
<button id="import-tab-mc" class="mtab" data-kind="mc">Minecraft Launcher</button>
<button id="import-tab-cf" class="mtab" data-kind="cf">CurseForge</button>
<button id="import-tab-mr" class="mtab" data-kind="mr">Modrinth App</button>
<button id="import-tab-mmc" class="mtab" data-kind="mmc">MultiMC / Prism</button>
<button id="import-tab-technic" class="mtab" data-kind="technic">Technic</button>
<button id="import-tab-code" class="mtab" data-kind="code">Per Code</button>
@@ -1460,6 +1581,31 @@
</div>
</div>
<div id="import-pane-mr" class="import-pane hidden">
<label class="field">
<span>Datenordner der Modrinth App</span>
<div class="path-row">
<input id="mr-path" type="text" placeholder="leer lassen = Standardort (%APPDATA%\ModrinthApp)" />
<button id="mr-pick" class="btn">Wählen…</button>
<button id="mr-scan" class="btn btn-primary">Suchen</button>
</div>
<small id="mr-status" class="hint">Leer lassen nutzt automatisch den Standardort der Modrinth App.</small>
</label>
<div id="mr-result" class="hidden">
<div class="mmc-bar">
<label class="checkbox-field" style="margin:0">
<input id="mr-all" type="checkbox" checked /> <span>Alle auswählen</span>
</label>
<label class="checkbox-field" style="margin:0">
<input id="mr-copy" type="checkbox" checked /> <span>Spieldaten mitkopieren (Welten, Mods, Konfiguration)</span>
</label>
</div>
<div id="mr-list" class="mods-list"></div>
<p class="hint" style="margin-top:8px">Übernommen werden Name, Gruppe, Symbol, Minecraft-Version, Loader, Arbeitsspeicher, Java-Einstellungen und die Spielzeit. Modrinth-Modpacks bleiben mit ihrem Paket verknüpft und lassen sich in AeroMC weiter aktualisieren.</p>
</div>
</div>
<div id="import-pane-mmc" class="import-pane hidden">
<label class="field">
<span>MultiMC- oder Prism-Ordner</span>
@@ -1540,6 +1686,7 @@
<div id="mc-progress" class="hint hidden"></div>
<div id="mmc-progress" class="hint hidden"></div>
<div id="cf-progress" class="hint hidden"></div>
<div id="mr-progress" class="hint hidden"></div>
<div id="technic-progress" class="hint hidden"></div>
<div id="code-progress" class="hint hidden"></div>
<div class="spacer"></div>
@@ -1548,6 +1695,7 @@
<button id="import-modpack-do" class="btn btn-primary hidden">Modpack wählen…</button>
<button id="mc-do" class="btn btn-primary hidden" disabled>Übernehmen</button>
<button id="cf-do" class="btn btn-primary hidden" disabled>Übernehmen</button>
<button id="mr-do" class="btn btn-primary hidden" disabled>Übernehmen</button>
<button id="mmc-do" class="btn btn-primary hidden" disabled>Übernehmen</button>
<button id="technic-do" class="btn btn-primary hidden" disabled>Übernehmen</button>
<button id="code-do" class="btn btn-primary hidden" disabled>Instanz anlegen</button>
@@ -1656,5 +1804,6 @@
<canvas id="skin-canvas" width="420" height="700" style="display:none"></canvas>
<script src="vendor/skinview3d.js"></script>
<script src="renderer.js"></script>
<script src="skineditor.js"></script>
</body>
</html>
+211 -8
View File
@@ -29,6 +29,7 @@ const bedrock = require('./bedrock');
const backup = require('./backup');
const mmcimport = require('./mmcimport');
const cfimport = require('./cfimport');
const mrimport = require('./mrimport');
const mcimport = require('./mcimport');
const technicimport = require('./technicimport');
const technicdiscover = require('./technicdiscover');
@@ -112,6 +113,7 @@ const POPOUT_SPEC = {
screens: { title: 'AeroMC – Bildschirmfotos', 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 },
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 },
login: { title: 'AeroMC – Konten', width: 460, height: 460, minWidth: 420, minHeight: 380 },
import: { title: 'AeroMC – Import', width: 860, height: 680, minWidth: 640, minHeight: 480 },
@@ -171,7 +173,7 @@ function openPopout(modalId, params) {
minWidth: spec.minWidth,
minHeight: spec.minHeight,
title: spec.title || POPOUT_DEFAULT.title,
backgroundColor: '#1b1e24',
backgroundColor: fensterFarbe(),
icon: windowIcon(),
frame: false, // eigener dunkler Kopfbereich (Titel + ✕) ersetzt die native Titelleiste
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('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) => {
const w = BrowserWindow.fromWebContents(e.sender);
if (w && !w.isDestroyed()) w.hide();
@@ -714,6 +722,28 @@ function verknuepfungsIcon(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() {
const png = path.join(__dirname, 'assets', 'logo.png');
const ico = path.join(__dirname, 'assets', 'icon.ico');
@@ -728,10 +758,12 @@ function createWindow(startHidden) {
height: 820,
minWidth: 940,
minHeight: 640,
backgroundColor: '#1b1e24',
backgroundColor: fensterFarbe(),
title: 'AeroMC',
icon: windowIcon(),
autoHideMenuBar: true,
titleBarStyle: 'hidden',
titleBarOverlay: titelleistenFarben((store.getSettings() || {}).theme),
show: !startHidden,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
@@ -800,7 +832,7 @@ function oeffneVorbereitetenImport() {
try {
const daten = JSON.parse(fs.readFileSync(datei, 'utf8').replace(/^/, ''));
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 : '' }));
} catch { /* beschädigt -> verwerfen */ }
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
// Hauptfenster bei jedem Klick seine ganze Liste neu auf und die flackert.
const nurLetzteInstanz = patch && Object.keys(patch).every((k) => k === 'lastSelectedId');
if (!nurLetzteInstanz && mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('settings:changed', s);
}
// an alle Fenster: das Design muss auch in offenen Popup-Fenstern wechseln
if (!nurLetzteInstanz) broadcast('settings:changed', s);
return s;
});
@@ -1733,6 +1764,30 @@ async function aktivesToken() {
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 () => {
const token = await aktivesToken();
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.' };
try {
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;
}
catch (err) { return { ok: false, message: 'Skin konnte nicht gesetzt werden: ' + err.message }; }
@@ -1775,11 +1830,78 @@ ipcMain.handle('skin:reset', async () => {
if (!token) return { ok: false, message: 'Kein angemeldetes Konto.' };
try {
const res = await skins.setzeSkinZurueck(token);
if (res && res.ok) broadcast('skin:changed');
if (res && res.ok) { armformCache.clear(); broadcast('skin:changed'); }
return res;
}
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 };
}
});
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:logout', () => { auth.clearAccount(); return true; });
@@ -3732,6 +3854,87 @@ ipcMain.handle('cf:import', (_e, entries, copyData) => {
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) => {
const res = await dialog.showOpenDialog(BrowserWindow.fromWebContents(e.sender) || mainWindow, {
title: 'Minecraft-Launcher-Ordner wählen (meist .minecraft)',
+437
View File
@@ -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 };
+15 -1
View File
@@ -154,6 +154,11 @@ contextBridge.exposeInMainWorld('api', {
skinChoose: () => ipcRenderer.invoke('skin:choose'),
skinApply: (pfad, variante) => ipcRenderer.invoke('skin:apply', pfad, variante),
skinReset: () => ipcRenderer.invoke('skin:reset'),
skinCurrentData: () => ipcRenderer.invoke('skin:currentData'),
skinModelFor: (uuid) => ipcRenderer.invoke('skin:modelFor', uuid),
skinReadData: (pfad) => ipcRenderer.invoke('skin:readData', pfad),
skinSaveData: (base64) => ipcRenderer.invoke('skin:saveData', base64),
skinApplyData: (base64, variante) => ipcRenderer.invoke('skin:applyData', base64, variante),
authRemove: (id) => ipcRenderer.invoke('auth:remove', id),
authLogin: () => ipcRenderer.invoke('auth:login'),
authLogout: () => ipcRenderer.invoke('auth:logout'),
@@ -192,6 +197,14 @@ contextBridge.exposeInMainWorld('api', {
return () => ipcRenderer.removeListener('mmc:progress', l);
},
cfPickFolder: () => ipcRenderer.invoke('cf:pickFolder'),
mrPickFolder: () => ipcRenderer.invoke('mr:pickFolder'),
mrScan: (folder) => ipcRenderer.invoke('mr:scan', folder),
mrImport: (entries, copyData) => ipcRenderer.invoke('mr:import', entries, copyData),
onMrProgress: (cb) => {
const l = (_e, p) => cb(p);
ipcRenderer.on('mr:progress', l);
return () => ipcRenderer.removeListener('mr:progress', l);
},
cfScan: (folder) => ipcRenderer.invoke('cf:scan', folder),
cfImport: (entries, copyData) => ipcRenderer.invoke('cf:import', entries, copyData),
onCfProgress: (cb) => {
@@ -269,6 +282,7 @@ contextBridge.exposeInMainWorld('api', {
// Einstellungen/Konsole als eigenes, frei verschiebbares Fenster
popoutOpen: (modalId, params) => ipcRenderer.send('popout:open', modalId, params),
popoutHide: () => ipcRenderer.send('popout:hide'),
setTitleBarDesign: (design) => ipcRenderer.send('window:titleBar', design),
onPopoutReopen: (cb) => {
const l = (_e, params) => cb(params);
ipcRenderer.on('popout:reopen', l);
@@ -285,7 +299,7 @@ contextBridge.exposeInMainWorld('api', {
return () => ipcRenderer.removeListener('instances:changed', l);
},
onSkinChanged: (cb) => {
const l = () => cb();
const l = (_e, info) => cb(info || {});
ipcRenderer.on('skin:changed', l);
return () => ipcRenderer.removeListener('skin:changed', l);
},
+146 -8
View File
@@ -537,13 +537,23 @@ function escapeHtml(s) {
}
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) {
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 ----------
let mmcFound = [];
let cfFound = [];
let mrFound = [];
let mcFound = [];
let technicFound = [];
let importKind = 'zip';
@@ -568,8 +578,8 @@ async function ensureDefaultMcScan() {
}
function setImportKind(kind) {
importKind = ['zip', 'modpack', 'mc', 'cf', 'mmc', 'technic', 'code'].includes(kind) ? kind : 'zip';
['zip', 'modpack', 'mc', 'cf', 'mmc', 'technic', 'code'].forEach((entryKind) => {
importKind = ['zip', 'modpack', 'mc', 'cf', 'mr', 'mmc', 'technic', 'code'].includes(kind) ? kind : 'zip';
['zip', 'modpack', 'mc', 'cf', 'mr', 'mmc', 'technic', 'code'].forEach((entryKind) => {
el(`import-tab-${entryKind}`).classList.toggle('on', 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('mc-do').classList.toggle('hidden', importKind !== 'mc');
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('technic-do').classList.toggle('hidden', importKind !== 'technic');
el('code-do').classList.toggle('hidden', importKind !== 'code');
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('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('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').classList.add('hidden');
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-do').disabled = true;
el('mmc-progress').textContent = '';
@@ -613,6 +631,8 @@ function openImportDialog(kind = 'zip', vorauswahl = null) {
ensureDefaultMcScan();
}
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 (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.
*/
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]);
for (const q of quellen) {
if (!q || !scanner[q.kind]) continue;
@@ -923,6 +943,82 @@ async function runCfImport() {
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() {
const folder = el('mmc-path').value.trim();
if (!folder) { el('mmc-status').textContent = 'Bitte zuerst einen Ordner angeben.'; return; }
@@ -1464,7 +1560,11 @@ async function applySkinBackground(uuidOverride) {
// Jede Pose hat auch eine Entsprechung beim 3D-Dienst (Kamerawinkel/Ansicht),
// damit die Auswahl immer sichtbar wirkt – auch wenn der Pose-Dienst offline ist.
// 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'; } }
const local = await renderSkin3D(`https://crafthead.net/skin/${uuid}`, s.bgSkinPose || 'walking', { modell });
if (local) { root.style.setProperty('--bg-skin', `url("${local}")`); return; }
// 2) Rückfall: externe Render-Dienste
@@ -1630,7 +1730,7 @@ const ICONS_SVG = {
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"/>',
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"/>',
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"/>',
@@ -4790,7 +4890,8 @@ async function openSettings(pane) {
window.api.updateLastCheck().then(renderUpdateStatus).catch(() => {});
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-post').value = s.globalPostExit || '';
el('s-wrapper').value = s.wrapperCommand || '';
@@ -4806,6 +4907,7 @@ async function openSettings(pane) {
}
sel.value = s.bgSkin || '';
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-seconds').value = s.bgSkinCycleSeconds || 30;
sel.disabled = !!s.bgSkinCycle;
@@ -4939,6 +5041,7 @@ async function saveSettings() {
globalPostExit: el('s-global-post').value.trim(),
bgSkin: el('s-bgskin').value,
bgSkinPose: el('s-bgskin-pose').value,
bgSkinModel: el('s-bgskin-model').value,
bgSkinCycle: el('s-bgskin-cycle').checked,
bgSkinCycleSeconds: Math.max(5, numOrNull(el('s-bgskin-cycle-seconds').value) || 30),
sortBy: el('s-sortby').value,
@@ -5471,6 +5574,9 @@ function wire() {
});
document.addEventListener('scroll', closeContextMenu, true);
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
el('status-auth').addEventListener('click', async () => {
@@ -5530,6 +5636,18 @@ function wire() {
});
el('cf-list').addEventListener('change', updateCfButton);
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 () => {
const p = await window.api.mmcPickFolder();
if (p) { el('mmc-path').value = p; await scanMmc(); }
@@ -5567,6 +5685,10 @@ function wire() {
el('cf-progress').textContent = `Übernehme … ${p.current}/${p.total}`;
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) => {
el('mmc-progress').textContent = `Übernehme … ${p.current}/${p.total}`;
el('mmc-progress').classList.remove('hidden');
@@ -5804,6 +5926,10 @@ function wire() {
el('skin-pick').addEventListener('click', waehleSkinDatei);
el('skin-variante').addEventListener('change', skinVarianteGewechselt);
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('logs-select').addEventListener('change', (e) => ladeLog(e.target.value));
el('logs-folder').addEventListener('click', async () => {
@@ -5986,6 +6112,11 @@ function wire() {
if (p) { el('s-java').value = p; await refreshJavaStatus(p); }
});
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-val').textContent = e.target.value;
document.documentElement.style.setProperty('--bg-skin-opacity', String(e.target.value / 100));
@@ -5995,11 +6126,13 @@ function wire() {
await window.api.saveSettings({
bgSkin: el('s-bgskin').value,
bgSkinPose: el('s-bgskin-pose').value,
bgSkinModel: el('s-bgskin-model').value,
});
await refreshSkinBackground();
};
el('s-bgskin').addEventListener('change', previewSkin);
el('s-bgskin-pose').addEventListener('change', previewSkin);
el('s-bgskin-model').addEventListener('change', previewSkin);
const previewSkinCycle = async () => {
el('s-bgskin').disabled = el('s-bgskin-cycle').checked;
await window.api.saveSettings({
@@ -6240,6 +6373,7 @@ const POPOUT_OPENERS = {
screens: () => openScreens(),
servers: () => openServers(),
skin: () => openSkin(),
skineditor: (p) => window.openSkinEditor(p),
bedrock: () => openBedrock(),
login: (p) => openLogin({ addAccount: p.addAccount === true || p.addAccount === 'true' }),
import: (p) => {
@@ -6398,7 +6532,11 @@ window.addEventListener('DOMContentLoaded', async () => {
// Ein Skin-Wechsel oder eine neue/aktualisierte Instanz aus einem anderen
// Fenster (Skin-/Import-Popup) betrifft ebenfalls nur die Anzeige im
// 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' && !el('modal-skin').classList.contains('hidden')) openSkin();
});
window.api.onInstancesChanged(() => { if (!POPOUT) reload(); });
try { showAuthHealth(await window.api.authHealth()); } catch { /* noch keine Prüfung */ }
await loadMcIcons(); // echte Minecraft-Icons bereitstellen
+983
View File
@@ -0,0 +1,983 @@
'use strict';
/*
* skineditor.js – Pixel-Editor für Minecraft-Skins
* -------------------------------------------------
* Ein Skin ist eine einzige 64×64-Textur. Jedes Körperteil hat darin zwei
* Bereiche: die Grundschicht (liegt direkt auf der Figur) und die 2. Schicht
* (eine etwas größere Hülle darüber – Haare, Jacke, Ärmel …). Die 2. Schicht
* darf durchsichtig sein und lässt die Figur dadurch plastischer wirken.
*
* Gemalt wird wahlweise auf der aufgeklappten Vorlage (2D) oder direkt auf der
* drehbaren Figur (3D). Beide Ansichten sind immer da – die gerade nicht
* gewählte steht klein rechts und ist ebenfalls bemalbar.
*
* Läuft im Popup-Fenster "skineditor" und nutzt die Helfer aus renderer.js
* (el, show, toast, askConfirm, POPOUT, SKIN_POSES).
*/
(() => {
const N = 64;
// Lage der Körperteile in der Textur. w/h/d = Breite/Höhe/Tiefe in Pixeln,
// base/over = linke obere Ecke des Bereichs der Grund- bzw. 2. Schicht.
const TEILE = [
{ key: 'head', name: 'Kopf', w: 8, h: 8, d: 8, base: [0, 0], over: [32, 0], spiegel: 'head' },
{ key: 'body', name: 'Körper', w: 8, h: 12, d: 4, base: [16, 16], over: [16, 32], spiegel: 'body' },
{ key: 'rightArm', name: 'Rechter Arm', w: 4, h: 12, d: 4, base: [40, 16], over: [40, 32], spiegel: 'leftArm', arm: true },
{ key: 'leftArm', name: 'Linker Arm', w: 4, h: 12, d: 4, base: [32, 48], over: [48, 48], spiegel: 'rightArm', arm: true },
{ key: 'rightLeg', name: 'Rechtes Bein', w: 4, h: 12, d: 4, base: [0, 16], over: [0, 32], spiegel: 'leftLeg' },
{ key: 'leftLeg', name: 'Linkes Bein', w: 4, h: 12, d: 4, base: [16, 48], over: [0, 48], spiegel: 'rightLeg' },
];
const TEIL = Object.fromEntries(TEILE.map((t) => [t.key, t]));
const SEITE_NAME = { oben: 'oben', unten: 'unten', rechts: 'rechte Seite', vorne: 'vorne', links: 'linke Seite', hinten: 'hinten' };
const SEITE_GEGENSTUECK = { rechts: 'links', links: 'rechts' };
const SCHICHT_NAME = ['Grundschicht', '2. Schicht'];
// Rechtecke [x, y, breite, höhe] der sechs Seiten eines Teils (Minecraft-Aufteilung:
// oben/unten in der ersten Reihe, darunter rechts | vorne | links | hinten)
function seiten(teil, schicht, slim) {
const w = teil.arm && slim ? 3 : teil.w;
const { h, d } = teil;
const [u, v] = schicht ? teil.over : teil.base;
return {
oben: [u + d, v, w, d],
unten: [u + d + w, v, w, d],
rechts: [u, v + d, d, h],
vorne: [u + d, v + d, w, h],
links: [u + d + w, v + d, d, h],
hinten: [u + 2 * d + w, v + d, w, h],
};
}
// Für jede Armform: Liste aller Flächen und eine Pixel→Fläche-Karte
const AUFTEILUNG = [false, true].map((slim) => {
const flaechen = [];
const karte = new Array(N * N).fill(null);
for (const teil of TEILE) {
for (const schicht of [0, 1]) {
for (const [seite, r] of Object.entries(seiten(teil, schicht, slim))) {
const f = { teil, schicht, seite, x: r[0], y: r[1], w: r[2], h: r[3] };
flaechen.push(f);
for (let yy = 0; yy < f.h; yy++) for (let xx = 0; xx < f.w; xx++) karte[(f.y + yy) * N + f.x + xx] = f;
}
}
}
return { flaechen, karte };
});
// ---------- Zustand ----------
const ed = {
daten: new Uint8ClampedArray(N * N * 4),
slim: false,
werkzeug: 'stift',
vorPipette: 'stift',
farbe: '#3a6ea5',
schicht: 0,
zeigeAussen: true,
durchscheinen: true,
spiegeln: false,
modus: '2d',
undo: [],
redo: [],
geaendert: false,
hover: null,
strich: null, // { snap, letzter: [x,y] | null, letzterBild: [cx,cy] | null, geaendert }
viewer: null,
bereit: false,
zeichnenGeplant: false,
};
const textur = document.createElement('canvas');
textur.width = textur.height = N;
const texturCtx = textur.getContext('2d', { willReadFrequently: true });
// Grundschicht als Schatten unter die 2. Schicht legen (Option "durchscheinen")
const schatten = document.createElement('canvas');
schatten.width = schatten.height = N;
const schattenCtx = schatten.getContext('2d');
// Hintergrund (Karomuster in den Flächen) je Armform einmal vorberechnet
const hintergrund = [null, null];
const aufteilung = () => AUFTEILUNG[ed.slim ? 1 : 0];
const flaecheBei = (x, y) => (x < 0 || y < 0 || x >= N || y >= N ? null : aufteilung().karte[y * N + x]);
const dunkel = () => document.documentElement.getAttribute('data-theme') === 'dark';
function hexZuRgb(hex) {
const m = /^#?([0-9a-f]{6})$/i.exec(hex || '');
if (!m) return [0, 0, 0];
const n = parseInt(m[1], 16);
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
}
const rgbZuHex = (r, g, b) => '#' + [r, g, b].map((v) => v.toString(16).padStart(2, '0')).join('');
// Pixel auf der gespiegelten Körperseite (rechter ↔ linker Arm/Bein,
// rechte ↔ linke Seite, waagerecht umgedreht)
function spiegelPixel(x, y) {
const f = flaecheBei(x, y);
if (!f) return null;
const partner = TEIL[f.teil.spiegel];
const seite = SEITE_GEGENSTUECK[f.seite] || f.seite;
const r = seiten(partner, f.schicht, ed.slim)[seite];
const fx = x - f.x;
const fy = y - f.y;
return [r[0] + (r[2] - 1 - fx), r[1] + fy];
}
// ---------- Daten ändern ----------
function setzePixel(x, y, rgba) {
const i = (y * N + x) * 4;
const d = ed.daten;
if (d[i] === rgba[0] && d[i + 1] === rgba[1] && d[i + 2] === rgba[2] && d[i + 3] === rgba[3]) return false;
d[i] = rgba[0]; d[i + 1] = rgba[1]; d[i + 2] = rgba[2]; d[i + 3] = rgba[3];
return true;
}
function zielPixel(x, y) {
const liste = [[x, y]];
if (ed.spiegeln) {
const s = spiegelPixel(x, y);
if (s && (s[0] !== x || s[1] !== y)) liste.push(s);
}
return liste;
}
function malFarbe() {
if (ed.werkzeug === 'radierer') return [0, 0, 0, 0];
return [...hexZuRgb(ed.farbe), 255];
}
// Einen Pixel mit dem aktuellen Werkzeug bearbeiten (Stift/Radierer)
function malePixel(x, y) {
const f = flaecheBei(x, y);
if (!f || f.schicht !== ed.schicht) return false;
let geaendert = false;
const rgba = malFarbe();
for (const [px, py] of zielPixel(x, y)) if (setzePixel(px, py, rgba)) geaendert = true;
return geaendert;
}
// Füllen: zusammenhängende gleichfarbige Pixel innerhalb derselben Fläche
function fuelle(x, y) {
const f = flaecheBei(x, y);
if (!f || f.schicht !== ed.schicht) return false;
const d = ed.daten;
const i0 = (y * N + x) * 4;
const alt = [d[i0], d[i0 + 1], d[i0 + 2], d[i0 + 3]];
const neu = malFarbe();
if (alt.every((v, k) => v === neu[k])) return false;
const gleich = (px, py) => {
const i = (py * N + px) * 4;
return d[i] === alt[0] && d[i + 1] === alt[1] && d[i + 2] === alt[2] && d[i + 3] === alt[3];
};
const gesehen = new Set();
const stapel = [[x, y]];
const treffer = [];
while (stapel.length) {
const [px, py] = stapel.pop();
const k = py * N + px;
if (gesehen.has(k)) continue;
if (px < f.x || py < f.y || px >= f.x + f.w || py >= f.y + f.h) continue;
gesehen.add(k);
if (!gleich(px, py)) continue;
treffer.push([px, py]);
stapel.push([px + 1, py], [px - 1, py], [px, py + 1], [px, py - 1]);
}
let geaendert = false;
for (const [px, py] of treffer) for (const [zx, zy] of zielPixel(px, py)) if (setzePixel(zx, zy, neu)) geaendert = true;
return geaendert;
}
function nimmFarbe(x, y) {
const i = (y * N + x) * 4;
const d = ed.daten;
if (d[i + 3] === 0) return false;
setzeFarbe(rgbZuHex(d[i], d[i + 1], d[i + 2]));
return true;
}
// ---------- Striche, Rückgängig ----------
function beginneStrich() {
ed.strich = { snap: ed.daten.slice(), letzter: null, letzterBild: null, geaendert: false };
}
function beendeStrich() {
const s = ed.strich;
ed.strich = null;
if (!s || !s.geaendert) return;
ed.undo.push(s.snap);
if (ed.undo.length > 100) ed.undo.shift();
ed.redo = [];
ed.geaendert = true;
if (ed.werkzeug !== 'radierer') merkeFarbe(ed.farbe);
aktualisiereKnoepfe();
}
// Werkzeug auf einen Texturpixel anwenden (während eines Strichs)
function wendeAn(x, y) {
const s = ed.strich;
if (!s) return;
if (ed.werkzeug === 'pipette') {
nimmFarbe(x, y);
return;
}
if (ed.werkzeug === 'fuellen') {
if (s.letzter) return; // ein Klick = eine Füllung
s.letzter = [x, y];
if (fuelle(x, y)) { s.geaendert = true; texturGeaendert(); }
return;
}
let geaendert = false;
// Lücken zwischen zwei Mausereignissen schließen (nur innerhalb der Vorlage
// sinnvoll, auf der Figur liegen benachbarte Pixel nicht nebeneinander)
const punkte = s.letzter && s.zweiD ? linie(s.letzter[0], s.letzter[1], x, y) : [[x, y]];
for (const [px, py] of punkte) if (malePixel(px, py)) geaendert = true;
s.letzter = [x, y];
if (geaendert) { s.geaendert = true; texturGeaendert(); }
}
function linie(x0, y0, x1, y1) {
const punkte = [];
const dx = Math.abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
const dy = -Math.abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
let err = dx + dy;
for (;;) {
punkte.push([x0, y0]);
if (x0 === x1 && y0 === y1) break;
const e2 = 2 * err;
if (e2 >= dy) { err += dy; x0 += sx; }
if (e2 <= dx) { err += dx; y0 += sy; }
}
return punkte;
}
function rueckgaengig() {
if (!ed.undo.length) return;
ed.redo.push(ed.daten.slice());
ed.daten = ed.undo.pop();
ed.geaendert = true;
texturGeaendert();
aktualisiereKnoepfe();
}
function wiederholen() {
if (!ed.redo.length) return;
ed.undo.push(ed.daten.slice());
ed.daten = ed.redo.pop();
ed.geaendert = true;
texturGeaendert();
aktualisiereKnoepfe();
}
// ganze Änderung (Laden, Leeren …) als ein Rückgängig-Schritt
function aenderung(fn) {
const snap = ed.daten.slice();
fn();
if (snap.every((v, i) => v === ed.daten[i])) return false;
ed.undo.push(snap);
if (ed.undo.length > 100) ed.undo.shift();
ed.redo = [];
ed.geaendert = true;
texturGeaendert();
aktualisiereKnoepfe();
return true;
}
// ---------- Farben ----------
const FARB_SCHLUESSEL = 'aeromc-skin-farben';
const STANDARD_FARBEN = ['#000000', '#ffffff', '#c69c6d', '#a87b4f', '#6b4a2f', '#3b2416', '#d32f2f', '#f9a825',
'#43a047', '#1e88e5', '#3949ab', '#8e24aa', '#00897b', '#757575', '#424242', '#ff8a65'];
let zuletzt = [];
try { zuletzt = JSON.parse(localStorage.getItem(FARB_SCHLUESSEL) || '[]'); } catch { zuletzt = []; }
if (!Array.isArray(zuletzt) || !zuletzt.length) zuletzt = STANDARD_FARBEN.slice();
function merkeFarbe(hex) {
zuletzt = [hex, ...zuletzt.filter((f) => f !== hex)].slice(0, 16);
try { localStorage.setItem(FARB_SCHLUESSEL, JSON.stringify(zuletzt)); } catch { /* egal */ }
zeigeZuletzt();
}
function zeigeZuletzt() {
const box = el('se-zuletzt');
box.innerHTML = '';
for (const f of zuletzt) {
const b = document.createElement('button');
b.className = 'se-farbfeld' + (f === ed.farbe ? ' on' : '');
b.style.background = f;
b.title = f;
b.addEventListener('click', () => setzeFarbe(f));
box.appendChild(b);
}
}
function setzeFarbe(hex) {
ed.farbe = hex.toLowerCase();
el('se-farbe').value = ed.farbe;
el('se-hex').value = ed.farbe;
// nach dem Aufnehmen zurück zum vorherigen Werkzeug; mit dem Radierer
// ergibt eine neue Farbe keinen Sinn -> Stift
if (ed.werkzeug === 'pipette') waehleWerkzeug(ed.vorPipette === 'radierer' ? 'stift' : ed.vorPipette);
else if (ed.werkzeug === 'radierer') waehleWerkzeug('stift');
zeigeZuletzt();
}
// ---------- Bedienelemente ----------
function waehleWerkzeug(w) {
if (w === 'radierer' && ed.schicht === 0) w = 'stift';
if (w === 'pipette' && ed.werkzeug !== 'pipette') ed.vorPipette = ed.werkzeug;
ed.werkzeug = w;
document.querySelectorAll('[data-se-tool]').forEach((b) => b.classList.toggle('on', b.dataset.seTool === w));
for (const h of [el('se-2d'), el('se-3d')]) h.dataset.werkzeug = w;
}
function waehleSchicht(s) {
ed.schicht = s;
document.querySelectorAll('[data-se-schicht]').forEach((b) => b.classList.toggle('on', Number(b.dataset.seSchicht) === s));
// die 2. Schicht muss sichtbar sein, wenn man darauf malen will
if (s === 1 && !ed.zeigeAussen) { ed.zeigeAussen = true; el('se-aussen').checked = true; }
const radierer = document.querySelector('[data-se-tool="radierer"]');
radierer.disabled = s === 0;
radierer.title = s === 0
? 'Radierer – nur in der 2. Schicht (die Grundschicht darf keine Löcher haben)'
: 'Radierer (E) – macht Pixel durchsichtig';
if (s === 0 && ed.werkzeug === 'radierer') waehleWerkzeug('stift');
el('se-durch').disabled = s === 0;
ed.hover = null;
texturGeaendert();
}
function waehleModus(m) {
ed.modus = m;
document.querySelectorAll('[data-se-modus]').forEach((b) => b.classList.toggle('on', b.dataset.seModus === m));
const gross = el('se-buehne');
const klein = el('se-mini');
gross.appendChild(m === '2d' ? el('se-2d') : el('se-3d'));
klein.appendChild(m === '2d' ? el('se-3d') : el('se-2d'));
el('se-mini-titel').textContent = m === '2d' ? '3D-Vorschau' : 'Vorlage';
zeigeInfo();
passeGroessenAn();
}
function aktualisiereKnoepfe() {
el('se-undo').disabled = !ed.undo.length;
el('se-redo').disabled = !ed.redo.length;
}
function zeigeInfo() {
const info = el('se-info');
const h = ed.hover;
const f = h && flaecheBei(h[0], h[1]);
if (f) {
info.textContent = `${f.teil.name} · ${SEITE_NAME[f.seite]} · ${SCHICHT_NAME[f.schicht]} · Pixel ${h[0]}, ${h[1]}`;
return;
}
info.textContent = ed.modus === '3d'
? 'Linke Maustaste malt · rechte Maustaste dreht · Mausrad zoomt · Alt+Klick nimmt Farbe auf'
: 'Linke Maustaste malt · Alt+Klick nimmt eine Farbe auf · abgedunkelte Bereiche gehören zur anderen Schicht';
}
function status(text) { el('se-status').textContent = text || ''; el('se-status').title = text || ''; }
// ---------- Zeichnen ----------
function texturGeaendert() {
texturCtx.putImageData(new ImageData(ed.daten, N, N), 0, 0);
// Schatten: Grundschicht-Pixel an die Stelle der zugehörigen 2. Schicht legen
const s = schattenCtx.createImageData(N, N);
for (const f of aufteilung().flaechen) {
if (f.schicht !== 1) continue;
const r = seiten(f.teil, 0, ed.slim)[f.seite];
for (let yy = 0; yy < f.h; yy++) {
for (let xx = 0; xx < f.w; xx++) {
const q = ((r[1] + yy) * N + r[0] + xx) * 4;
const z = ((f.y + yy) * N + f.x + xx) * 4;
s.data[z] = ed.daten[q]; s.data[z + 1] = ed.daten[q + 1]; s.data[z + 2] = ed.daten[q + 2]; s.data[z + 3] = ed.daten[q + 3];
}
}
}
schattenCtx.putImageData(s, 0, 0);
planeZeichnen();
}
function planeZeichnen() {
if (ed.zeichnenGeplant) return;
ed.zeichnenGeplant = true;
requestAnimationFrame(() => {
ed.zeichnenGeplant = false;
zeichne2D();
zeichne3D();
zeigeInfo();
});
}
function hintergrundFuer(slim) {
const k = slim ? 1 : 0;
if (hintergrund[k] && hintergrund[k].dunkel === dunkel()) return hintergrund[k].canvas;
const c = document.createElement('canvas');
c.width = c.height = N;
const g = c.getContext('2d');
const img = g.createImageData(N, N);
const [a, b] = dunkel() ? [[58, 58, 64], [48, 48, 54]] : [[236, 236, 238], [218, 219, 223]];
for (const f of AUFTEILUNG[k].flaechen) {
for (let yy = f.y; yy < f.y + f.h; yy++) {
for (let xx = f.x; xx < f.x + f.w; xx++) {
const farbe = (xx + yy) % 2 ? a : b;
const i = (yy * N + xx) * 4;
img.data[i] = farbe[0]; img.data[i + 1] = farbe[1]; img.data[i + 2] = farbe[2]; img.data[i + 3] = 255;
}
}
}
g.putImageData(img, 0, 0);
hintergrund[k] = { canvas: c, dunkel: dunkel() };
return c;
}
// sichtbare Textur für die 2D-Ansicht: ohne ungenutzte Bereiche und, falls
// ausgeblendet, ohne 2. Schicht
const sicht = document.createElement('canvas');
sicht.width = sicht.height = N;
const sichtCtx = sicht.getContext('2d');
function zeichne2D() {
const canvas = el('se-2d-canvas');
const k = Number(canvas.dataset.k) || 0;
if (!k) return;
const g = canvas.getContext('2d');
g.setTransform(1, 0, 0, 1, 0, 0);
g.clearRect(0, 0, canvas.width, canvas.height);
g.imageSmoothingEnabled = false;
g.setTransform(k, 0, 0, k, 0, 0);
g.fillStyle = dunkel() ? '#1c1c1f' : '#c9ccd1';
g.fillRect(0, 0, N, N);
g.drawImage(hintergrundFuer(ed.slim), 0, 0);
if (ed.schicht === 1 && ed.durchscheinen) {
g.globalAlpha = 0.35;
g.drawImage(schatten, 0, 0);
g.globalAlpha = 1;
}
sichtCtx.clearRect(0, 0, N, N);
const img = sichtCtx.createImageData(N, N);
const karte = aufteilung().karte;
for (let i = 0; i < N * N; i++) {
const f = karte[i];
if (!f || (f.schicht === 1 && !ed.zeigeAussen)) continue;
const q = i * 4;
img.data[q] = ed.daten[q]; img.data[q + 1] = ed.daten[q + 1]; img.data[q + 2] = ed.daten[q + 2]; img.data[q + 3] = ed.daten[q + 3];
}
sichtCtx.putImageData(img, 0, 0);
g.drawImage(sicht, 0, 0);
// andere Schicht abdunkeln
g.fillStyle = dunkel() ? 'rgba(20,20,22,.62)' : 'rgba(40,44,52,.45)';
for (const f of aufteilung().flaechen) if (f.schicht !== ed.schicht) g.fillRect(f.x, f.y, f.w, f.h);
const px = 1 / k;
// Pixelraster
if (k >= 6) {
g.strokeStyle = dunkel() ? 'rgba(255,255,255,.07)' : 'rgba(0,0,0,.08)';
g.lineWidth = px;
g.beginPath();
for (let i = 1; i < N; i++) { g.moveTo(i, 0); g.lineTo(i, N); g.moveTo(0, i); g.lineTo(N, i); }
g.stroke();
}
// Flächenränder
g.lineWidth = 1.5 * px;
for (const f of aufteilung().flaechen) {
g.strokeStyle = f.schicht === ed.schicht
? (dunkel() ? 'rgba(255,255,255,.45)' : 'rgba(0,0,0,.4)')
: (dunkel() ? 'rgba(255,255,255,.12)' : 'rgba(0,0,0,.12)');
g.strokeRect(f.x, f.y, f.w, f.h);
}
// Mauszeiger und gespiegelter Partner
if (ed.hover) {
const [hx, hy] = ed.hover;
g.lineWidth = 2 * px;
g.strokeStyle = dunkel() ? '#ffffff' : '#000000';
g.strokeRect(hx + px, hy + px, 1 - 2 * px, 1 - 2 * px);
if (ed.spiegeln) {
const s = spiegelPixel(hx, hy);
if (s && (s[0] !== hx || s[1] !== hy)) {
g.setLineDash([2 * px, 2 * px]);
g.strokeRect(s[0] + px, s[1] + px, 1 - 2 * px, 1 - 2 * px);
g.setLineDash([]);
}
}
}
}
function zeichne3D() {
const v = ed.viewer;
if (!v || !v.skinTexture) return;
const g = v.skinCanvas.getContext('2d');
if (v.skinCanvas.width !== N || v.skinCanvas.height !== N) { v.skinCanvas.width = N; v.skinCanvas.height = N; }
g.clearRect(0, 0, N, N);
g.drawImage(textur, 0, 0);
// Pixel unter der Maus hervorheben
if (ed.hover && ed.werkzeug !== 'pipette') {
const ziele = ed.spiegeln ? zielPixel(ed.hover[0], ed.hover[1]) : [ed.hover];
for (const [hx, hy] of ziele) {
const i = (hy * N + hx) * 4;
const hell = ed.daten[i + 3] === 0 || (ed.daten[i] + ed.daten[i + 1] + ed.daten[i + 2]) < 380;
g.fillStyle = hell ? 'rgba(255,255,255,.75)' : 'rgba(0,0,0,.55)';
g.fillRect(hx, hy, 1, 1);
}
}
v.skinTexture.needsUpdate = true;
v.playerObject.skin.setOuterLayerVisible(ed.zeigeAussen);
}
// ---------- Größen ----------
function passeGroessenAn() {
const h2 = el('se-2d');
const c = el('se-2d-canvas');
const platz = Math.min(h2.clientWidth, h2.clientHeight) - 8;
const s = Math.max(1, Math.floor(platz / N));
const dpr = window.devicePixelRatio || 1;
c.style.width = c.style.height = (s * N) + 'px';
c.width = c.height = Math.round(s * N * dpr);
c.dataset.k = String((s * N * dpr) / N);
const h3 = el('se-3d');
if (ed.viewer && h3.clientWidth && h3.clientHeight) ed.viewer.setSize(h3.clientWidth, h3.clientHeight);
planeZeichnen();
}
// ---------- 2D-Eingabe ----------
function texelAus2D(e) {
const c = el('se-2d-canvas');
const r = c.getBoundingClientRect();
const x = Math.floor(((e.clientX - r.left) / r.width) * N);
const y = Math.floor(((e.clientY - r.top) / r.height) * N);
return x >= 0 && y >= 0 && x < N && y < N ? [x, y] : null;
}
function verbinde2D() {
const c = el('se-2d-canvas');
c.addEventListener('pointerdown', (e) => {
if (e.button !== 0) return;
const t = texelAus2D(e);
if (!t) return;
e.preventDefault();
try { c.setPointerCapture(e.pointerId); } catch { /* egal */ }
if (e.altKey) { nimmFarbe(t[0], t[1]); return; }
beginneStrich();
ed.strich.zweiD = true;
wendeAn(t[0], t[1]);
});
c.addEventListener('pointermove', (e) => {
const t = texelAus2D(e);
if (ed.strich && t) wendeAn(t[0], t[1]);
const neu = t && flaecheBei(t[0], t[1]) ? t : null;
if (String(neu) !== String(ed.hover)) { ed.hover = neu; planeZeichnen(); }
});
const ende = () => beendeStrich();
c.addEventListener('pointerup', ende);
c.addEventListener('pointercancel', ende);
c.addEventListener('pointerleave', () => { if (!ed.strich && ed.hover) { ed.hover = null; planeZeichnen(); } });
}
// ---------- 3D: Strahl gegen die Figur ----------
// three.js' Raycaster wird von skinview3d nicht mitgeliefert – daher eigene
// Schnittprüfung Strahl/Dreieck (Möller–Trumbore) gegen die Quader der Figur.
function schnitt(o, d, a, b, c) {
const e1x = b.x - a.x, e1y = b.y - a.y, e1z = b.z - a.z;
const e2x = c.x - a.x, e2y = c.y - a.y, e2z = c.z - a.z;
const px = d.y * e2z - d.z * e2y, py = d.z * e2x - d.x * e2z, pz = d.x * e2y - d.y * e2x;
const det = e1x * px + e1y * py + e1z * pz;
if (Math.abs(det) < 1e-9) return null;
const inv = 1 / det;
const tx = o.x - a.x, ty = o.y - a.y, tz = o.z - a.z;
const u = (tx * px + ty * py + tz * pz) * inv;
if (u < 0 || u > 1) return null;
const qx = ty * e1z - tz * e1y, qy = tz * e1x - tx * e1z, qz = tx * e1y - ty * e1x;
const v = (d.x * qx + d.y * qy + d.z * qz) * inv;
if (v < 0 || u + v > 1) return null;
const t = (e2x * qx + e2y * qy + e2z * qz) * inv;
return t > 1e-6 ? { t, u, v } : null;
}
// liefert den Texturpixel der getroffenen Schicht (0/1) oder null
function strahl(clientX, clientY, schicht) {
const v = ed.viewer;
if (!v) return null;
const r = v.canvas.getBoundingClientRect();
if (!r.width || !r.height) return null;
const cam = v.camera;
const V3 = cam.position.constructor;
cam.updateMatrixWorld();
const o = new V3().setFromMatrixPosition(cam.matrixWorld);
const nx = ((clientX - r.left) / r.width) * 2 - 1;
const ny = -((clientY - r.top) / r.height) * 2 + 1;
const d = new V3(nx, ny, 0.5).unproject(cam).sub(o).normalize();
v.playerObject.updateMatrixWorld(true);
const ecken = [new V3(), new V3(), new V3()];
let best = null;
for (const teil of v.playerObject.skin.getBodyParts()) {
if (!teil.visible) continue;
const mesh = schicht ? teil.outerLayer : teil.innerLayer;
if (!mesh || !mesh.visible) continue;
const geo = mesh.geometry;
const pos = geo.attributes.position;
const idx = geo.index;
const anzahl = idx ? idx.count : pos.count;
for (let i = 0; i < anzahl; i += 3) {
const ia = idx ? [idx.getX(i), idx.getX(i + 1), idx.getX(i + 2)] : [i, i + 1, i + 2];
for (let k = 0; k < 3; k++) ecken[k].fromBufferAttribute(pos, ia[k]).applyMatrix4(mesh.matrixWorld);
const h = schnitt(o, d, ecken[0], ecken[1], ecken[2]);
if (h && (!best || h.t < best.t)) best = { t: h.t, b: [1 - h.u - h.v, h.u, h.v], ia, uv: geo.attributes.uv };
}
}
if (!best) return null;
let U = 0, W = 0;
let uMin = 1, uMax = 0, vMin = 1, vMax = 0;
for (let k = 0; k < 3; k++) {
const u = best.uv.getX(best.ia[k]);
const w = best.uv.getY(best.ia[k]);
U += best.b[k] * u; W += best.b[k] * w;
uMin = Math.min(uMin, u); uMax = Math.max(uMax, u); vMin = Math.min(vMin, w); vMax = Math.max(vMax, w);
}
// Dreieck gehört zu genau einer Seitenfläche – Rundung an der Kante darf
// nicht in die Nachbarfläche rutschen
const x0 = Math.round(uMin * N), x1 = Math.round(uMax * N) - 1;
const y0 = Math.round((1 - vMax) * N), y1 = Math.round((1 - vMin) * N) - 1;
const x = Math.min(x1, Math.max(x0, Math.floor(U * N)));
const y = Math.min(y1, Math.max(y0, Math.floor((1 - W) * N)));
return { t: best.t, x, y };
}
// sichtbarer Pixel für die Pipette: erst die 2. Schicht (falls dort Farbe ist)
function sichtbarerPixel(cx, cy) {
if (ed.zeigeAussen) {
const a = strahl(cx, cy, 1);
if (a && ed.daten[(a.y * N + a.x) * 4 + 3] > 0) return [a.x, a.y];
}
const b = strahl(cx, cy, 0);
return b ? [b.x, b.y] : null;
}
function verbinde3D() {
const huelle = el('se-3d');
const canvas = el('se-3d-canvas');
// in der Einfangphase: trifft die Maus die Figur, malen wir und die
// Drehsteuerung von skinview3d bekommt das Ereignis gar nicht erst
huelle.addEventListener('pointerdown', (e) => {
if (e.button !== 0 || !ed.viewer) return;
if (e.altKey || ed.werkzeug === 'pipette') {
const t = sichtbarerPixel(e.clientX, e.clientY);
if (!t) return;
e.stopPropagation(); e.preventDefault();
nimmFarbe(t[0], t[1]);
return;
}
const t = strahl(e.clientX, e.clientY, ed.schicht);
if (!t) return;
e.stopPropagation(); e.preventDefault();
try { canvas.setPointerCapture(e.pointerId); } catch { /* egal */ }
beginneStrich();
ed.strich.letzterBild = [e.clientX, e.clientY];
wendeAn(t.x, t.y);
}, true);
canvas.addEventListener('pointermove', (e) => {
if (ed.strich) {
// Strecke seit dem letzten Ereignis in kleinen Schritten abtasten
const [ax, ay] = ed.strich.letzterBild || [e.clientX, e.clientY];
const schritte = Math.max(1, Math.ceil(Math.hypot(e.clientX - ax, e.clientY - ay) / 2));
for (let i = 1; i <= schritte; i++) {
const t = strahl(ax + ((e.clientX - ax) * i) / schritte, ay + ((e.clientY - ay) * i) / schritte, ed.schicht);
if (t) wendeAn(t.x, t.y);
}
ed.strich.letzterBild = [e.clientX, e.clientY];
}
if (e.buttons && !ed.strich) return; // dreht gerade
const t = strahl(e.clientX, e.clientY, ed.schicht);
const neu = t ? [t.x, t.y] : null;
if (String(neu) !== String(ed.hover)) { ed.hover = neu; planeZeichnen(); }
});
const ende = () => beendeStrich();
canvas.addEventListener('pointerup', ende);
canvas.addEventListener('pointercancel', ende);
canvas.addEventListener('pointerleave', () => { if (!ed.strich && ed.hover) { ed.hover = null; planeZeichnen(); } });
}
async function erzeugeViewer() {
if (ed.viewer || typeof skinview3d === 'undefined') {
if (typeof skinview3d === 'undefined') el('se-3d-leer').textContent = '3D-Ansicht nicht verfügbar';
return;
}
const h = el('se-3d');
const viewer = new skinview3d.SkinViewer({
canvas: el('se-3d-canvas'),
width: h.clientWidth || 300,
height: h.clientHeight || 300,
skin: textur,
model: ed.slim ? 'slim' : 'default',
});
await new Promise((resolve) => {
const check = (n) => {
if ((viewer.playerObject && viewer.playerObject.skin && viewer.playerObject.skin.map) || n > 150) resolve();
else setTimeout(() => check(n + 1), 40);
};
check(0);
});
viewer.fov = 36;
viewer.zoom = 0.78;
viewer.autoRotate = false;
viewer.animation = null;
// leicht abgespreizte Arme/Beine, damit man auch an die Innenseiten kommt
const p = viewer.playerObject;
p.rotation.set(0, -0.35, 0);
p.skin.leftArm.rotation.set(0, 0, 0.2);
p.skin.rightArm.rotation.set(0, 0, -0.2);
p.skin.leftLeg.rotation.set(0, 0, 0.06);
p.skin.rightLeg.rotation.set(0, 0, -0.06);
viewer.controls.enableRotate = true;
viewer.controls.enableZoom = true;
viewer.controls.enablePan = false;
// rechte Maustaste dreht immer – auch wenn man gerade auf die Figur zeigt
viewer.controls.mouseButtons = { LEFT: 0, MIDDLE: 1, RIGHT: 0 };
ed.viewer = viewer;
el('se-3d-leer').classList.add('hidden');
passeGroessenAn();
}
async function setzeArmform(slim) {
ed.slim = !!slim;
el('se-variante').value = ed.slim ? 'slim' : 'classic';
ed.hover = null;
if (ed.viewer) {
await ed.viewer.loadSkin(textur, { model: ed.slim ? 'slim' : 'default' });
}
texturGeaendert();
}
// ---------- Laden / Vorlage ----------
function vorlage() {
const d = new Uint8ClampedArray(N * N * 4);
const farben = { head: '#c69c6d', body: '#2f9e9e', rightArm: '#c69c6d', leftArm: '#c69c6d', rightLeg: '#3b3f9e', leftLeg: '#3b3f9e' };
for (const f of AUFTEILUNG[ed.slim ? 1 : 0].flaechen) {
if (f.schicht !== 0) continue;
const [r, g, b] = hexZuRgb(farben[f.teil.key]);
for (let yy = f.y; yy < f.y + f.h; yy++) {
for (let xx = f.x; xx < f.x + f.w; xx++) {
const i = (yy * N + xx) * 4;
d[i] = r; d[i + 1] = g; d[i + 2] = b; d[i + 3] = 255;
}
}
}
return d;
}
function ladeBild(dataUri) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
if (img.width !== N || (img.height !== N && img.height !== N / 2)) {
reject(new Error(`Ein Skin muss 64×64 oder 64×32 Pixel groß sein (diese Datei: ${img.width}×${img.height}).`));
return;
}
const c = document.createElement('canvas');
c.width = c.height = N;
const g = c.getContext('2d', { willReadFrequently: true });
g.drawImage(img, 0, 0);
const daten = g.getImageData(0, 0, N, N).data;
if (img.height === N / 2) {
// alte 64×32-Skins: linker Arm und linkes Bein sind dort gespiegelte
// Kopien der rechten – wie Minecraft es selbst macht
const karte = AUFTEILUNG[0].karte;
for (let i = 0; i < N * N / 2; i++) {
const f = karte[i];
if (!f || f.schicht !== 0 || (f.teil.key !== 'rightArm' && f.teil.key !== 'rightLeg')) continue;
const partner = TEIL[f.teil.spiegel];
const seite = SEITE_GEGENSTUECK[f.seite] || f.seite;
const r = seiten(partner, 0, false)[seite];
const x = i % N, y = Math.floor(i / N);
const z = ((r[1] + (y - f.y)) * N + r[0] + (r[2] - 1 - (x - f.x))) * 4;
for (let k = 0; k < 4; k++) daten[z + k] = daten[i * 4 + k];
}
}
resolve(daten);
};
img.onerror = () => reject(new Error('Bild konnte nicht gelesen werden.'));
img.src = dataUri;
});
}
async function uebernimmDaten(daten, variante, text) {
if (variante) await setzeArmform(variante === 'slim');
aenderung(() => { ed.daten = new Uint8ClampedArray(daten); });
ed.geaendert = false;
texturGeaendert();
status(text || '');
}
function wennVerwerfenOk(aktion) {
if (!ed.geaendert) { aktion(); return; }
askConfirm('Änderungen verwerfen?', 'Der Skin im Editor hat ungespeicherte Änderungen. Trotzdem fortfahren?', 'Verwerfen', aktion);
}
async function ladeAktuellen() {
status('Aktueller Skin wird geladen …');
const res = await window.api.skinCurrentData();
if (!res.ok) {
if (res.standard) {
await uebernimmDaten(vorlage(), res.variante, 'Dein Konto nutzt den Standard-Skin – hier ist eine leere Vorlage zum Loslegen.');
} else {
status('✖ ' + (res.message || 'Skin konnte nicht geladen werden.'));
}
return;
}
try {
await uebernimmDaten(await ladeBild(res.dataUri), res.variante, `Skin von ${res.name || 'deinem Konto'} geladen.`);
} catch (err) { status('✖ ' + err.message); }
}
async function ladeDatei(pfad, variante) {
const res = pfad ? await window.api.skinReadData(pfad) : await window.api.skinChoose();
if (res.canceled) return;
if (!res.ok) { status('✖ ' + (res.message || 'Datei konnte nicht geladen werden.')); return; }
try {
await uebernimmDaten(await ladeBild(res.dataUri), variante, `${(res.pfad || pfad || '').split(/[\\/]/).pop()} geladen.`);
} catch (err) { status('✖ ' + err.message); }
}
// ---------- Speichern / Übernehmen ----------
function alsBase64() {
const c = document.createElement('canvas');
c.width = c.height = N;
c.getContext('2d').putImageData(new ImageData(ed.daten, N, N), 0, 0);
return c.toDataURL('image/png').split(',')[1];
}
// Minecraft zeigt durchsichtige Pixel der Grundschicht schwarz an
function loecherInGrundschicht() {
let n = 0;
for (const f of aufteilung().flaechen) {
if (f.schicht !== 0) continue;
for (let yy = f.y; yy < f.y + f.h; yy++) for (let xx = f.x; xx < f.x + f.w; xx++) if (ed.daten[(yy * N + xx) * 4 + 3] < 255) n++;
}
return n;
}
function loecherHinweis() {
const n = loecherInGrundschicht();
return n ? ` Hinweis: ${n} durchsichtige Pixel in der Grundschicht erscheinen im Spiel schwarz.` : '';
}
async function speichere() {
const res = await window.api.skinSaveData(alsBase64());
if (res.canceled) return;
if (!res.ok) { status('✖ ' + (res.message || 'Speichern fehlgeschlagen.')); return; }
ed.geaendert = false;
status('✔ Gespeichert: ' + res.pfad.split(/[\\/]/).pop() + '.' + loecherHinweis());
}
async function uebernimm() {
const btn = el('se-uebernehmen');
btn.disabled = true;
status('Wird hochgeladen …');
try {
const res = await window.api.skinApplyData(alsBase64(), ed.slim ? 'slim' : 'classic');
if (res.ok) {
ed.geaendert = false;
status('✔ Skin gesetzt. Im Spiel kann es einen Moment dauern.' + loecherHinweis());
toast('Skin geändert.');
} else {
status('✖ ' + (res.message || 'Fehlgeschlagen.'));
}
} finally {
btn.disabled = false;
}
}
// ---------- Öffnen ----------
let verdrahtet = false;
function verdrahte() {
if (verdrahtet) return;
verdrahtet = true;
document.querySelectorAll('[data-se-tool]').forEach((b) => b.addEventListener('click', () => waehleWerkzeug(b.dataset.seTool)));
document.querySelectorAll('[data-se-schicht]').forEach((b) => b.addEventListener('click', () => waehleSchicht(Number(b.dataset.seSchicht))));
document.querySelectorAll('[data-se-modus]').forEach((b) => b.addEventListener('click', () => waehleModus(b.dataset.seModus)));
el('se-farbe').addEventListener('input', () => setzeFarbe(el('se-farbe').value));
el('se-hex').addEventListener('change', () => {
let v = el('se-hex').value.trim();
if (!v.startsWith('#')) v = '#' + v;
if (/^#[0-9a-f]{6}$/i.test(v)) setzeFarbe(v);
else el('se-hex').value = ed.farbe;
});
el('se-aussen').addEventListener('change', () => {
ed.zeigeAussen = el('se-aussen').checked;
if (!ed.zeigeAussen && ed.schicht === 1) waehleSchicht(0);
planeZeichnen();
});
el('se-durch').addEventListener('change', () => { ed.durchscheinen = el('se-durch').checked; planeZeichnen(); });
el('se-spiegeln').addEventListener('change', () => { ed.spiegeln = el('se-spiegeln').checked; planeZeichnen(); });
el('se-leeren').addEventListener('click', () => {
askConfirm('2. Schicht leeren?', 'Alle Pixel der 2. Schicht werden durchsichtig. Mit Strg+Z lässt sich das rückgängig machen.', 'Leeren', () => {
const geaendert = aenderung(() => {
for (const f of aufteilung().flaechen) {
if (f.schicht !== 1) continue;
for (let yy = f.y; yy < f.y + f.h; yy++) for (let xx = f.x; xx < f.x + f.w; xx++) setzePixel(xx, yy, [0, 0, 0, 0]);
}
});
status(geaendert ? '2. Schicht geleert.' : 'Die 2. Schicht war schon leer.');
});
});
el('se-variante').addEventListener('change', () => setzeArmform(el('se-variante').value === 'slim'));
el('se-undo').addEventListener('click', rueckgaengig);
el('se-redo').addEventListener('click', wiederholen);
el('se-neu').addEventListener('click', () => wennVerwerfenOk(() => uebernimmDaten(vorlage(), null, 'Neue Vorlage – leg los!')));
el('se-laden').addEventListener('click', () => wennVerwerfenOk(ladeAktuellen));
el('se-oeffnen').addEventListener('click', () => wennVerwerfenOk(() => ladeDatei(null, null)));
el('se-speichern').addEventListener('click', speichere);
el('se-uebernehmen').addEventListener('click', uebernimm);
document.addEventListener('keydown', (e) => {
if (el('modal-skineditor').classList.contains('hidden')) return;
if (!el('modal-confirm').classList.contains('hidden')) return;
const a = document.activeElement;
if (a && (['INPUT', 'TEXTAREA', 'SELECT'].includes(a.tagName) && a.type !== 'checkbox')) return;
const strg = e.ctrlKey || e.metaKey;
const k = e.key.toLowerCase();
if (strg && k === 'z' && !e.shiftKey) { e.preventDefault(); rueckgaengig(); return; }
if (strg && (k === 'y' || (k === 'z' && e.shiftKey))) { e.preventDefault(); wiederholen(); return; }
if (strg || e.altKey) return;
const werkzeug = { b: 'stift', e: 'radierer', i: 'pipette', g: 'fuellen' }[k];
if (werkzeug) { e.preventDefault(); waehleWerkzeug(werkzeug); return; }
if (k === 'm') { e.preventDefault(); el('se-spiegeln').click(); return; }
if (k === 'l') { e.preventDefault(); waehleSchicht(ed.schicht ? 0 : 1); }
});
verbinde2D();
verbinde3D();
new ResizeObserver(() => passeGroessenAn()).observe(el('se-buehne'));
new ResizeObserver(() => passeGroessenAn()).observe(el('se-mini'));
// Hell/Dunkel gewechselt -> Hintergrund neu
new MutationObserver(() => planeZeichnen()).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
}
async function openSkinEditor(p = {}) {
if (POPOUT !== 'skineditor') { window.api.popoutOpen('skineditor', p); return; }
verdrahte();
show('modal-skineditor');
if (!ed.bereit) {
ed.bereit = true;
el('se-farbe').value = ed.farbe;
el('se-hex').value = ed.farbe;
zeigeZuletzt();
waehleWerkzeug('stift');
waehleSchicht(0);
aktualisiereKnoepfe();
ed.daten = vorlage();
texturGeaendert();
waehleModus('2d');
await erzeugeViewer();
texturGeaendert();
}
const variante = p.variante === 'slim' || p.variante === 'classic' ? p.variante : null;
if (p.pfad) wennVerwerfenOk(() => ladeDatei(p.pfad, variante));
else if (p.quelle === 'aktuell') wennVerwerfenOk(ladeAktuellen);
else if (variante && !ed.geaendert) await setzeArmform(variante === 'slim');
}
window.openSkinEditor = openSkinEditor;
// für Tests
window.__skinEditor = { ed, strahl, spiegelPixel, seiten, AUFTEILUNG };
})();
+1
View File
@@ -87,6 +87,7 @@ const DEFAULT_SETTINGS = {
bgSkin: '', // '' = aus, 'active' = aktives Konto, sonst Konto-UUID
bgSkinOpacity: 55, // Deckkraft des Hintergrund-Skins in %
bgSkinPose: 'walking', // Pose des 3D-Renders (falls der Pose-Dienst erreichbar ist)
bgSkinModel: 'auto', // Armform der Figur: 'auto' (vom Konto), 'classic' (Steve), 'slim' (Alex)
bgSkinCycle: false, // bei 2+ angemeldeten Konten automatisch durchwechseln
bgSkinCycleSeconds: 30, // Sekunden zwischen zwei Kontowechseln
wrapperCommand: '', // Externe Programme: Startprogramm, das Java umschließt
+69 -5
View File
@@ -10,7 +10,7 @@
}
*{box-sizing:border-box}
html,body{margin:0;height:100%;font-family:var(--font);font-size:12px;color:var(--text);background:var(--win)}
body{display:grid;grid-template-rows:auto 1fr auto;height:100vh;overflow:hidden}
body{display:grid;grid-template-rows:auto auto 1fr auto;grid-template-columns:minmax(0,1fr);height:100vh;overflow:hidden}
.hidden{display:none!important}
.mono{font-family:var(--mono);font-size:12px}
.spacer{flex:1}
@@ -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"] ::-webkit-scrollbar-thumb{background:#4a4a52;border-color:var(--area)}
/* ---------- Titelleiste (ersetzt die weiße Windows-Leiste) ---------- */
/* Farben passend zu TITELLEISTE in main.js (dort für die Fensterknöpfe) */
:root{--titel-bg:#f0f0f0;--titel-fg:#1c1c1c}
:root[data-theme="dark"]{--titel-bg:#2d2d30;--titel-fg:#e4e4e6}
.titelleiste{display:flex;align-items:center;gap:8px;height:32px;padding:0 150px 0 10px;background:var(--titel-bg);color:var(--titel-fg);-webkit-app-region:drag;user-select:none;font-size:12px}
.titel-logo{width:16px;height:16px;flex:none}
.titel-text{white-space:nowrap}
body.fenster-inaktiv .titelleiste>*{opacity:.55}
body.is-popout>.titelleiste{display:none}
/* ---------- Toolbar ---------- */
.toolbar{display:flex;align-items:center;gap:2px;padding:5px 8px;background:var(--win);border-bottom:1px solid var(--border)}
.tbtn{display:inline-flex;align-items:center;gap:7px;padding:6px 10px;border:1px solid transparent;border-radius:3px;background:none;color:var(--text);cursor:pointer;font-family:inherit;font-size:12px}
.toolbar{display:flex;align-items:center;gap:2px;padding:5px 8px;min-width:0;overflow:hidden;background:var(--win);border-bottom:1px solid var(--border)}
.tbtn{display:inline-flex;align-items:center;gap:7px;white-space:nowrap;padding:6px 10px;border:1px solid transparent;border-radius:3px;background:none;color:var(--text);cursor:pointer;font-family:inherit;font-size:12px}
.tbtn:hover{background:#e2e6ec;border-color:var(--border2)}
.tbtn:active{background:#d7dde5}
.tbtn svg{width:18px;height:18px;color:#4b5563}
@@ -95,7 +105,7 @@ body{display:grid;grid-template-rows:auto 1fr auto;height:100vh;overflow:hidden}
.search-wrap input:focus{outline:none;border-color:var(--accent)}
/* ---------- Arbeitsbereich ---------- */
.workspace{display:grid;grid-template-columns:1fr 172px;overflow:hidden;background:var(--area)}
.workspace{display:grid;grid-template-columns:minmax(0,1fr) 172px;overflow:hidden;min-width:0;background:var(--area)}
.list-area{position:relative;overflow:hidden;min-height:0}
/* Skin-Hintergrund (wie MultiMCs Katze) – liegt hinter den Instanzen */
.list-area::after{
@@ -160,7 +170,7 @@ body.file-drop-active::after{
.empty p{max-width:340px;margin:0 0 8px}
/* ---------- Statusleiste (zwei Zeilen) ---------- */
.statusbar{display:flex;flex-direction:column;background:var(--win);border-top:1px solid var(--border)}
.statusbar{display:flex;flex-direction:column;min-width:0;background:var(--win);border-top:1px solid var(--border)}
.st-row{display:flex;justify-content:space-between;padding:3px 10px;font-size:11px;color:#333}
.st-row2{background:#e4e4e4;border-top:1px solid var(--border2);color:#444}
.auth-warn{
@@ -640,3 +650,57 @@ img.account-av{object-fit:cover;image-rendering:auto}
::-webkit-scrollbar{width:14px;height:14px}
::-webkit-scrollbar-thumb{background:#c1c1c1;border:3px solid var(--area);border-radius:7px}
::-webkit-scrollbar-thumb:hover{background:#a8a8a8}
/* ===================== Skin-Editor ===================== */
#modal-skineditor .se-karte{width:1100px;max-width:calc(100vw - 32px);height:760px;max-height:calc(100vh - 40px)}
.se-modus{display:inline-flex;border:1px solid var(--border);border-radius:4px;overflow:hidden;background:var(--bar)}
.se-modus-btn{padding:5px 14px;border:none;background:none;color:var(--dim);font:inherit;font-size:12px;cursor:pointer}
.se-modus-btn+.se-modus-btn{border-left:1px solid var(--border)}
.se-modus-btn:hover{color:var(--text)}
.se-modus-btn.on{background:var(--accent);color:#fff;font-weight:600}
.se-body{display:grid;grid-template-columns:200px minmax(0,1fr) 270px;gap:14px;flex:1 1 auto;min-height:0;overflow:hidden;padding:12px 14px}
.se-werkzeuge{display:flex;flex-direction:column;gap:14px;overflow:auto;min-height:0;padding-right:2px}
.se-gruppe{display:flex;flex-direction:column;gap:6px}
.se-titel{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--dim)}
.se-tools{display:grid;grid-template-columns:1fr 1fr;gap:4px}
.se-tool{display:flex;flex-direction:column;align-items:center;gap:3px;padding:7px 4px;border:1px solid var(--border);border-radius:4px;background:var(--bar);color:var(--text);font:inherit;font-size:11px;cursor:pointer}
.se-tool svg{width:18px;height:18px}
.se-tool:hover:not(:disabled){border-color:var(--sel-br)}
.se-tool.on{background:var(--sel);border-color:var(--sel-br);font-weight:600}
.se-tool:disabled{opacity:.4;cursor:default}
.se-farbzeile{display:flex;gap:6px;align-items:center}
.se-farbzeile input[type=color]{width:42px;height:30px;padding:0 2px;border:1px solid var(--border);border-radius:4px;background:var(--bar);cursor:pointer}
.se-farbzeile input[type=text]{flex:1;min-width:0;height:30px;padding:0 8px;border:1px solid var(--border);border-radius:4px;font-family:var(--mono);font-size:12px;background:#fff;color:var(--text)}
:root[data-theme="dark"] .se-farbzeile input[type=text]{background:#1e1e1e}
.se-zuletzt{display:grid;grid-template-columns:repeat(8,1fr);gap:3px}
.se-farbfeld{aspect-ratio:1;border:1px solid rgba(0,0,0,.25);border-radius:3px;cursor:pointer;padding:0}
.se-farbfeld:hover{transform:scale(1.12)}
.se-farbfeld.on{outline:2px solid var(--sel-br);outline-offset:1px}
.se-ebenen{display:grid;grid-template-columns:1fr 1fr;border:1px solid var(--border);border-radius:4px;overflow:hidden}
.se-ebene{padding:6px 4px;border:none;background:var(--bar);color:var(--dim);font:inherit;font-size:12px;cursor:pointer}
.se-ebene+.se-ebene{border-left:1px solid var(--border)}
.se-ebene.on{background:var(--sel);color:var(--text);font-weight:600}
.se-check{display:flex;align-items:center;gap:6px;cursor:pointer}
.se-check input{margin:0}
.se-check:has(input:disabled){opacity:.5;cursor:default}
.se-feld{display:flex;flex-direction:column;gap:3px}
.se-feld select{height:28px;border:1px solid var(--border);border-radius:3px;background:#fff;color:var(--text);font:inherit}
:root[data-theme="dark"] .se-feld select{background:#1e1e1e}
.se-undo{display:grid;grid-template-columns:1fr 1fr;gap:4px}
.se-undo .btn{justify-content:center}
.se-gruppe>.btn-mini{align-self:flex-start}
.se-mitte{display:flex;flex-direction:column;gap:6px;min-width:0;min-height:0}
.se-buehne{flex:1 1 auto;min-height:0;position:relative;border:1px solid var(--border);border-radius:6px;background:var(--area);overflow:hidden}
.se-info{min-height:16px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.se-seite{display:flex;flex-direction:column;gap:6px;min-height:0}
.se-mini{position:relative;height:320px;border:1px solid var(--border);border-radius:6px;background:var(--area);overflow:hidden}
.se-tipp{margin:4px 0 0;line-height:1.45}
.se-ansicht{position:absolute;inset:0;display:flex;align-items:center;justify-content:center}
.se-ansicht canvas{display:block}
.se-2d canvas{cursor:crosshair;border-radius:2px}
.se-3d canvas{cursor:grab}
.se-3d canvas:active{cursor:grabbing}
.se-2d[data-werkzeug="pipette"] canvas{cursor:copy}
.se-3d-leer{position:absolute}
.se-fuss{align-items:center}
.se-status{flex:1;min-width:0;margin-left:6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}