441 lines
18 KiB
C#
441 lines
18 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}";
|
||
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);
|
||
}
|
||
}
|