Setup-Assistent und Update-Abfrage bei jedem Start

Die heruntergeladene Datei ist ein Setup: Sprache, Ordner (nur ohne Admin-Rechte beschreibbar),
Verknuepfungen, Fortschritt, Fertig-Seite; Ordner steht in der Registry. Deinstallieren loescht im
gewaehlten Ordner nur die eigenen Dateien. Beim Start fragt der Launcher nach neuen Updates
(Launcher und Mod).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 20:01:27 +01:00
co-authored by Claude Opus 5.5
parent 884492f665
commit 5fd162fecb
10 changed files with 461 additions and 109 deletions
+307
View File
@@ -0,0 +1,307 @@
using System.Drawing.Drawing2D;
using System.Runtime.InteropServices;
namespace BSO.Launcher;
/// <summary>
/// Setup-Assistent der heruntergeladenen BSO-Launcher-Setup.exe: Willkommen (mit Sprachwahl) -> Ordner und Verknuepfungen
/// -> Installieren -> Fertig. Ist dieselbe oder eine neuere Version schon installiert, bietet er Starten an (gleiche Version:
/// auch Neu installieren). Eigenes dunkles Fenster im Stil des Launchers.
/// </summary>
public sealed class SetupForm : Form
{
static readonly Color Back = Color.FromArgb(0x10, 0x13, 0x1a), Field = Color.FromArgb(0x1a, 0x1f, 0x29), Line = Color.FromArgb(0x26, 0x2c, 0x38),
Bright = Color.FromArgb(0xe8, 0xec, 0xf2), Muted = Color.FromArgb(0x8b, 0x95, 0xa5), Accent = Color.FromArgb(0x1f, 0x6f, 0xe5),
Bad = Color.FromArgb(0xff, 0x8a, 0x8d);
const int Header = 96;
readonly string version = SelfUpdate.CurrentVersion;
readonly string? installed = Installer.InstalledVersion();
readonly FlowLayoutPanel content = new(), buttons = new(), langs = new();
readonly Label close = new();
readonly Image? logo = Installer.Logo();
Action page;
bool installing;
// Auswahl - bleibt beim Sprachwechsel und Zurueck erhalten
string folder = Installer.RegisteredDir ?? Installer.DefaultDir;
bool desktop, startMenu;
string? installedTo;
static string L(string de, string en) => Lang.Current == "en" ? en : de;
public SetupForm()
{
bool fresh = installed == null;
desktop = fresh || Installer.HasDesktopShortcut;
startMenu = fresh || Installer.HasStartMenuShortcut;
Text = "BSO Launcher Setup";
FormBorderStyle = FormBorderStyle.None;
StartPosition = FormStartPosition.CenterScreen;
AutoScaleMode = AutoScaleMode.Dpi;
ClientSize = new Size(600, 430);
BackColor = Back;
ForeColor = Bright;
Font = new Font("Segoe UI", 10f);
DoubleBuffered = true;
try { Icon = Icon.ExtractAssociatedIcon(Environment.ProcessPath ?? Application.ExecutablePath); } catch { }
content.SetBounds(32, Header + 18, 536, 250);
content.FlowDirection = FlowDirection.TopDown;
content.WrapContents = false;
buttons.SetBounds(32, 368, 536, 44);
buttons.FlowDirection = FlowDirection.RightToLeft;
buttons.WrapContents = false;
langs.SetBounds(460, 34, 96, 30);
langs.FlowDirection = FlowDirection.LeftToRight;
langs.WrapContents = false;
close.SetBounds(560, 8, 32, 28);
close.Text = "✕";
close.ForeColor = Muted;
close.TextAlign = ContentAlignment.MiddleCenter;
close.Cursor = Cursors.Hand;
close.Click += (_, _) => { if (!installing) Close(); };
close.MouseEnter += (_, _) => close.ForeColor = Bright;
close.MouseLeave += (_, _) => close.ForeColor = Muted;
Controls.AddRange(new Control[] { content, buttons, langs, close });
MouseDown += (_, e) => { if (e.Button == MouseButtons.Left && e.Y < Header) DragWindow(); };
page = Welcome;
Go(Welcome);
}
// ---------------------------------------------------------------- Seiten
void Welcome()
{
if (installed != null && !SelfUpdate.IsNewer(version, installed))
{
bool same = installed == version;
Title(L("BSO Launcher ist schon installiert", "BSO Launcher is already installed"));
Paragraph(same
? L($"Version {installed} ist auf diesem PC schon installiert. Du kannst ihn starten oder neu installieren.",
$"Version {installed} is already installed on this PC. You can start it or install it again.")
: L($"Auf diesem PC ist schon die neuere Version {installed} installiert (dieses Setup: {version}).",
$"The newer version {installed} is already installed on this PC (this setup: {version})."));
Paragraph(Installer.InstallDir, Muted, 9f);
Button(L("Launcher starten", "Start launcher"), true, () => { Installer.StartInstalled(); Close(); });
if (same) Button(L("Neu installieren", "Reinstall"), false, () => Go(Options));
return;
}
if (installed != null)
{
Title(L($"Update auf Version {version}", $"Update to version {version}"));
Paragraph(L($"Installiert ist Version {installed}. Das Setup aktualisiert den BSO Launcher – deine Anmeldung und Einstellungen bleiben erhalten.",
$"Version {installed} is installed. This setup updates the BSO Launcher – your sign-in and settings are kept."));
}
else
{
Title(L("Willkommen", "Welcome"));
Paragraph(L($"Dieses Setup installiert den BSO Launcher {version} – Multiplayer für Bus-Simulator 2012.",
$"This setup installs the BSO Launcher {version} – multiplayer for Bus-Simulator 2012."));
Paragraph(L("Der Launcher installiert danach die BSO-Mod für dich und hält sich und die Mod selbst aktuell. Admin-Rechte brauchst du nicht.",
"The launcher then installs the BSO mod for you and keeps itself and the mod up to date. No admin rights needed."));
}
Paragraph(L("Sprache wählst du oben rechts.", "Choose your language in the top right."), Muted, 9f);
Button(L("Weiter", "Next"), true, () => Go(Options));
Button(L("Abbrechen", "Cancel"), false, Close);
}
void Options()
{
Title(L("Ordner und Verknüpfungen", "Folder and shortcuts"));
Paragraph(L("Installieren in:", "Install to:"), Muted, 9f, 0);
var row = new FlowLayoutPanel { AutoSize = true, WrapContents = false, Margin = new Padding(0, 4, 0, 4) };
var path = new TextBox { Text = folder, Width = 404, BackColor = Field, ForeColor = Bright, BorderStyle = BorderStyle.FixedSingle, Margin = new Padding(0, 2, 8, 0) };
path.TextChanged += (_, _) => folder = path.Text;
var browse = MakeButton(L("Ändern …", "Change …"), false, () =>
{
using var dialog = new FolderBrowserDialog { Description = L("Ordner für den BSO Launcher wählen", "Choose a folder for the BSO Launcher"), UseDescriptionForTitle = true };
try { dialog.InitialDirectory = Directory.Exists(folder) ? folder : Path.GetDirectoryName(folder) ?? ""; } catch { }
if (dialog.ShowDialog(this) == DialogResult.OK) path.Text = Installer.NormalizeDir(dialog.SelectedPath);
});
browse.MinimumSize = new Size(110, 30);
row.Controls.AddRange(new Control[] { path, browse });
content.Controls.Add(row);
Paragraph(L($"Braucht etwa {Installer.SelfSize / 1024 / 1024} MB · nur für deinen Windows-Benutzer",
$"Needs about {Installer.SelfSize / 1024 / 1024} MB · only for your Windows user"), Muted, 9f);
Check(L("Verknüpfung auf dem Desktop", "Desktop shortcut"), desktop, v => desktop = v);
Check(L("Eintrag im Startmenü", "Start menu entry"), startMenu, v => startMenu = v);
var error = Paragraph("", Bad, 9.5f);
Button(L("Installieren", "Install"), true, () =>
{
string dir;
try { dir = Installer.NormalizeDir(folder); }
catch { error.Text = L("Ungültiger Ordner", "Invalid folder"); return; }
if (Installer.CheckWritable(dir) is { } problem) { error.Text = Lang.T(problem); return; }
Go(() => Install(dir));
});
Button(L("Zurück", "Back"), false, () => Go(Welcome));
}
void Install(string dir)
{
Title(L("Wird installiert …", "Installing …"));
var status = Paragraph(Lang.T("Vorbereiten …"), Muted, 9.5f);
var bar = new Bar { Width = 536, Height = 8, Margin = new Padding(0, 8, 0, 0) };
content.Controls.Add(bar);
installing = true;
langs.Enabled = false;
_ = Task.Run(() =>
{
try
{
Installer.Install(dir, desktop, startMenu, (p, text) => BeginInvoke(() => { bar.Value = p; status.Text = text; }));
installedTo = dir;
BeginInvoke(() => { installing = false; langs.Enabled = true; Go(Finish); });
}
catch (Exception e)
{
BeginInvoke(() => { installing = false; langs.Enabled = true; Go(() => Failed(e.Message)); });
}
});
}
void Failed(string message)
{
Title(L("Installation fehlgeschlagen", "Installation failed"));
Paragraph(message, Bad);
Paragraph(L("Läuft der Launcher noch? Dann schliessen und nochmal versuchen.", "Is the launcher still running? Close it and try again."), Muted, 9f);
Button(L("Schliessen", "Close"), false, Close);
Button(L("Zurück", "Back"), true, () => Go(Options));
}
void Finish()
{
Title(L("Fertig!", "Done!"));
Paragraph(L($"Der BSO Launcher {version} ist installiert.", $"The BSO Launcher {version} is installed."));
Paragraph(installedTo ?? Installer.InstallDir, Muted, 9f);
Paragraph(L("Deinstallieren kannst du ihn jederzeit in den Launcher-Einstellungen oder unter Windows-Einstellungen → Apps.",
"You can uninstall it any time in the launcher settings or under Windows Settings → Apps."), Muted, 9f);
bool start = true;
Check(L("BSO Launcher jetzt starten", "Start BSO Launcher now"), true, v => start = v);
Button(L("Fertig", "Finish"), true, () => { if (start) Installer.StartInstalled(); Close(); });
}
// ---------------------------------------------------------------- Bausteine
void Go(Action next)
{
page = next;
Invalidate(); // Kopf (Sprache)
SuspendLayout();
content.Controls.Clear();
buttons.Controls.Clear();
DrawLangs();
next();
ResumeLayout();
}
void DrawLangs()
{
langs.Controls.Clear();
foreach (string l in new[] { "de", "en" })
{
var b = MakeButton(l.ToUpperInvariant(), Lang.Current == l, () =>
{
Lang.Set(l);
try { var s = Settings.Load(); s.Language = l; s.Save(); } catch { } // gleich auch fuer den Launcher
Go(page);
});
b.AutoSize = false;
b.MinimumSize = Size.Empty;
b.Size = new Size(44, 28);
b.Padding = Padding.Empty;
b.Margin = new Padding(0, 0, 4, 0);
b.Font = new Font("Segoe UI", 8.5f, FontStyle.Bold);
langs.Controls.Add(b);
}
}
void Title(string text) => Paragraph(text, Bright, 15f, 6, FontStyle.Bold);
Label Paragraph(string text, Color? color = null, float size = 10f, int below = 8, FontStyle style = FontStyle.Regular)
{
var label = new Label
{
Text = text, AutoSize = true, MaximumSize = new Size(536, 0), ForeColor = color ?? Bright,
Font = new Font("Segoe UI", size, style), Margin = new Padding(0, 0, 0, below), UseMnemonic = false
};
content.Controls.Add(label);
return label;
}
void Check(string text, bool value, Action<bool> changed)
{
var box = new CheckBox { Text = text, Checked = value, AutoSize = true, ForeColor = Bright, Margin = new Padding(0, 4, 0, 2), Cursor = Cursors.Hand };
box.CheckedChanged += (_, _) => changed(box.Checked);
content.Controls.Add(box);
}
void Button(string text, bool primary, Action click) => buttons.Controls.Add(MakeButton(text, primary, click));
static Button MakeButton(string text, bool primary, Action click)
{
var b = new Button
{
Text = text, AutoSize = true, MinimumSize = new Size(120, 36), FlatStyle = FlatStyle.Flat, Cursor = Cursors.Hand,
BackColor = primary ? Accent : Field, ForeColor = Color.White, Font = new Font("Segoe UI", 9.5f, FontStyle.Bold),
Margin = new Padding(10, 0, 0, 0), Padding = new Padding(12, 0, 12, 0), UseMnemonic = false
};
b.FlatAppearance.BorderSize = primary ? 0 : 1;
b.FlatAppearance.BorderColor = Line;
b.Click += (_, _) => click();
return b;
}
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
using (var border = new Pen(Line)) { g.DrawRectangle(border, 0, 0, ClientSize.Width - 1, ClientSize.Height - 1); g.DrawLine(border, 0, Header, ClientSize.Width, Header); }
if (logo != null) g.DrawImage(logo, 28, 22, 52, 52);
using var title = new Font("Segoe UI", 15f, FontStyle.Bold);
TextRenderer.DrawText(g, "BSO Launcher", title, new Point(92, 20), Bright);
using var sub = new Font("Segoe UI", 9f);
TextRenderer.DrawText(g, L($"Setup · Version {version}", $"Setup · version {version}"), sub, new Point(94, 52), Muted);
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
if (installing && e.CloseReason == CloseReason.UserClosing) e.Cancel = true; // nicht mitten im Kopieren abbrechen
base.OnFormClosing(e);
}
[DllImport("user32.dll")] static extern bool ReleaseCapture();
[DllImport("user32.dll")] static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
void DragWindow()
{
ReleaseCapture();
SendMessage(Handle, 0x00A1, 2, IntPtr.Zero); // WM_NCLBUTTONDOWN, HTCAPTION
}
/// <summary>Fortschrittsbalken im Launcher-Stil (Verlauf blau -> tuerkis).</summary>
sealed class Bar : Control
{
double value;
[System.ComponentModel.DesignerSerializationVisibility(System.ComponentModel.DesignerSerializationVisibility.Hidden)]
public double Value { get => value; set { this.value = Math.Clamp(value, 0, 1); Invalidate(); } }
public Bar() { DoubleBuffered = true; }
protected override void OnPaint(PaintEventArgs e)
{
var all = new Rectangle(0, 0, Width, Height);
using (var back = new SolidBrush(Color.FromArgb(0x23, 0x2a, 0x36))) e.Graphics.FillRectangle(back, all);
if (value <= 0) return;
using var fill = new LinearGradientBrush(all, Color.FromArgb(0x1f, 0x6f, 0xe5), Color.FromArgb(0x5e, 0xe4, 0xff), 0f);
e.Graphics.FillRectangle(fill, new Rectangle(0, 0, (int)(Width * value), Height));
}
}
}