612 lines
25 KiB
C#
612 lines
25 KiB
C#
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<FoundLauncher> _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 ----------
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
private void ZeigeLizenz(string text)
|
||
{
|
||
LicensePanel.Children.Clear();
|
||
var abschnitte = new List<List<string>>();
|
||
var aktuell = new List<string>();
|
||
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;
|
||
|
||
/// <summary>Seite wechseln – mit kurzem Einblenden von rechts.</summary>
|
||
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<FoundLauncher> SelectedImports() =>
|
||
ImportList.Children.OfType<CheckBox>()
|
||
.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;
|
||
}
|
||
|
||
/// <summary>Fortschritt ausblenden und den Haken mit kurzem "Aufploppen" zeigen.</summary>
|
||
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<bool>? _dialog;
|
||
|
||
/// <summary>
|
||
/// Zeigt eine Rückfrage über dem Fenster. Mit <paramref name="zurueck"/>: 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).
|
||
/// </summary>
|
||
private Task<bool> Frage(string titel, string text, string bestaetigen, string? zurueck = null, string symbol = "\uE7BA")
|
||
{
|
||
_dialog?.TrySetResult(false);
|
||
_dialog = new TaskCompletionSource<bool>();
|
||
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);
|
||
}
|
||
}
|