using System.Diagnostics; using System.IO; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Threading; namespace AeroMCSetup; public partial class MainWindow : Window { private readonly SetupMode _mode; private int _page; private bool _busy; private bool _done; private string? _installedExe; // andere Launcher für den optionalen Import (leer → Import-Schritt entfällt) private List _found = []; private bool HasImport => _found.Count > 0; private const int PageNumImport = 3; private const int PageNumProgress = 4; public MainWindow(SetupMode mode) { InitializeComponent(); _mode = mode; VersionText.Text = $"Version {Installer.Version}"; ZeigeLizenz(Installer.LicenseText); var installed = Installer.InstalledDir; DirBox.Text = installed ?? Installer.DefaultDir; DesktopLinkBox.IsChecked = installed is null || Installer.DesktopLinkExists; if (installed is not null) { UpdateHint.Visibility = Visibility.Visible; UpdateHintText.Text = Installer.InstalledVersion == Installer.Version ? $"AeroMC {Installer.Version} ist bereits installiert und wird neu installiert. Deine Instanzen, Welten und Einstellungen bleiben erhalten." : $"Die vorhandene Installation{(Installer.InstalledVersion is { } v ? $" (Version {v})" : "")} wird durch Version {Installer.Version} ersetzt. Deine Instanzen, Welten und Einstellungen bleiben erhalten."; } if (Installer.IsRunning(installed)) RunningHint.Visibility = Visibility.Visible; switch (mode) { case SetupMode.Uninstall: Title = WindowTitle.Text = "AeroMC deinstallieren"; Steps.Visibility = Visibility.Collapsed; ShowOnly(PageUninstall); NextButton.Content = "Deinstallieren"; BackButton.Visibility = Visibility.Collapsed; LoadDataInfo(); break; case SetupMode.Update: // aus dem Launcher heraus: ohne Rückfragen direkt aktualisieren und danach wieder starten Title = WindowTitle.Text = "AeroMC wird aktualisiert"; Steps.Visibility = Visibility.Collapsed; Loaded += async (_, _) => await RunInstall(DirBox.Text, DesktopLinkBox.IsChecked == true, update: true); ShowOnly(PageProgress); ProgressTitle.Text = "AeroMC wird aktualisiert …"; BackButton.Visibility = Visibility.Hidden; break; default: SetupImportPage(); ShowPage(0); break; } } // ---------- Lizenztext ---------- /// /// LICENSE ist für die Textansicht auf ~80 Zeichen umbrochen und eingerückt. Hier wird das /// wieder zu fließendem Text zusammengesetzt, damit der Umbruch zur Fensterbreite passt: /// Leerzeilen trennen Abschnitte, "1. …" sind Überschriften, "- …" Aufzählungspunkte /// (Folgezeilen gehören zum Punkt), alles andere ist Absatztext. /// private void ZeigeLizenz(string text) { LicensePanel.Children.Clear(); var abschnitte = new List>(); var aktuell = new List(); foreach (var zeile in text.Replace("\r", "").Split('\n')) { if (zeile.Trim().Length == 0) { if (aktuell.Count > 0) { abschnitte.Add(aktuell); aktuell = []; } } else aktuell.Add(zeile); } if (aktuell.Count > 0) abschnitte.Add(aktuell); for (int a = 0; a < abschnitte.Count; a++) { var abschnitt = abschnitte[a]; int i = 0; if (a == 0) { LicensePanel.Children.Add(LizenzText(abschnitt[0].Trim(), 14, FontWeights.SemiBold, "TextBrush", 0)); i = 1; } else if (System.Text.RegularExpressions.Regex.IsMatch(abschnitt[0], @"^\s*\d+\.\s")) { LicensePanel.Children.Add(LizenzText(abschnitt[0].Trim(), 13, FontWeights.SemiBold, "TextBrush", 14)); i = 1; } // nach einer Überschrift eng anschließen, sonst Abstand zum vorigen Abschnitt double absatzAbstand = i == 1 && a > 0 ? 4 : 10; var absatz = new System.Text.StringBuilder(); string? punkt = null; void AbsatzFertig() { if (absatz.Length == 0) return; LicensePanel.Children.Add(LizenzText(absatz.ToString(), 12.5, FontWeights.Normal, "TextSecondaryBrush", absatzAbstand)); absatz.Clear(); } void PunktFertig() { if (punkt is null) return; LicensePanel.Children.Add(LizenzPunkt(punkt)); punkt = null; } for (; i < abschnitt.Count; i++) { var t = abschnitt[i].Trim(); if (t.StartsWith("- ")) { AbsatzFertig(); PunktFertig(); punkt = t.Substring(2).Trim(); } else if (punkt is not null) punkt += " " + t; else absatz.Append(absatz.Length > 0 ? " " : "").Append(t); } AbsatzFertig(); PunktFertig(); } } private TextBlock LizenzText(string text, double groesse, FontWeight gewicht, string farbe, double abstandOben) { var tb = new TextBlock { Text = text, TextWrapping = TextWrapping.Wrap, FontSize = groesse, FontWeight = gewicht, LineHeight = groesse + 6, Margin = new Thickness(0, abstandOben, 0, 0), }; tb.SetResourceReference(TextBlock.ForegroundProperty, farbe); return tb; } // Aufzählungspunkt mit hängendem Einzug (umbrochene Zeilen beginnen unter dem Text, nicht unter dem Punkt) private Grid LizenzPunkt(string text) { var grid = new Grid { Margin = new Thickness(4, 4, 0, 0) }; grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(16) }); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); var punkt = LizenzText("•", 12.5, FontWeights.Normal, "AccentBrush", 0); var inhalt = LizenzText(text, 12.5, FontWeights.Normal, "TextSecondaryBrush", 0); Grid.SetColumn(inhalt, 1); grid.Children.Add(punkt); grid.Children.Add(inhalt); return grid; } // ---------- Seiten ---------- private UIElement? _shownPage; /// Seite wechseln – mit kurzem Einblenden von rechts. private void ShowOnly(UIElement page) { foreach (var p in new UIElement[] { PageWelcome, PageLicense, PageOptions, PageImport, PageProgress, PageUninstall }) p.Visibility = p == page ? Visibility.Visible : Visibility.Collapsed; if (page == _shownPage) return; _shownPage = page; var ease = new CubicEase { EasingMode = EasingMode.EaseOut }; var slide = new TranslateTransform(18, 0); page.RenderTransform = slide; slide.BeginAnimation(TranslateTransform.XProperty, new DoubleAnimation(18, 0, TimeSpan.FromMilliseconds(260)) { EasingFunction = ease }); page.BeginAnimation(OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(220))); } // Farben der Schritt-Anzeige in der (immer dunkelblauen) Seitenleiste private static readonly Brush StepDoneFill = Frozen(Color.FromRgb(0x4A, 0x86, 0xEA)); private static readonly Brush StepCurrentFill = Brushes.White; private static readonly Brush StepCurrentText = Frozen(Color.FromRgb(0x1C, 0x4E, 0xB8)); private static readonly Brush StepUpcoming = Frozen(Color.FromRgb(0x7F, 0x95, 0xC2)); private static readonly Brush StepUpcomingStroke = Frozen(Color.FromRgb(0x46, 0x5E, 0x92)); private static readonly Brush StepDoneText = Frozen(Color.FromRgb(0xB9, 0xC8, 0xE8)); private static readonly Brush StepLineIdle = Frozen(Color.FromRgb(0x2E, 0x45, 0x78)); private static Brush Frozen(Color c) { var b = new SolidColorBrush(c); b.Freeze(); return b; } private void UpdateSteps(int page) { var dots = new[] { Dot0, Dot1, Dot2, Dot3, Dot4 }; var nums = new[] { Num0, Num1, Num2, Num3, Num4 }; var labels = new[] { Label0, Label1, Label2, Label3, Label4 }; var lines = new[] { Line0, Line1, Line2, Line3 }; var iconFont = (FontFamily)FindResource("IconFont"); var uiFont = (FontFamily)FindResource("UiFont"); // ohne gefundene Launcher fällt der Import-Schritt weg → Nummern rücken auf ImportStepRow.Visibility = Line3.Visibility = HasImport ? Visibility.Visible : Visibility.Collapsed; int number = 0; for (int i = 0; i < dots.Length; i++) { if (i == PageNumImport && !HasImport) continue; number++; bool done = i < page || (_done && i == page); bool current = i == page && !done; dots[i].Background = done ? StepDoneFill : current ? StepCurrentFill : Brushes.Transparent; dots[i].BorderBrush = done ? StepDoneFill : current ? StepCurrentFill : StepUpcomingStroke; nums[i].Text = done ? "" : number.ToString(); nums[i].FontFamily = done ? iconFont : uiFont; nums[i].FontSize = done ? 11 : 12; nums[i].Foreground = done ? Brushes.White : current ? StepCurrentText : StepUpcoming; labels[i].Foreground = current ? Brushes.White : done ? StepDoneText : StepUpcoming; labels[i].FontWeight = current ? FontWeights.SemiBold : FontWeights.Normal; } for (int i = 0; i < lines.Length; i++) lines[i].Background = i < page ? StepDoneFill : StepLineIdle; } private void ShowPage(int page) { _page = page; ShowOnly(page switch { 0 => PageWelcome, 1 => PageLicense, 2 => PageOptions, PageNumImport => PageImport, _ => PageProgress }); UpdateSteps(page); BackButton.Visibility = page is 1 or 2 or PageNumImport ? Visibility.Visible : Visibility.Hidden; NextButton.Content = page switch { 2 => HasImport ? "Weiter" : "Installieren", PageNumImport => "Installieren", PageNumProgress => "Fertigstellen", _ => "Weiter", }; NextButton.IsEnabled = page != 1 || AcceptLicense.IsChecked == true; } private void AcceptLicense_Click(object sender, RoutedEventArgs e) => NextButton.IsEnabled = AcceptLicense.IsChecked == true; private void Back_Click(object sender, RoutedEventArgs e) { if (!_busy && _page > 0) ShowPage(_page - 1); } private async void Next_Click(object sender, RoutedEventArgs e) { if (_busy) return; if (_mode == SetupMode.Uninstall) { if (_done) { Close(); return; } await RunUninstall(); return; } if (_mode == SetupMode.Update) { Close(); // nur nach einem Fehler erreichbar return; } switch (_page) { case 0: ShowPage(1); break; case 1 when AcceptLicense.IsChecked == true: ShowPage(2); break; case 2: var dir = DirBox.Text.Trim(); if (!IsFullPath(dir) || dir.IndexOfAny(Path.GetInvalidPathChars()) >= 0) { await Frage("Ungültiger Ordner", "Bitte einen vollständigen Ordner angeben, z. B. C:\\Spiele\\AeroMC.", "OK"); return; } if (dir.TrimEnd('\\', '/').Length > Installer.MaxDirLength) { await Frage("Ordnerpfad zu lang", $"Der Pfad hat {dir.TrimEnd('\\', '/').Length} Zeichen, Windows erlaubt hier höchstens {Installer.MaxDirLength}. " + "Bitte einen kürzeren Ordner wählen, z. B. C:\\Spiele\\AeroMC.", "OK"); return; } if (HasImport) ShowPage(PageNumImport); else await RunInstall(dir, DesktopLinkBox.IsChecked == true, update: false); break; case PageNumImport: await RunInstall(DirBox.Text.Trim(), DesktopLinkBox.IsChecked == true, update: false); break; case PageNumProgress: if (LaunchBox.IsChecked == true) StartApp(); Close(); break; } } // ---------- Import aus anderen Launchern ---------- private void SetupImportPage() { try { _found = LauncherImport.Detect(); } catch { _found = []; } ImportList.Children.Clear(); for (int i = 0; i < _found.Count; i++) { var f = _found[i]; var text = new StackPanel { Margin = new Thickness(4, 0, 0, 0) }; text.Children.Add(new TextBlock { Text = f.Name, FontWeight = FontWeights.SemiBold }); var detail = new TextBlock { Text = $"{f.Count} {f.Unit} gefunden · {f.Path}", FontSize = 12, Margin = new Thickness(0, 2, 0, 0), TextTrimming = TextTrimming.CharacterEllipsis, ToolTip = f.Path, }; detail.SetResourceReference(TextBlock.ForegroundProperty, "TextSecondaryBrush"); text.Children.Add(detail); var box = new CheckBox { Content = text, Tag = f, Margin = new Thickness(0, 10, 0, 10), IsChecked = false }; if (i > 0) { var line = new Border { Height = 1 }; line.SetResourceReference(Border.BackgroundProperty, "ControlStrokeBrush"); ImportList.Children.Add(line); } ImportList.Children.Add(box); } } private List SelectedImports() => ImportList.Children.OfType() .Where(c => c.IsChecked == true && c.Tag is FoundLauncher) .Select(c => (FoundLauncher)c.Tag) .ToList(); private void ChangeDir_Click(object sender, RoutedEventArgs e) { using var dlg = new System.Windows.Forms.FolderBrowserDialog { Description = "Installationsordner wählen", SelectedPath = Directory.Exists(DirBox.Text) ? DirBox.Text : Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), ShowNewFolderButton = true, }; if (dlg.ShowDialog() != System.Windows.Forms.DialogResult.OK) return; var chosen = dlg.SelectedPath; // wie bei üblichen Installern: Unterordner "AeroMC" anhängen DirBox.Text = Path.GetFileName(chosen.TrimEnd('\\')).Equals("AeroMC", StringComparison.OrdinalIgnoreCase) ? chosen : Path.Combine(chosen, "AeroMC"); } // ---------- Installation ---------- private async Task RunInstall(string dir, bool desktop, bool update) { _busy = true; if (!update) ShowPage(PageNumProgress); SetButtons(false); var progress = new Progress<(double Value, string Text)>(p => SetProgress(p.Value, p.Text)); try { await Task.Run(() => Installer.Install(dir, desktop, progress, waitForExit: update)); _installedExe = Path.Combine(dir, Installer.ExeName); _done = true; if (update) { ShowSuccess("Update abgeschlossen", $"AeroMC {Installer.Version} wird gestartet …"); StartApp(); _busy = false; var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1.5) }; timer.Tick += (_, _) => { timer.Stop(); Close(); }; timer.Start(); return; } // ausgewählte Launcher für den ersten Start vormerken (Fehler hier sollen die Installation nicht kippen) var selected = SelectedImports(); try { LauncherImport.WritePending(selected); } catch { selected = []; } ShowSuccess("Alles bereit!", "AeroMC ist installiert. Du findest es im Startmenü" + (desktop ? " und auf dem Desktop." : ".")); if (selected.Count > 0) ProgressText.Text += $" Beim ersten Start bietet AeroMC dir die Instanzen aus {string.Join(" und ", selected.Select(f => f.Name))} zum Übernehmen an."; DonePanel.Visibility = Visibility.Visible; ShowPage(PageNumProgress); } catch (Exception ex) { ProgressTitle.Text = update ? "Update fehlgeschlagen" : "Installation fehlgeschlagen"; ProgressBarPanel.Visibility = Visibility.Collapsed; ProgressText.Text = ex.Message; ProgressText.Foreground = (Brush)FindResource("DangerBrush"); NextButton.Content = "Schließen"; _installedExe = null; } _busy = false; NextButton.IsEnabled = true; CloseButton.IsEnabled = true; BackButton.Visibility = Visibility.Hidden; CancelButton.Visibility = Visibility.Collapsed; } /// Fortschritt ausblenden und den Haken mit kurzem "Aufploppen" zeigen. private void ShowSuccess(string title, string text) { ProgressBarPanel.Visibility = Visibility.Collapsed; ProgressTitle.Text = title; ProgressText.Text = text; SuccessIcon.Visibility = Visibility.Visible; var pop = new DoubleAnimation(0.4, 1, TimeSpan.FromMilliseconds(420)) { EasingFunction = new BackEase { EasingMode = EasingMode.EaseOut, Amplitude = 0.5 } }; SuccessScale.BeginAnimation(ScaleTransform.ScaleXProperty, pop); SuccessScale.BeginAnimation(ScaleTransform.ScaleYProperty, pop); SuccessIcon.BeginAnimation(OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(200))); } private void StartApp() { if (_installedExe is null || !File.Exists(_installedExe)) return; try { Process.Start(new ProcessStartInfo(_installedExe) { UseShellExecute = true, WorkingDirectory = Path.GetDirectoryName(_installedExe)!, }); } catch { } } // ---------- Deinstallation ---------- private async void LoadDataInfo() { var root = Installer.DataDir; var custom = Installer.CustomInstancesDir; var customNote = custom is null ? "" : $" Instanzen in deinem eigenen Ordner {custom} werden nie angefasst."; if (!Directory.Exists(root)) { DataInfo.Text = "Es wurden keine Launcher-Daten gefunden." + customNote; RemoveDataBox.IsEnabled = false; return; } DataInfo.Text = $"Sie liegen in {root}. Größe wird berechnet …"; var size = await Task.Run(Installer.DataSize); DataInfo.Text = $"Sie liegen in {root} ({Installer.FormatSize(size)}) und stehen wieder bereit, falls du AeroMC später neu installierst." + customNote; } private void RemoveDataBox_Click(object sender, RoutedEventArgs e) => RemoveDataWarning.Visibility = RemoveDataBox.IsChecked == true ? Visibility.Visible : Visibility.Collapsed; private async Task RunUninstall() { _busy = true; bool removeData = RemoveDataBox.IsChecked == true; ShowOnly(PageProgress); ProgressTitle.Text = "AeroMC wird entfernt …"; LaunchBox.Visibility = Visibility.Collapsed; SetButtons(false); var progress = new Progress<(double Value, string Text)>(p => SetProgress(p.Value, p.Text)); try { await Task.Run(() => Installer.Uninstall(removeData, progress)); ShowSuccess("AeroMC wurde entfernt", removeData ? "AeroMC, deine Instanzen, Konten und Einstellungen wurden entfernt (die Daten liegen im Papierkorb)." : "Deine Instanzen und Einstellungen liegen weiterhin in " + Installer.DataDir + " – falls du AeroMC später wieder installierst."); } catch (Exception ex) { ProgressTitle.Text = "Deinstallation fehlgeschlagen"; ProgressBarPanel.Visibility = Visibility.Collapsed; ProgressText.Text = ex.Message; } _done = true; _busy = false; CloseButton.IsEnabled = true; NextButton.Content = "Schließen"; NextButton.IsEnabled = true; CancelButton.Visibility = Visibility.Collapsed; } // ---------- Helfer ---------- private void SetButtons(bool enabled) { NextButton.IsEnabled = enabled; BackButton.IsEnabled = enabled; CancelButton.IsEnabled = enabled; CloseButton.IsEnabled = enabled; } private void SetProgress(double value, string text) { if (_done) return; ProgressText.Text = text; PercentText.Text = $"{(int)Math.Round(value * 100)} %"; _progress = value; AnimateProgress(); } private double _progress; private void AnimateProgress() { var target = Math.Max(0, ProgressTrack.ActualWidth * _progress); ProgressFill.BeginAnimation(WidthProperty, new DoubleAnimation(target, TimeSpan.FromMilliseconds(250)) { EasingFunction = new CubicEase() }); } // Kommt der erste Wert, bevor die Seite fertig angeordnet ist (Breite 0), nachträglich richtig setzen private void ProgressTrack_SizeChanged(object sender, SizeChangedEventArgs e) => AnimateProgress(); private static bool IsFullPath(string p) => p.Length >= 3 && char.IsLetter(p[0]) && p[1] == ':' && (p[2] == '\\' || p[2] == '/'); private void Minimize_Click(object sender, RoutedEventArgs e) => WindowState = WindowState.Minimized; private async void Cancel_Click(object sender, RoutedEventArgs e) { if (_busy || _dialog is not null) return; if (!_done && _mode == SetupMode.Install && _page > 0) { bool abbrechen = await Frage("Installation abbrechen?", "AeroMC wird nicht installiert. Du kannst das Setup später jederzeit erneut starten.", "Ja, abbrechen", "Weiter installieren", "\uE897"); if (!abbrechen) return; } Close(); } // ---------- eigene Rückfragen im Setup-Look ---------- private TaskCompletionSource? _dialog; /// /// Zeigt eine Rückfrage über dem Fenster. Mit : zwei Knöpfe – /// der hervorgehobene ist der sichere Weg zurück (liefert false), der andere bestätigt (true). /// Ohne: nur ein Hinweis mit einem Knopf (liefert true). /// private Task Frage(string titel, string text, string bestaetigen, string? zurueck = null, string symbol = "\uE7BA") { _dialog?.TrySetResult(false); _dialog = new TaskCompletionSource(); DialogTitle.Text = titel; DialogText.Text = text; DialogIcon.Text = symbol; if (zurueck is null) { DialogSecondary.Visibility = Visibility.Collapsed; DialogPrimary.Content = bestaetigen; } else { DialogSecondary.Visibility = Visibility.Visible; DialogSecondary.Content = bestaetigen; DialogPrimary.Content = zurueck; } DialogOverlay.Visibility = Visibility.Visible; var ein = new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(160)); DialogOverlay.BeginAnimation(OpacityProperty, ein); var groesse = new DoubleAnimation(0.94, 1, TimeSpan.FromMilliseconds(220)) { EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } }; DialogScale.BeginAnimation(ScaleTransform.ScaleXProperty, groesse); DialogScale.BeginAnimation(ScaleTransform.ScaleYProperty, groesse); DialogPrimary.Focus(); return _dialog.Task; } private void SchliesseFrage(bool ergebnis) { if (_dialog is null) return; var tcs = _dialog; _dialog = null; DialogOverlay.Visibility = Visibility.Collapsed; tcs.TrySetResult(ergebnis); } // hervorgehobener Knopf: bei Rückfragen "zurück" (false), beim reinen Hinweis "OK" (true) private void DialogPrimary_Click(object sender, RoutedEventArgs e) => SchliesseFrage(DialogSecondary.Visibility != Visibility.Visible); private void DialogSecondary_Click(object sender, RoutedEventArgs e) => SchliesseFrage(true); protected override void OnPreviewKeyDown(System.Windows.Input.KeyEventArgs e) { // solange die Rückfrage offen ist: Enter = hervorgehobener Knopf, Esc = zurück if (_dialog is not null) { if (e.Key == System.Windows.Input.Key.Escape) { SchliesseFrage(DialogSecondary.Visibility != Visibility.Visible); e.Handled = true; return; } if (e.Key == System.Windows.Input.Key.Enter) { DialogPrimary_Click(this, new RoutedEventArgs()); e.Handled = true; return; } } base.OnPreviewKeyDown(e); } protected override void OnClosing(System.ComponentModel.CancelEventArgs e) { if (_busy) e.Cancel = true; // während der Installation nicht schließen _dialog?.TrySetResult(false); base.OnClosing(e); } }