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 GitManagerSetup; public partial class MainWindow : Window { private readonly SetupMode _mode; private int _page; private bool _busy; private bool _done; private string? _installedExe; private const int PageNumOptions = 1; private const int PageNumProgress = 2; public MainWindow(SetupMode mode) { InitializeComponent(); _mode = mode; VersionText.Text = $"Version {Installer.Version}"; 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 ? $"Git Manager GUI {Installer.Version} ist bereits installiert und wird neu installiert. Zugangsdaten und Einstellungen bleiben erhalten." : $"Die vorhandene Installation{(Installer.InstalledVersion is { } v ? $" (Version {v})" : "")} wird durch Version {Installer.Version} ersetzt. Zugangsdaten und Einstellungen bleiben erhalten."; } if (Installer.IsRunning(installed)) RunningHint.Visibility = Visibility.Visible; switch (mode) { case SetupMode.Uninstall: Title = WindowTitle.Text = "Git Manager GUI deinstallieren"; Steps.Visibility = Visibility.Collapsed; ShowOnly(PageUninstall); NextButton.Content = "Deinstallieren"; BackButton.Visibility = Visibility.Collapsed; LoadDataInfo(); break; case SetupMode.Update: // aus der App heraus: ohne Rückfragen direkt aktualisieren und danach wieder starten Title = WindowTitle.Text = "Git Manager GUI wird aktualisiert"; Steps.Visibility = Visibility.Collapsed; Loaded += async (_, _) => await RunInstall(DirBox.Text, DesktopLinkBox.IsChecked == true, update: true); ShowOnly(PageProgress); ProgressTitle.Text = "Git Manager GUI wird aktualisiert …"; BackButton.Visibility = Visibility.Hidden; break; default: 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, PageOptions, 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 dunklen) Seitenleiste – Cyan wie in der App private static readonly Brush StepDoneFill = Frozen(Color.FromRgb(0x58, 0xD5, 0xFF)); private static readonly Brush StepDoneGlyph = Frozen(Color.FromRgb(0x04, 0x10, 0x1F)); private static readonly Brush StepCurrentFill = Brushes.White; private static readonly Brush StepCurrentText = Frozen(Color.FromRgb(0x13, 0x30, 0x5C)); private static readonly Brush StepUpcoming = Frozen(Color.FromRgb(0x70, 0x83, 0xA4)); private static readonly Brush StepUpcomingStroke = Frozen(Color.FromRgb(0x3A, 0x4E, 0x72)); private static readonly Brush StepDoneText = Frozen(Color.FromRgb(0xAE, 0xBD, 0xD8)); private static readonly Brush StepLineIdle = Frozen(Color.FromRgb(0x24, 0x36, 0x58)); 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 }; var nums = new[] { Num0, Num1, Num2 }; var labels = new[] { Label0, Label1, Label2 }; var lines = new[] { Line0, Line1 }; var iconFont = (FontFamily)FindResource("IconFont"); var uiFont = (FontFamily)FindResource("UiFont"); for (int i = 0; i < dots.Length; i++) { 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 ? "" : (i + 1).ToString(); nums[i].FontFamily = done ? iconFont : uiFont; nums[i].FontSize = done ? 11 : 12; nums[i].Foreground = done ? StepDoneGlyph : 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, PageNumOptions => PageOptions, _ => PageProgress }); UpdateSteps(page); BackButton.Visibility = page == PageNumOptions ? Visibility.Visible : Visibility.Hidden; NextButton.Content = page switch { PageNumOptions => "Installieren", PageNumProgress => "Fertigstellen", _ => "Weiter", }; } 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(PageNumOptions); break; case PageNumOptions: 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:\\Programme\\Git Manager GUI.", "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.", "OK"); return; } await RunInstall(dir, DesktopLinkBox.IsChecked == true, update: false); break; case PageNumProgress: // Start im Hintergrund: Process.Start kann blockieren, solange Windows // die EXE prüft – das Fenster darf dabei nie einfrieren if (LaunchBox.IsChecked == true) { _busy = true; NextButton.IsEnabled = false; NextButton.Content = "Wird gestartet …"; await Task.Run(StartApp); _busy = false; } Close(); break; } } 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 "Git Manager GUI" anhängen DirBox.Text = Path.GetFileName(chosen.TrimEnd('\\')).Equals(Installer.AppName, StringComparison.OrdinalIgnoreCase) ? chosen : Path.Combine(chosen, Installer.AppName); } // ---------- 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", $"Git Manager GUI {Installer.Version} wird gestartet …"); StartApp(); _busy = false; var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1.5) }; timer.Tick += (_, _) => { timer.Stop(); Close(); }; timer.Start(); return; } ShowSuccess("Alles bereit!", "Git Manager GUI ist installiert. Du findest es im Startmenü" + (desktop ? " und auf dem Desktop." : ".")); 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; if (!Directory.Exists(root)) { DataInfo.Text = "Es wurden keine App-Daten gefunden."; 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 Git Manager GUI später neu installierst."; } 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 = "Git Manager GUI 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("Git Manager GUI wurde entfernt", removeData ? "Programm, Zugangsdaten und Einstellungen wurden entfernt (die Daten liegen im Papierkorb)." : "Deine Zugangsdaten und Einstellungen liegen weiterhin in " + Installer.DataDir + " – falls du Git Manager GUI 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?", "Git Manager GUI 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); } }