Files
AeroMc-Launcher/installer/MainWindow.xaml.cs
T
2026-10-02 06:22:54 +00:00

441 lines
18 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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}";
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;
/// <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)
{
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<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 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);
}
}