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}"; LicenseText.Text = 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; } } // ---------- 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) { MessageBox.Show(this, "Bitte einen gültigen Ordner angeben, z. B. C:\\Spiele\\AeroMC.", "AeroMC Setup"); return; } if (dir.TrimEnd('\\', '/').Length > Installer.MaxDirLength) { MessageBox.Show(this, $"Der Ordnerpfad ist zu lang ({dir.TrimEnd('\\', '/').Length} Zeichen). Windows erlaubt hier höchstens " + $"{Installer.MaxDirLength} Zeichen – bitte einen kürzeren Ordner wählen, z. B. C:\\Spiele\\AeroMC.", "AeroMC Setup"); 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 void Cancel_Click(object sender, RoutedEventArgs e) { if (_busy) return; if (!_done && _mode == SetupMode.Install && _page > 0 && MessageBox.Show(this, "Installation wirklich abbrechen?", "AeroMC Setup", MessageBoxButton.YesNo, MessageBoxImage.Question) != MessageBoxResult.Yes) return; Close(); } protected override void OnClosing(System.ComponentModel.CancelEventArgs e) { if (_busy) e.Cancel = true; // während der Installation nicht schließen base.OnClosing(e); } }