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:
+84
-101
@@ -5,9 +5,10 @@ using Microsoft.Win32;
|
||||
namespace BSO.Launcher;
|
||||
|
||||
/// <summary>
|
||||
/// Installation wie bei Discord (wie beim MSCO Launcher): die heruntergeladene .exe installiert sich selbst ohne
|
||||
/// Admin-Rechte nach %LocalAppData%\Programs\BSO Launcher, legt Verknuepfungen (Desktop, Startmenue) an, traegt sich unter
|
||||
/// "Apps & Features" ein (Deinstallieren) und startet dann den installierten Launcher.
|
||||
/// Installation: die heruntergeladene .exe (BSO-Launcher-Setup.exe) ist zugleich das Setup (SetupForm). Sie kopiert sich
|
||||
/// ohne Admin-Rechte in den gewaehlten Ordner (Standard %LocalAppData%\Programs\BSO Launcher), legt die gewaehlten
|
||||
/// Verknuepfungen an, traegt sich unter "Apps & Features" ein (Deinstallieren) und startet dann den installierten Launcher.
|
||||
/// Der Ordner steht in der Registry (InstallLocation) - danach aktualisiert sich der Launcher dort selbst.
|
||||
/// </summary>
|
||||
public static class Installer
|
||||
{
|
||||
@@ -15,15 +16,57 @@ public static class Installer
|
||||
const string ExeName = "BSO-Launcher.exe";
|
||||
const string UninstallKey = @"Software\Microsoft\Windows\CurrentVersion\Uninstall\BSO-Launcher";
|
||||
|
||||
public static string InstallDir =>
|
||||
public static string DefaultDir =>
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", AppName);
|
||||
|
||||
/// <summary>Ordner der bestehenden Installation (aus "Apps & Features") oder null.</summary>
|
||||
public static string? RegisteredDir
|
||||
{
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(UninstallKey);
|
||||
return key?.GetValue("InstallLocation") is string dir && dir.Length > 0 ? dir : null;
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
}
|
||||
|
||||
public static string InstallDir => RegisteredDir ?? DefaultDir;
|
||||
public static string InstalledExe => Path.Combine(InstallDir, ExeName);
|
||||
static string Self => Environment.ProcessPath ?? Application.ExecutablePath;
|
||||
public static long SelfSize => new FileInfo(Self).Length;
|
||||
|
||||
static string StartMenuShortcut =>
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Programs), AppName + ".lnk");
|
||||
static string DesktopShortcut =>
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), AppName + ".lnk");
|
||||
public static bool HasStartMenuShortcut => File.Exists(StartMenuShortcut);
|
||||
public static bool HasDesktopShortcut => File.Exists(DesktopShortcut);
|
||||
|
||||
/// <summary>Gewaehlter Ordner -> Installationsordner: immer ein eigener Unterordner "BSO Launcher" (beim Deinstallieren
|
||||
/// wird nur der geloescht, nie z.B. "D:\Spiele").</summary>
|
||||
public static string NormalizeDir(string chosen)
|
||||
{
|
||||
string dir = Path.GetFullPath(Environment.ExpandEnvironmentVariables(chosen.Trim().Trim('"')));
|
||||
return string.Equals(Path.GetFileName(dir.TrimEnd('\\')), AppName, StringComparison.OrdinalIgnoreCase) ? dir : Path.Combine(dir, AppName);
|
||||
}
|
||||
|
||||
/// <summary>Darf dieser Benutzer dort schreiben? (Programme ohne Admin-Rechte - auch fuer die Selbst-Updates noetig.)</summary>
|
||||
public static string? CheckWritable(string dir)
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(dir);
|
||||
string probe = Path.Combine(dir, ".bso-write-test");
|
||||
File.WriteAllText(probe, "");
|
||||
File.Delete(probe);
|
||||
return null;
|
||||
}
|
||||
catch (UnauthorizedAccessException) { return "Keine Schreibrechte in diesem Ordner - bitte einen Ordner waehlen, in dem du ohne Admin-Rechte schreiben darfst"; }
|
||||
catch (Exception e) { return "Ordner nicht nutzbar: " + e.Message; }
|
||||
}
|
||||
|
||||
/// <summary>Laeuft gerade die installierte Kopie?</summary>
|
||||
public static bool RunningInstalled =>
|
||||
@@ -35,23 +78,19 @@ public static class Installer
|
||||
return FileVersionInfo.GetVersionInfo(InstalledExe).ProductVersion?.Split('+')[0];
|
||||
}
|
||||
|
||||
/// <summary>Muss installiert (oder aktualisiert) werden? false = installierte Version ist gleich/neuer.</summary>
|
||||
public static bool NeedsInstall()
|
||||
{
|
||||
string? installed = InstalledVersion();
|
||||
return installed == null || SelfUpdate.IsNewer(SelfUpdate.CurrentVersion, installed);
|
||||
}
|
||||
|
||||
/// <summary>Installieren. progress(0..1, Text).</summary>
|
||||
public static void Install(Action<double, string> progress)
|
||||
/// <summary>Installieren (auch Update/Neu-Installation, auch in einen anderen Ordner). progress(0..1, Text).</summary>
|
||||
public static void Install(string dir, bool desktop, bool startMenu, Action<double, string> progress)
|
||||
{
|
||||
string? oldExe = File.Exists(InstalledExe) ? InstalledExe : null;
|
||||
string exe = Path.Combine(dir, ExeName);
|
||||
progress(0.1, Lang.T("Laufenden Launcher schliessen …"));
|
||||
foreach (var p in Process.GetProcessesByName("BSO-Launcher"))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (p.Id == Environment.ProcessId) continue;
|
||||
if (!string.Equals(p.MainModule?.FileName, InstalledExe, StringComparison.OrdinalIgnoreCase)) continue;
|
||||
string? file = p.MainModule?.FileName;
|
||||
if (!string.Equals(file, exe, StringComparison.OrdinalIgnoreCase) && !string.Equals(file, oldExe, StringComparison.OrdinalIgnoreCase)) continue;
|
||||
p.CloseMainWindow();
|
||||
if (!p.WaitForExit(3000)) p.Kill(true);
|
||||
}
|
||||
@@ -59,37 +98,49 @@ public static class Installer
|
||||
}
|
||||
|
||||
progress(0.3, Lang.T("Dateien kopieren …"));
|
||||
Directory.CreateDirectory(InstallDir);
|
||||
string tmp = InstalledExe + ".new";
|
||||
Directory.CreateDirectory(dir);
|
||||
string tmp = exe + ".new";
|
||||
File.Copy(Self, tmp, true);
|
||||
File.Move(tmp, InstalledExe, true);
|
||||
File.Move(tmp, exe, true);
|
||||
// Vorher woanders installiert: die alte Kopie wegraeumen (nur unsere Dateien, den Ordner nur wenn leer)
|
||||
if (oldExe != null && !string.Equals(oldExe, exe, StringComparison.OrdinalIgnoreCase)) RemoveFiles(Path.GetDirectoryName(oldExe)!);
|
||||
|
||||
progress(0.6, Lang.T("Verknuepfungen anlegen …"));
|
||||
CreateShortcut(StartMenuShortcut);
|
||||
CreateShortcut(DesktopShortcut);
|
||||
Register(dir); // ab hier zeigen InstallDir/InstalledExe auf den neuen Ordner
|
||||
if (startMenu) CreateShortcut(StartMenuShortcut); else TryDelete(StartMenuShortcut);
|
||||
if (desktop) CreateShortcut(DesktopShortcut); else TryDelete(DesktopShortcut);
|
||||
|
||||
progress(0.8, Lang.T("Registrieren …"));
|
||||
Register();
|
||||
progress(1.0, Lang.T("Fertig"));
|
||||
}
|
||||
|
||||
static void TryDelete(string file) { try { File.Delete(file); } catch { } }
|
||||
|
||||
/// <summary>Unsere Dateien im Installationsordner loeschen, den Ordner nur wenn er danach leer ist.</summary>
|
||||
static void RemoveFiles(string dir)
|
||||
{
|
||||
foreach (string name in new[] { ExeName, ExeName + ".old", ExeName + ".new" }) TryDelete(Path.Combine(dir, name));
|
||||
try { if (!Directory.EnumerateFileSystemEntries(dir).Any()) Directory.Delete(dir); } catch { }
|
||||
}
|
||||
|
||||
/// <summary>Eintrag unter "Apps & Features" (bzw. dessen Version nach einem Selbst-Update).</summary>
|
||||
public static void Register()
|
||||
public static void Register(string? dir = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
dir ??= InstallDir;
|
||||
string exe = Path.Combine(dir, ExeName);
|
||||
using var key = Registry.CurrentUser.CreateSubKey(UninstallKey);
|
||||
key.SetValue("DisplayName", AppName);
|
||||
key.SetValue("DisplayVersion", SelfUpdate.CurrentVersion);
|
||||
key.SetValue("Publisher", "MBM.wtf");
|
||||
key.SetValue("DisplayIcon", InstalledExe + ",0");
|
||||
key.SetValue("InstallLocation", InstallDir);
|
||||
key.SetValue("UninstallString", $"\"{InstalledExe}\" --uninstall");
|
||||
key.SetValue("QuietUninstallString", $"\"{InstalledExe}\" --uninstall --quiet");
|
||||
key.SetValue("DisplayIcon", exe + ",0");
|
||||
key.SetValue("InstallLocation", dir);
|
||||
key.SetValue("UninstallString", $"\"{exe}\" --uninstall");
|
||||
key.SetValue("QuietUninstallString", $"\"{exe}\" --uninstall --quiet");
|
||||
key.SetValue("URLInfoAbout", Api.BaseUrl);
|
||||
key.SetValue("NoModify", 1, RegistryValueKind.DWord);
|
||||
key.SetValue("NoRepair", 1, RegistryValueKind.DWord);
|
||||
try { key.SetValue("EstimatedSize", (int)(new FileInfo(InstalledExe).Length / 1024), RegistryValueKind.DWord); } catch { }
|
||||
try { key.SetValue("EstimatedSize", (int)(new FileInfo(exe).Length / 1024), RegistryValueKind.DWord); } catch { }
|
||||
}
|
||||
catch { /* Eintrag ist nur Komfort */ }
|
||||
}
|
||||
@@ -119,8 +170,9 @@ public static class Installer
|
||||
Process.Start(start);
|
||||
}
|
||||
|
||||
/// <summary>Ordner/Dateien loeschen, sobald dieser Prozess beendet ist (seine .exe ist bis dahin gesperrt).</summary>
|
||||
static void RemoveAfterExit(params string[] paths)
|
||||
/// <summary>Ordner/Dateien loeschen, sobald dieser Prozess beendet ist (seine .exe ist bis dahin gesperrt). Danach
|
||||
/// emptyDir entfernen, aber nur wenn er leer ist (der Benutzer hat den Ordner gewaehlt - fremde Dateien bleiben).</summary>
|
||||
static void RemoveAfterExit(string emptyDir, params string[] paths)
|
||||
{
|
||||
static string Quote(string p) => "'" + p.Replace("'", "''") + "'";
|
||||
string list = string.Join(",", paths.Where(p => !string.IsNullOrWhiteSpace(p)).Select(Quote));
|
||||
@@ -128,7 +180,8 @@ public static class Installer
|
||||
$"Wait-Process -Id {Environment.ProcessId} -ErrorAction SilentlyContinue; " +
|
||||
$"foreach ($i in 1..30) {{ $left = $false; foreach ($p in @({list})) {{ " +
|
||||
"if (Test-Path -LiteralPath $p) { Remove-Item -LiteralPath $p -Recurse -Force -ErrorAction SilentlyContinue; " +
|
||||
"if (Test-Path -LiteralPath $p) { $left = $true } } }; if (-not $left) { break }; Start-Sleep -Seconds 1 }";
|
||||
"if (Test-Path -LiteralPath $p) { $left = $true } } }; if (-not $left) { break }; Start-Sleep -Seconds 1 }; " +
|
||||
$"$d = {Quote(emptyDir)}; if ((Test-Path -LiteralPath $d) -and -not (Get-ChildItem -LiteralPath $d -Force)) {{ Remove-Item -LiteralPath $d -Force }}";
|
||||
var psi = new ProcessStartInfo("powershell.exe") { CreateNoWindow = true, UseShellExecute = false, WindowStyle = ProcessWindowStyle.Hidden };
|
||||
foreach (string a in new[] { "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command", script })
|
||||
psi.ArgumentList.Add(a);
|
||||
@@ -160,7 +213,7 @@ public static class Installer
|
||||
try { File.Delete(DesktopShortcut); } catch { }
|
||||
try { Registry.CurrentUser.DeleteSubKeyTree(UninstallKey, false); } catch { }
|
||||
// Die laufende .exe laesst sich nicht selbst loeschen: aufraeumen, sobald dieser Prozess beendet ist
|
||||
RemoveAfterExit(dir, Settings.Folder);
|
||||
RemoveAfterExit(dir, Path.Combine(dir, ExeName), Path.Combine(dir, ExeName + ".old"), Path.Combine(dir, ExeName + ".new"), Settings.Folder);
|
||||
if (!quiet)
|
||||
MessageBox.Show(Lang.T(problems.Count == 0
|
||||
? "BSO wurde komplett deinstalliert."
|
||||
@@ -175,73 +228,3 @@ public static class Installer
|
||||
return s == null ? null : Image.FromStream(s);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Kleines Installationsfenster (wie der Discord-Installer): Logo, Status, Fortschrittsbalken.</summary>
|
||||
public sealed class InstallForm : Form
|
||||
{
|
||||
readonly Label status = new();
|
||||
double progress;
|
||||
string? error;
|
||||
|
||||
public InstallForm()
|
||||
{
|
||||
FormBorderStyle = FormBorderStyle.None;
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
ClientSize = new Size(420, 260);
|
||||
BackColor = Color.FromArgb(0x15, 0x19, 0x22);
|
||||
ShowInTaskbar = true;
|
||||
Text = "BSO Launcher wird installiert";
|
||||
DoubleBuffered = true;
|
||||
try { Icon = Icon.ExtractAssociatedIcon(Environment.ProcessPath ?? Application.ExecutablePath); } catch { }
|
||||
|
||||
status.ForeColor = Color.FromArgb(0x8b, 0x95, 0xa5);
|
||||
status.Font = new Font("Segoe UI", 9.5f);
|
||||
status.TextAlign = ContentAlignment.MiddleCenter;
|
||||
status.SetBounds(20, 178, 380, 24);
|
||||
status.Text = Lang.T("Vorbereiten …");
|
||||
Controls.Add(status);
|
||||
Shown += async (_, _) => await Run();
|
||||
}
|
||||
|
||||
protected override void OnPaint(PaintEventArgs e)
|
||||
{
|
||||
var g = e.Graphics;
|
||||
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
|
||||
using (var border = new Pen(Color.FromArgb(0x26, 0x2c, 0x38))) g.DrawRectangle(border, 0, 0, Width - 1, Height - 1);
|
||||
var logo = Installer.Logo();
|
||||
if (logo != null) g.DrawImage(logo, (Width - 72) / 2, 34, 72, 72);
|
||||
using var title = new Font("Segoe UI", 15f, FontStyle.Bold);
|
||||
TextRenderer.DrawText(g, "BSO Launcher", title, new Rectangle(0, 116, Width, 34), Color.White, TextFormatFlags.HorizontalCenter);
|
||||
using var sub = new Font("Segoe UI", 9f);
|
||||
TextRenderer.DrawText(g, "Bus Simulator Online", sub, new Rectangle(0, 148, Width, 22), Color.FromArgb(0x8b, 0x95, 0xa5), TextFormatFlags.HorizontalCenter);
|
||||
// Fortschrittsbalken
|
||||
var bar = new Rectangle(40, 214, Width - 80, 8);
|
||||
using (var back = new SolidBrush(Color.FromArgb(0x23, 0x2a, 0x36))) g.FillRectangle(back, bar);
|
||||
if (progress > 0)
|
||||
{
|
||||
var fill = new Rectangle(bar.X, bar.Y, (int)(bar.Width * progress), bar.Height);
|
||||
using var brush = new System.Drawing.Drawing2D.LinearGradientBrush(bar, Color.FromArgb(0x1f, 0x6f, 0xe5), Color.FromArgb(0x5e, 0xe4, 0xff), 0f);
|
||||
g.FillRectangle(error != null ? Brushes.IndianRed : brush, fill);
|
||||
}
|
||||
}
|
||||
|
||||
async Task Run()
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Run(() => Installer.Install((p, text) => BeginInvoke(() => { progress = p; status.Text = text; Invalidate(); })));
|
||||
await Task.Delay(400);
|
||||
Installer.StartInstalled();
|
||||
Close();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
error = e.Message;
|
||||
status.ForeColor = Color.FromArgb(0xff, 0x8a, 0x8d);
|
||||
status.Text = Lang.T("Installation fehlgeschlagen: " + e.Message);
|
||||
Invalidate();
|
||||
await Task.Delay(6000);
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,9 @@ public static class Lang
|
||||
("Erst Bus-Simulator 2012 schliessen - solange es laeuft, sind die Dateien in Benutzung.", "Close Bus-Simulator 2012 first - its files are in use while it runs."),
|
||||
("BSO wurde komplett deinstalliert.", "BSO was uninstalled completely."),
|
||||
("BSO wurde deinstalliert. Einiges liess sich nicht entfernen:\n\n{0}", "BSO was uninstalled. Some things could not be removed:\n\n{0}"),
|
||||
("Keine Schreibrechte in diesem Ordner - bitte einen Ordner waehlen, in dem du ohne Admin-Rechte schreiben darfst",
|
||||
"No write access to this folder - please choose a folder you can write to without admin rights"),
|
||||
("Ordner nicht nutzbar: {0}", "Folder not usable: {0}"),
|
||||
("Neuigkeiten nicht verfuegbar", "News not available"),
|
||||
("Ordner \"Bus-Simulator 2012\" waehlen", "Choose the \"Bus-Simulator 2012\" folder"),
|
||||
("Konto konnte nicht geprueft werden: {0}", "Account could not be checked: {0}")
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user